feat: add MenuItem.userAccelerator property (#26682)

* 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
This commit is contained in:
Samuel Attard 2021-06-29 16:28:16 -07:00 committed by GitHub
parent 3e69985b76
commit da9261497e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 9 deletions

View file

@ -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`

View file

@ -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.

View file

@ -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);
}
@ -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();
}

View file

@ -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;
@ -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;

View file

@ -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 {
@ -52,6 +71,32 @@ void MenuMac::PopupAt(BaseWindow* window,
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,

View file

@ -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;

View file

@ -315,11 +315,9 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
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);
@ -437,7 +435,17 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
}
}
}
[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,