feat: add setSuspended and isSuspended to globalShortcut (#50425)

Adds the ability to temporarily suspend and resume global shortcut
handling via `globalShortcut.setSuspended()` and query the current
state via `globalShortcut.isSuspended()`. When suspended, registered
shortcuts stop listening and new registrations are rejected. When
resumed, previously registered shortcuts are automatically restored.
This commit is contained in:
Shelley Vohr
2026-04-07 15:21:43 +02:00
committed by GitHub
parent 9ba299afff
commit 2e74ad2c68
4 changed files with 218 additions and 36 deletions

View File

@@ -148,3 +148,34 @@ added:
-->
Unregisters all of the global shortcuts.
### `globalShortcut.setSuspended(suspended)`
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/50425
```
-->
* `suspended` boolean - Whether global shortcut handling should be suspended.
Suspends or resumes global shortcut handling. When suspended, all registered
global shortcuts will stop listening for key presses. When resumed, all
previously registered shortcuts will begin listening again. New shortcut
registrations will fail while handling is suspended.
This can be useful when you want to temporarily allow the user to press key
combinations without your application intercepting them, for example while
displaying a UI to rebind shortcuts.
### `globalShortcut.isSuspended()`
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/50425
```
-->
Returns `boolean` - Whether global shortcut handling is currently suspended.

View File

@@ -232,6 +232,30 @@ void GlobalShortcut::UnregisterAll() {
}
}
void GlobalShortcut::SetSuspended(bool suspend) {
if (!electron::Browser::Get()->is_ready()) {
gin_helper::ErrorThrower(JavascriptEnvironment::GetIsolate())
.ThrowError("globalShortcut cannot be used before the app is ready");
return;
}
if (ui::GlobalAcceleratorListener::GetInstance()) {
ui::GlobalAcceleratorListener::GetInstance()->SetShortcutHandlingSuspended(
suspend);
}
}
bool GlobalShortcut::IsSuspended() {
if (!electron::Browser::Get()->is_ready()) {
gin_helper::ErrorThrower(JavascriptEnvironment::GetIsolate())
.ThrowError("globalShortcut cannot be used before the app is ready");
return false;
}
if (ui::GlobalAcceleratorListener::GetInstance())
return ui::GlobalAcceleratorListener::GetInstance()
->IsShortcutHandlingSuspended();
return false;
}
// static
gin_helper::Handle<GlobalShortcut> GlobalShortcut::Create(
v8::Isolate* isolate) {
@@ -247,7 +271,9 @@ gin::ObjectTemplateBuilder GlobalShortcut::GetObjectTemplateBuilder(
.SetMethod("register", &GlobalShortcut::Register)
.SetMethod("isRegistered", &GlobalShortcut::IsRegistered)
.SetMethod("unregister", &GlobalShortcut::Unregister)
.SetMethod("unregisterAll", &GlobalShortcut::UnregisterAll);
.SetMethod("unregisterAll", &GlobalShortcut::UnregisterAll)
.SetMethod("setSuspended", &GlobalShortcut::SetSuspended)
.SetMethod("isSuspended", &GlobalShortcut::IsSuspended);
}
const char* GlobalShortcut::GetTypeName() {

View File

@@ -55,6 +55,8 @@ class GlobalShortcut final
void Unregister(const ui::Accelerator& accelerator);
void UnregisterSome(const std::vector<ui::Accelerator>& accelerators);
void UnregisterAll();
void SetSuspended(bool suspend);
bool IsSuspended();
// GlobalAcceleratorListener::Observer implementation.
void OnKeyPressed(const ui::Accelerator& accelerator) override;

View File

@@ -10,57 +10,180 @@ ifdescribe(process.platform !== 'win32')('globalShortcut module', () => {
globalShortcut.unregisterAll();
});
it('can register and unregister single accelerators', () => {
const combinations = [...singleModifierCombinations, ...doubleModifierCombinations];
afterEach(() => {
globalShortcut.unregisterAll();
});
combinations.forEach((accelerator) => {
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Initially registered for ${accelerator}`);
describe('register', () => {
it('can register and unregister single accelerators', () => {
const combinations = [...singleModifierCombinations, ...doubleModifierCombinations];
globalShortcut.register(accelerator, () => { });
expect(globalShortcut.isRegistered(accelerator)).to.be.true(`Registration failed for ${accelerator}`);
combinations.forEach((accelerator) => {
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Initially registered for ${accelerator}`);
globalShortcut.unregister(accelerator);
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Unregistration failed for ${accelerator}`);
globalShortcut.register(accelerator, () => { });
expect(globalShortcut.isRegistered(accelerator)).to.be.true(`Registration failed for ${accelerator}`);
globalShortcut.register(accelerator, () => { });
expect(globalShortcut.isRegistered(accelerator)).to.be.true(`Re-registration failed for ${accelerator}`);
globalShortcut.unregister(accelerator);
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Unregistration failed for ${accelerator}`);
globalShortcut.unregisterAll();
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Re-unregistration failed for ${accelerator}`);
globalShortcut.register(accelerator, () => { });
expect(globalShortcut.isRegistered(accelerator)).to.be.true(`Re-registration failed for ${accelerator}`);
globalShortcut.unregisterAll();
expect(globalShortcut.isRegistered(accelerator)).to.be.false(`Re-unregistration failed for ${accelerator}`);
});
});
it('returns true on successful registration', () => {
const result = globalShortcut.register('CmdOrCtrl+Q', () => {});
expect(result).to.be.true();
});
it('can re-register the same accelerator without error', () => {
globalShortcut.register('CmdOrCtrl+Z', () => {});
expect(() => {
globalShortcut.register('CmdOrCtrl+Z', () => {});
}).to.not.throw();
expect(globalShortcut.isRegistered('CmdOrCtrl+Z')).to.be.true();
});
});
it('can register and unregister multiple accelerators', () => {
const accelerators = ['CmdOrCtrl+X', 'CmdOrCtrl+Y'];
describe('registerAll', () => {
it('can register and unregister multiple accelerators', () => {
const accelerators = ['CmdOrCtrl+X', 'CmdOrCtrl+Y'];
expect(globalShortcut.isRegistered(accelerators[0])).to.be.false('first initially unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second initially unregistered');
expect(globalShortcut.isRegistered(accelerators[0])).to.be.false('first initially unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second initially unregistered');
globalShortcut.registerAll(accelerators, () => {});
globalShortcut.registerAll(accelerators, () => {});
expect(globalShortcut.isRegistered(accelerators[0])).to.be.true('first registration worked');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.true('second registration worked');
expect(globalShortcut.isRegistered(accelerators[0])).to.be.true('first registration worked');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.true('second registration worked');
globalShortcut.unregisterAll();
globalShortcut.unregisterAll();
expect(globalShortcut.isRegistered(accelerators[0])).to.be.false('first unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second unregistered');
expect(globalShortcut.isRegistered(accelerators[0])).to.be.false('first unregistered');
expect(globalShortcut.isRegistered(accelerators[1])).to.be.false('second unregistered');
});
it('returns true on successful registration', () => {
const result = globalShortcut.registerAll(['CmdOrCtrl+Q', 'CmdOrCtrl+W'], () => {});
expect(result).to.be.true();
});
it('does not crash when registering media keys as global shortcuts', () => {
const accelerators = [
'VolumeUp',
'VolumeDown',
'VolumeMute',
'MediaNextTrack',
'MediaPreviousTrack',
'MediaStop', 'MediaPlayPause'
];
expect(() => {
globalShortcut.registerAll(accelerators, () => {});
}).to.not.throw();
});
});
it('does not crash when registering media keys as global shortcuts', () => {
const accelerators = [
'VolumeUp',
'VolumeDown',
'VolumeMute',
'MediaNextTrack',
'MediaPreviousTrack',
'MediaStop', 'MediaPlayPause'
];
describe('isRegistered', () => {
it('returns false for an accelerator that was never registered', () => {
expect(globalShortcut.isRegistered('CmdOrCtrl+Shift+F9')).to.be.false();
});
expect(() => {
globalShortcut.registerAll(accelerators, () => {});
}).to.not.throw();
it('returns false after the accelerator is unregistered', () => {
globalShortcut.register('CmdOrCtrl+J', () => {});
globalShortcut.unregister('CmdOrCtrl+J');
expect(globalShortcut.isRegistered('CmdOrCtrl+J')).to.be.false();
});
});
globalShortcut.unregisterAll();
describe('unregister', () => {
it('does not throw when unregistering a non-registered accelerator', () => {
expect(() => {
globalShortcut.unregister('CmdOrCtrl+Shift+F8');
}).to.not.throw();
});
it('does not affect other registered shortcuts', () => {
globalShortcut.register('CmdOrCtrl+A', () => {});
globalShortcut.register('CmdOrCtrl+B', () => {});
globalShortcut.register('CmdOrCtrl+C', () => {});
globalShortcut.unregister('CmdOrCtrl+B');
expect(globalShortcut.isRegistered('CmdOrCtrl+A')).to.be.true('A should still be registered');
expect(globalShortcut.isRegistered('CmdOrCtrl+B')).to.be.false('B should be unregistered');
expect(globalShortcut.isRegistered('CmdOrCtrl+C')).to.be.true('C should still be registered');
});
});
describe('unregisterAll', () => {
it('does not throw when no shortcuts are registered', () => {
expect(() => {
globalShortcut.unregisterAll();
}).to.not.throw();
});
it('unregisters all previously registered shortcuts', () => {
globalShortcut.register('CmdOrCtrl+A', () => {});
globalShortcut.register('CmdOrCtrl+B', () => {});
globalShortcut.register('CmdOrCtrl+C', () => {});
globalShortcut.unregisterAll();
expect(globalShortcut.isRegistered('CmdOrCtrl+A')).to.be.false();
expect(globalShortcut.isRegistered('CmdOrCtrl+B')).to.be.false();
expect(globalShortcut.isRegistered('CmdOrCtrl+C')).to.be.false();
});
it('allows re-registration after clearing all shortcuts', () => {
globalShortcut.register('CmdOrCtrl+A', () => {});
globalShortcut.unregisterAll();
const result = globalShortcut.register('CmdOrCtrl+A', () => {});
expect(result).to.be.true();
expect(globalShortcut.isRegistered('CmdOrCtrl+A')).to.be.true();
});
});
describe('setSuspended / isSuspended', () => {
afterEach(() => {
globalShortcut.setSuspended(false);
});
it('is not suspended by default', () => {
expect(globalShortcut.isSuspended()).to.be.false();
});
it('can suspend and resume shortcut handling', () => {
globalShortcut.setSuspended(true);
expect(globalShortcut.isSuspended()).to.be.true();
globalShortcut.setSuspended(false);
expect(globalShortcut.isSuspended()).to.be.false();
});
it('can be called multiple times with the same value', () => {
globalShortcut.setSuspended(true);
globalShortcut.setSuspended(true);
expect(globalShortcut.isSuspended()).to.be.true();
globalShortcut.setSuspended(false);
globalShortcut.setSuspended(false);
expect(globalShortcut.isSuspended()).to.be.false();
});
it('does not affect existing registrations', () => {
globalShortcut.register('CmdOrCtrl+A', () => {});
globalShortcut.setSuspended(true);
expect(globalShortcut.isRegistered('CmdOrCtrl+A')).to.be.true();
globalShortcut.setSuspended(false);
expect(globalShortcut.isRegistered('CmdOrCtrl+A')).to.be.true();
});
});
});