diff --git a/docs/api/menu-item.md b/docs/api/menu-item.md index 8ee760d1cb44..980b27d2dee3 100644 --- a/docs/api/menu-item.md +++ b/docs/api/menu-item.md @@ -20,8 +20,14 @@ See [`Menu`](menu.md) for examples. * `event` [KeyboardEvent](structures/keyboard-event.md) * `role` string (optional) - Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteAndMatchStyle`, `delete`, `selectAll`, `reload`, `forceReload`, `toggleDevTools`, `resetZoom`, `zoomIn`, `zoomOut`, `toggleSpellChecker`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, `showSubstitutions`, `toggleSmartQuotes`, `toggleSmartDashes`, `toggleTextReplacement`, `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `shareMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `showAllTabs`, `mergeAllWindows`, `clearRecentDocuments`, `moveTabToNewWindow` or `windowMenu` - Define the action of the menu item, when specified the `click` property will be ignored. See [roles](#roles). - * `type` string (optional) - Can be `normal`, `separator`, `submenu`, `checkbox` or - `radio`. + * `type` string (optional) + * `normal` + * `separator` + * `submenu` + * `checkbox` + * `radio` + * `header` - Only available on macOS 14 and up. + * `palette` - Only available on macOS 14 and up. * `label` string (optional) * `sublabel` string (optional) _macOS_ - Available in macOS >= 14.4 * `toolTip` string (optional) _macOS_ - Hover text for this menu item. @@ -162,7 +168,10 @@ item's submenu, if present. #### `menuItem.type` -A `string` indicating the type of the item. Can be `normal`, `separator`, `submenu`, `checkbox` or `radio`. +A `string` indicating the type of the item. Can be `normal`, `separator`, `submenu`, `checkbox`, `radio`, `header` or `palette`. + +> [!NOTE] +> `header` and `palette` are only available on macOS 14 and up. #### `menuItem.role` diff --git a/lib/browser/api/menu-item.ts b/lib/browser/api/menu-item.ts index 1ff4836caf30..6c58a80d33c6 100644 --- a/lib/browser/api/menu-item.ts +++ b/lib/browser/api/menu-item.ts @@ -71,7 +71,7 @@ const MenuItem = function (this: any, options: any) { }; }; -MenuItem.types = ['normal', 'separator', 'submenu', 'checkbox', 'radio']; +MenuItem.types = ['normal', 'separator', 'submenu', 'checkbox', 'radio', 'header', 'palette']; MenuItem.prototype.getDefaultRoleAccelerator = function () { return roles.getDefaultAccelerator(this.role); diff --git a/lib/browser/api/menu.ts b/lib/browser/api/menu.ts index 8e0a63f76465..10cad67f10c4 100644 --- a/lib/browser/api/menu.ts +++ b/lib/browser/api/menu.ts @@ -143,6 +143,9 @@ Menu.prototype.insert = function (pos, item) { if (item.toolTip) this.setToolTip(pos, item.toolTip); if (item.icon) this.setIcon(pos, item.icon); if (item.role) this.setRole(pos, item.role); + if (item.type === 'palette' || item.type === 'header') { + this.setCustomType(pos, item.type); + } // Make menu accessible to items. item.overrideReadOnlyProperty('menu', this); @@ -264,9 +267,11 @@ function removeExtraSeparators (items: (MenuItemConstructorOptions | MenuItem)[] function insertItemByType (this: MenuType, item: MenuItem, pos: number) { const types = { normal: () => this.insertItem(pos, item.commandId, item.label), + header: () => this.insertItem(pos, item.commandId, item.label), checkbox: () => this.insertCheckItem(pos, item.commandId, item.label), separator: () => this.insertSeparator(pos), submenu: () => this.insertSubMenu(pos, item.commandId, item.label, item.submenu), + palette: () => this.insertSubMenu(pos, item.commandId, item.label, item.submenu), radio: () => { // Grouping radio menu items item.overrideReadOnlyProperty('groupId', generateGroupId(this.items, pos)); diff --git a/shell/browser/api/electron_api_menu.cc b/shell/browser/api/electron_api_menu.cc index 595a3513c499..da093a8f4002 100644 --- a/shell/browser/api/electron_api_menu.cc +++ b/shell/browser/api/electron_api_menu.cc @@ -213,6 +213,10 @@ void Menu::SetRole(int index, const std::u16string& role) { model_->SetRole(index, role); } +void Menu::SetCustomType(int index, const std::u16string& customType) { + model_->SetCustomType(index, customType); +} + void Menu::Clear() { model_->Clear(); } @@ -286,6 +290,7 @@ void Menu::FillObjectTemplate(v8::Isolate* isolate, .SetMethod("setSublabel", &Menu::SetSublabel) .SetMethod("setToolTip", &Menu::SetToolTip) .SetMethod("setRole", &Menu::SetRole) + .SetMethod("setCustomType", &Menu::SetCustomType) .SetMethod("clear", &Menu::Clear) .SetMethod("getIndexOfCommandId", &Menu::GetIndexOfCommandId) .SetMethod("getItemCount", &Menu::GetItemCount) diff --git a/shell/browser/api/electron_api_menu.h b/shell/browser/api/electron_api_menu.h index 6b5ee0635da3..06fb805c06da 100644 --- a/shell/browser/api/electron_api_menu.h +++ b/shell/browser/api/electron_api_menu.h @@ -116,6 +116,7 @@ class Menu : public gin::Wrappable, void SetSublabel(int index, const std::u16string& sublabel); void SetToolTip(int index, const std::u16string& toolTip); void SetRole(int index, const std::u16string& role); + void SetCustomType(int index, const std::u16string& customType); void Clear(); int GetIndexOfCommandId(int command_id) const; int GetItemCount() const; diff --git a/shell/browser/ui/cocoa/electron_menu_controller.mm b/shell/browser/ui/cocoa/electron_menu_controller.mm index 71c9d8b49e86..1e78e57f01dc 100644 --- a/shell/browser/ui/cocoa/electron_menu_controller.mm +++ b/shell/browser/ui/cocoa/electron_menu_controller.mm @@ -330,6 +330,17 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { } } + std::u16string role = model->GetRoleAt(index); + electron::ElectronMenuModel::ItemType type = model->GetTypeAt(index); + std::u16string customType = model->GetCustomTypeAt(index); + + // The sectionHeaderWithTitle menu item is only available in macOS 14.0+. + if (@available(macOS 14, *)) { + if (customType == u"header") { + item = [NSMenuItem sectionHeaderWithTitle:label]; + } + } + // If the menu item has an icon, set it. ui::ImageModel icon = model->GetIconAt(index); if (icon.IsImage()) @@ -338,9 +349,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { std::u16string toolTip = model->GetToolTipAt(index); [item setToolTip:base::SysUTF16ToNSString(toolTip)]; - std::u16string role = model->GetRoleAt(index); - electron::ElectronMenuModel::ItemType type = model->GetTypeAt(index); - if (role == u"services") { std::u16string title = u"Services"; NSString* sub_label = l10n_util::FixUpWindowsStyleLabel(title); @@ -372,6 +380,14 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) { NSMenu* submenu = MenuHasVisibleItems(submenuModel) ? [self menuFromModel:submenuModel] : MakeEmptySubmenu(); + + // NSMenuPresentationStylePalette is only available in macOS 14.0+. + if (@available(macOS 14, *)) { + if (customType == u"palette") { + submenu.presentationStyle = NSMenuPresentationStylePalette; + } + } + [submenu setTitle:[item title]]; [item setSubmenu:submenu]; diff --git a/shell/browser/ui/electron_menu_model.cc b/shell/browser/ui/electron_menu_model.cc index fd854a6313bb..5795514d3d63 100644 --- a/shell/browser/ui/electron_menu_model.cc +++ b/shell/browser/ui/electron_menu_model.cc @@ -37,6 +37,18 @@ std::u16string ElectronMenuModel::GetToolTipAt(size_t index) { return iter == std::end(toolTips_) ? std::u16string() : iter->second; } +void ElectronMenuModel::SetCustomType(size_t index, + const std::u16string& customType) { + int command_id = GetCommandIdAt(index); + customTypes_[command_id] = customType; +} + +std::u16string ElectronMenuModel::GetCustomTypeAt(size_t index) { + const int command_id = GetCommandIdAt(index); + const auto iter = customTypes_.find(command_id); + return iter == std::end(customTypes_) ? std::u16string() : iter->second; +} + void ElectronMenuModel::SetRole(size_t index, const std::u16string& role) { int command_id = GetCommandIdAt(index); roles_[command_id] = role; diff --git a/shell/browser/ui/electron_menu_model.h b/shell/browser/ui/electron_menu_model.h index 943f335f41bd..574d76ac639c 100644 --- a/shell/browser/ui/electron_menu_model.h +++ b/shell/browser/ui/electron_menu_model.h @@ -84,6 +84,8 @@ class ElectronMenuModel : public ui::SimpleMenuModel { void SetToolTip(size_t index, const std::u16string& toolTip); std::u16string GetToolTipAt(size_t index); + void SetCustomType(size_t index, const std::u16string& customType); + std::u16string GetCustomTypeAt(size_t index); void SetRole(size_t index, const std::u16string& role); std::u16string GetRoleAt(size_t index); void SetSecondaryLabel(size_t index, const std::u16string& sublabel); @@ -125,6 +127,8 @@ class ElectronMenuModel : public ui::SimpleMenuModel { base::flat_map toolTips_; // command id -> tooltip base::flat_map roles_; // command id -> role base::flat_map sublabels_; // command id -> sublabel + base::flat_map + customTypes_; // command id -> custom type base::ObserverList observers_; base::WeakPtrFactory weak_factory_{this}; diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 2f0e6b87f0e4..df4f70a89e19 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -279,7 +279,7 @@ declare module NodeJS { interface ContextMenuItem { id: number; label: string; - type: 'normal' | 'separator' | 'subMenu' | 'checkbox'; + type: 'normal' | 'separator' | 'subMenu' | 'checkbox' | 'header' | 'palette'; checked: boolean; enabled: boolean; subItems: ContextMenuItem[]; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 5d281261bb10..09ae513f3507 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -172,6 +172,7 @@ declare namespace Electron { setToolTip(index: number, tooltip: string): void; setIcon(index: number, image: string | NativeImage): void; setRole(index: number, role: string): void; + setCustomType(index: number, customType: string): void; insertItem(index: number, commandId: number, label: string): void; insertCheckItem(index: number, commandId: number, label: string): void; insertRadioItem(index: number, commandId: number, label: string, groupId: number): void;