Skip to content

Commit

Permalink
feat: add MenuItem.userAccelerator property (electron#26682)
Browse files Browse the repository at this point in the history
* feat: add MenuItem.userAccelerator property

This allows folks to read the user-assigned accelerator that macOS users can provide in system preferences. Useful for showing in-app shortcut help dialogs, you need to know if the accelerator you provided is not being used in favor of a user assigned one.

* chore: update syntax

* chore: add safety check for command index being -1
  • Loading branch information
MarshallOfSound authored and BlackHole1 committed Aug 27, 2021
1 parent 46c1f94 commit 60f5c4b
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 9 deletions.
8 changes: 7 additions & 1 deletion docs/api/menu-item.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,13 @@ A `String` (optional) indicating the item's role, if set. Can be `undo`, `redo`,

#### `menuItem.accelerator`

A `Accelerator` (optional) indicating the item's accelerator, if set.
An `Accelerator` (optional) indicating the item's accelerator, if set.

#### `menuItem.userAccelerator` _Readonly_ _macOS_

An `Accelerator | null` indicating the item's [user-assigned accelerator](https://developer.apple.com/documentation/appkit/nsmenuitem/1514850-userkeyequivalent?language=objc) for the menu item.

**Note:** This property is only initialized after the `MenuItem` has been added to a `Menu`. Either via `Menu.buildFromTemplate` or via `Menu.append()/insert()`. Accessing before initialization will just return `null`.

#### `menuItem.icon`

Expand Down
9 changes: 9 additions & 0 deletions lib/browser/api/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ const MenuItem = function (this: any, options: any) {

this.overrideReadOnlyProperty('commandId', ++nextCommandId);

Object.defineProperty(this, 'userAccelerator', {
get: () => {
if (process.platform !== 'darwin') return null;
if (!this.menu) return null;
return this.menu._getUserAcceleratorAt(this.commandId);
},
enumerable: true
});

const click = options.click;
this.click = (event: Event, focusedWindow: BrowserWindow, focusedWebContents: WebContents) => {
// Manually flip the checked flags when clicked.
Expand Down
5 changes: 4 additions & 1 deletion shell/browser/api/electron_api_menu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ void Menu::Clear() {
model_->Clear();
}

int Menu::GetIndexOfCommandId(int command_id) {
int Menu::GetIndexOfCommandId(int command_id) const {
return model_->GetIndexOfCommandId(command_id);
}

Expand Down Expand Up @@ -299,6 +299,9 @@ v8::Local<v8::ObjectTemplate> Menu::FillObjectTemplate(
.SetMethod("closePopupAt", &Menu::ClosePopupAt)
#if DCHECK_IS_ON()
.SetMethod("getAcceleratorTextAt", &Menu::GetAcceleratorTextAtForTesting)
#endif
#if defined(OS_MAC)
.SetMethod("_getUserAcceleratorAt", &Menu::GetUserAcceleratorAt)
#endif
.Build();
}
Expand Down
3 changes: 2 additions & 1 deletion shell/browser/api/electron_api_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Menu : public gin::Wrappable<Menu>,
bool GetSharingItemForCommandId(
int command_id,
ElectronMenuModel::SharingItem* item) const override;
v8::Local<v8::Value> GetUserAcceleratorAt(int command_id) const;
#endif
void ExecuteCommand(int command_id, int event_flags) override;
void OnMenuWillShow(ui::SimpleMenuModel* source) override;
Expand Down Expand Up @@ -108,7 +109,7 @@ class Menu : public gin::Wrappable<Menu>,
void SetToolTip(int index, const std::u16string& toolTip);
void SetRole(int index, const std::u16string& role);
void Clear();
int GetIndexOfCommandId(int command_id);
int GetIndexOfCommandId(int command_id) const;
int GetItemCount() const;
int GetCommandIdAt(int index) const;
std::u16string GetLabelAt(int index) const;
Expand Down
45 changes: 45 additions & 0 deletions shell/browser/api/electron_api_menu_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,31 @@
#include "content/public/browser/web_contents.h"
#include "shell/browser/native_window.h"
#include "shell/browser/unresponsive_suppressor.h"
#include "shell/common/keyboard_util.h"
#include "shell/common/node_includes.h"

namespace {

static scoped_nsobject<NSMenu> applicationMenu_;

ui::Accelerator GetAcceleratorFromKeyEquivalentAndModifierMask(
NSString* key_equivalent,
NSUInteger modifier_mask) {
absl::optional<char16_t> shifted_char;
ui::KeyboardCode code = electron::KeyboardCodeFromStr(
base::SysNSStringToUTF8(key_equivalent), &shifted_char);
int modifiers = 0;
if (modifier_mask & NSEventModifierFlagShift)
modifiers |= ui::EF_SHIFT_DOWN;
if (modifier_mask & NSEventModifierFlagControl)
modifiers |= ui::EF_CONTROL_DOWN;
if (modifier_mask & NSEventModifierFlagOption)
modifiers |= ui::EF_ALT_DOWN;
if (modifier_mask & NSEventModifierFlagCommand)
modifiers |= ui::EF_COMMAND_DOWN;
return ui::Accelerator(code, modifiers);
}

} // namespace

namespace electron {
Expand Down Expand Up @@ -52,6 +71,32 @@
base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(popup));
}

v8::Local<v8::Value> Menu::GetUserAcceleratorAt(int command_id) const {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
if (![NSMenuItem usesUserKeyEquivalents])
return v8::Null(isolate);

auto controller = base::scoped_nsobject<ElectronMenuController>(
[[ElectronMenuController alloc] initWithModel:model()
useDefaultAccelerator:NO]);

int command_index = GetIndexOfCommandId(command_id);
if (command_index == -1)
return v8::Null(isolate);

base::scoped_nsobject<NSMenuItem> item =
[controller makeMenuItemForIndex:command_index fromModel:model()];
if ([[item userKeyEquivalent] length] == 0)
return v8::Null(isolate);

NSString* user_key_equivalent = [item keyEquivalent];
NSUInteger user_modifier_mask = [item keyEquivalentModifierMask];
ui::Accelerator accelerator = GetAcceleratorFromKeyEquivalentAndModifierMask(
user_key_equivalent, user_modifier_mask);

return gin::ConvertToV8(isolate, accelerator.GetShortcutText());
}

void MenuMac::PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
int32_t window_id,
int x,
Expand Down
4 changes: 4 additions & 0 deletions shell/browser/ui/cocoa/electron_menu_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class ElectronMenuModel;
// default initializer was used, then this will create the menu on first call.
- (NSMenu*)menu;

- (base::scoped_nsobject<NSMenuItem>)
makeMenuItemForIndex:(NSInteger)index
fromModel:(electron::ElectronMenuModel*)model;

// Whether the menu is currently open.
- (BOOL)isMenuOpen;

Expand Down
20 changes: 14 additions & 6 deletions shell/browser/ui/cocoa/electron_menu_controller.mm
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,9 @@ - (NSMenuItem*)menuItemForService:(NSSharingService*)service
return item.autorelease();
}

// Adds an item or a hierarchical menu to the item at the |index|,
// associated with the entry in the model identified by |modelIndex|.
- (void)addItemToMenu:(NSMenu*)menu
atIndex:(NSInteger)index
fromModel:(electron::ElectronMenuModel*)model {
- (base::scoped_nsobject<NSMenuItem>)
makeMenuItemForIndex:(NSInteger)index
fromModel:(electron::ElectronMenuModel*)model {
std::u16string label16 = model->GetLabelAt(index);
NSString* label = l10n_util::FixUpWindowsStyleLabel(label16);

Expand Down Expand Up @@ -437,7 +435,17 @@ - (void)addItemToMenu:(NSMenu*)menu
}
}
}
[menu insertItem:item atIndex:index];

return item;
}

// Adds an item or a hierarchical menu to the item at the |index|,
// associated with the entry in the model identified by |modelIndex|.
- (void)addItemToMenu:(NSMenu*)menu
atIndex:(NSInteger)index
fromModel:(electron::ElectronMenuModel*)model {
[menu insertItem:[self makeMenuItemForIndex:index fromModel:model]
atIndex:index];
}

// Called before the menu is to be displayed to update the state (enabled,
Expand Down

0 comments on commit 60f5c4b

Please sign in to comment.