Skip to content

Commit

Permalink
feat: add support for share menu on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
zcbenz committed Sep 24, 2020
1 parent 6fb7066 commit 437afa3
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 7 deletions.
12 changes: 11 additions & 1 deletion docs/api/menu-item.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See [`Menu`](menu.md) for examples.
* `menuItem` MenuItem
* `browserWindow` [BrowserWindow](browser-window.md) | undefined - This will not be defined if no window is open.
* `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`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `mergeAllWindows`, `clearRecentDocuments`, `moveTabToNewWindow` or `windowMenu` - Define the action of the menu item, when specified the
* `role` String (optional) - Can be `undo`, `redo`, `cut`, `copy`, `paste`, `pasteAndMatchStyle`, `delete`, `selectAll`, `reload`, `forceReload`, `toggleDevTools`, `resetZoom`, `zoomIn`, `zoomOut`, `togglefullscreen`, `window`, `minimize`, `close`, `help`, `about`, `services`, `hide`, `hideOthers`, `unhide`, `quit`, `startSpeaking`, `stopSpeaking`, `zoom`, `front`, `appMenu`, `fileMenu`, `editMenu`, `viewMenu`, `shareMenu`, `recentDocuments`, `toggleTabBar`, `selectNextTab`, `selectPreviousTab`, `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`.
Expand All @@ -31,6 +31,7 @@ See [`Menu`](menu.md) for examples.
menu items.
* `registerAccelerator` Boolean (optional) _Linux_ _Windows_ - If false, the accelerator won't be registered
with the system, but it will still be displayed. Defaults to true.
* `sharingItem` SharingItem (optional) _macOS_ - The item to share when the `role` is `shareMenu`.
* `submenu` (MenuItemConstructorOptions[] | [Menu](menu.md)) (optional) - Should be specified
for `submenu` type menu items. If `submenu` is specified, the `type: 'submenu'` can be omitted.
If the value is not a [`Menu`](menu.md) then it will be automatically converted to one using
Expand Down Expand Up @@ -112,6 +113,7 @@ The following additional roles are available on _macOS_:
* `services` - The submenu is a ["Services"](https://developer.apple.com/documentation/appkit/nsapplication/1428608-servicesmenu?language=objc) menu. This is only intended for use in the Application Menu and is *not* the same as the "Services" submenu used in context menus in macOS apps, which is not implemented in Electron.
* `recentDocuments` - The submenu is an "Open Recent" menu.
* `clearRecentDocuments` - Map to the `clearRecentDocuments` action.
* `shareMenu` - The submenu is [share menu][ShareMenu]. The `sharingItem` property must also be set to indicate the item to share.

When specifying a `role` on macOS, `label` and `accelerator` are the only
options that will affect the menu item. All other options will be ignored.
Expand Down Expand Up @@ -200,10 +202,18 @@ system or just displayed.

This property can be dynamically changed.

#### `menuItem.sharingItem` _macOS_

A `SharingItem` indicating the item to share when the `role` is `shareMenu`.

This property can be dynamically changed.

#### `menuItem.commandId`

A `Number` indicating an item's sequential unique id.

#### `menuItem.menu`

A `Menu` that the item is a part of.

[ShareMenu]: https://developer.apple.com/design/human-interface-guidelines/macos/extensions/share-extensions/
6 changes: 5 additions & 1 deletion docs/api/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
Process: [Main](../glossary.md#main-process)

### `new Menu()`
### `new Menu([options])`

* `options` Object (optional)
* `sharingItem` SharingItem (optional) _macOS_ - Turn the menu into a [share menu][ShareMenu] and specify the item to share.

Creates a new menu.

Expand Down Expand Up @@ -401,4 +404,5 @@ Menu:
```

[AboutInformationPropertyListFiles]: https://developer.apple.com/library/ios/documentation/general/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html
[ShareMenu]: https://developer.apple.com/design/human-interface-guidelines/macos/extensions/share-extensions/
[setMenu]: https://github.com/electron/electron/blob/master/docs/api/browser-window.md#winsetmenumenu-linux-windows
5 changes: 5 additions & 0 deletions docs/api/structures/sharing-item.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SharingItem Object

* `texts` String[] (optional) - An array of text to share.
* `filePaths` String[] (optional) - An array of files to share.
* `urls` String[] (optional) - An array of URLs to share.
1 change: 1 addition & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ auto_filenames = {
"docs/api/structures/segmented-control-segment.md",
"docs/api/structures/service-worker-info.md",
"docs/api/structures/shared-worker-info.md",
"docs/api/structures/sharing-item.md",
"docs/api/structures/shortcut-details.md",
"docs/api/structures/size.md",
"docs/api/structures/task.md",
Expand Down
7 changes: 6 additions & 1 deletion lib/browser/api/menu-item-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const isLinux = process.platform === 'linux';

type RoleId = 'about' | 'close' | 'copy' | 'cut' | 'delete' | 'forcereload' | 'front' | 'help' | 'hide' | 'hideothers' | 'minimize' |
'paste' | 'pasteandmatchstyle' | 'quit' | 'redo' | 'reload' | 'resetzoom' | 'selectall' | 'services' | 'recentdocuments' | 'clearrecentdocuments' | 'startspeaking' | 'stopspeaking' |
'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu'
'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu' | 'sharemenu'
interface Role {
label: string;
accelerator?: string;
Expand Down Expand Up @@ -261,6 +261,11 @@ export const roleList: Record<RoleId, Role> = {
{ role: 'close' }
] as MenuItemConstructorOptions[])
]
},
// Share submenu
sharemenu: {
label: 'Share',
submenu: []
}
};

Expand Down
6 changes: 6 additions & 0 deletions lib/browser/api/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Menu.prototype._shouldRegisterAcceleratorForCommandId = function (id) {
return this.commandsMap[id] ? this.commandsMap[id].registerAccelerator : false;
};

if (process.platform === 'darwin') {
Menu.prototype._getSharingItemForCommandId = function (id) {
return this.commandsMap[id] ? this.commandsMap[id].sharingItem : null;
};
}

Menu.prototype._executeCommand = function (event, id) {
const command = this.commandsMap[id];
if (!command) return;
Expand Down
49 changes: 49 additions & 0 deletions shell/browser/api/electron_api_menu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,39 @@
#include "shell/browser/native_window.h"
#include "shell/common/gin_converters/accelerator_converter.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "ui/base/models/image_model.h"

#if defined(OS_MAC)

namespace gin {

using SharingItem = electron::ElectronMenuModel::SharingItem;

template <>
struct Converter<SharingItem> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
SharingItem* out) {
gin_helper::Dictionary dict;
if (!ConvertFromV8(isolate, val, &dict))
return false;
dict.GetOptional("texts", &(out->texts));
dict.GetOptional("filePaths", &(out->file_paths));
dict.GetOptional("urls", &(out->urls));
return true;
}
};

} // namespace gin

#endif

namespace electron {

namespace api {
Expand All @@ -26,6 +53,15 @@ gin::WrapperInfo Menu::kWrapperInfo = {gin::kEmbedderNativeGin};

Menu::Menu(gin::Arguments* args) : model_(new ElectronMenuModel(this)) {
model_->AddObserver(this);

#if defined(OS_MAC)
gin_helper::Dictionary options;
if (args->GetNext(&options)) {
ElectronMenuModel::SharingItem item;
if (options.Get("sharingItem", &item))
model_->SetSharingItem(std::move(item));
}
#endif
}

Menu::~Menu() {
Expand Down Expand Up @@ -81,6 +117,19 @@ bool Menu::ShouldRegisterAcceleratorForCommandId(int command_id) const {
command_id);
}

#if defined(OS_MAC)
bool Menu::GetSharingItemForCommandId(
int command_id,
ElectronMenuModel::SharingItem* item) const {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Value> val =
gin_helper::CallMethod(isolate, const_cast<Menu*>(this),
"_getSharingItemForCommandId", command_id);
return gin::ConvertFromV8(isolate, val, item);
}
#endif

void Menu::ExecuteCommand(int command_id, int flags) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
Expand Down
5 changes: 5 additions & 0 deletions shell/browser/api/electron_api_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class Menu : public gin::Wrappable<Menu>,
bool use_default_accelerator,
ui::Accelerator* accelerator) const override;
bool ShouldRegisterAcceleratorForCommandId(int command_id) const override;
#if defined(OS_MAC)
bool GetSharingItemForCommandId(
int command_id,
ElectronMenuModel::SharingItem* item) const override;
#endif
void ExecuteCommand(int command_id, int event_flags) override;
void OnMenuWillShow(ui::SimpleMenuModel* source) override;

Expand Down
3 changes: 2 additions & 1 deletion shell/browser/ui/cocoa/electron_menu_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class ElectronMenuModel;
// allow for hierarchical menus). The tag is the index into that model for
// that particular item. It is important that the model outlives this object
// as it only maintains weak references.
@interface ElectronMenuController : NSObject <NSMenuDelegate> {
@interface ElectronMenuController
: NSObject <NSMenuDelegate, NSSharingServiceDelegate> {
@protected
base::WeakPtr<electron::ElectronMenuModel> model_;
base::scoped_nsobject<NSMenu> menu_;
Expand Down
91 changes: 88 additions & 3 deletions shell/browser/ui/cocoa/electron_menu_controller.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
#include "base/task/post_task.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/mac/url_conversions.h"
#include "shell/browser/mac/electron_application.h"
#include "shell/browser/native_window.h"
#include "shell/browser/ui/electron_menu_model.h"
#include "shell/browser/window_list.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/platform_accelerator_cocoa.h"
#include "ui/base/l10n/l10n_util_mac.h"
Expand All @@ -24,6 +27,7 @@
#include "ui/strings/grit/ui_strings.h"

using content::BrowserThread;
using SharingItem = electron::ElectronMenuModel::SharingItem;

namespace {

Expand Down Expand Up @@ -86,6 +90,24 @@ bool MenuHasVisibleItems(const electron::ElectronMenuModel* model) {
return submenu.autorelease();
}

// Convert an SharingItem to an array of NSObjects.
NSArray* ConvertSharingItemToNS(const SharingItem& item) {
NSMutableArray* result = [NSMutableArray array];
if (item.texts) {
for (const std::string& str : *item.texts)
[result addObject:base::SysUTF8ToNSString(str)];
}
if (item.file_paths) {
for (const base::FilePath& path : *item.file_paths)
[result addObject:base::mac::FilePathToNSURL(path)];
}
if (item.urls) {
for (const GURL& url : *item.urls)
[result addObject:net::NSURLWithGURL(url)];
}
return result;
}

} // namespace

// This class stores a base::WeakPtr<electron::ElectronMenuModel> as an
Expand Down Expand Up @@ -267,6 +289,31 @@ - (void)replaceSubmenuShowingRecentDocuments:(NSMenuItem*)item {
recentDocumentsMenuItem_.reset([item retain]);
}

// Fill the menu with Share Menu items.
- (NSMenu*)createShareMenuForItem:(const SharingItem&)item {
NSArray* items = ConvertSharingItemToNS(item);
if ([items count] == 0)
return MakeEmptySubmenu();
base::scoped_nsobject<NSMenu> menu([[NSMenu alloc] init]);
NSArray* services = [NSSharingService sharingServicesForItems:items];
for (NSSharingService* service in services)
[menu addItem:[self menuItemForService:service withItems:items]];
return menu.autorelease();
}

// Creates a menu item that calls |service| when invoked.
- (NSMenuItem*)menuItemForService:(NSSharingService*)service
withItems:(NSArray*)items {
base::scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc]
initWithTitle:service.menuItemTitle
action:@selector(performShare:)
keyEquivalent:@""]);
[item setTarget:self];
[item setImage:service.image];
[item setRepresentedObject:@{@"service" : service, @"items" : items}];
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
Expand Down Expand Up @@ -300,6 +347,12 @@ - (void)addItemToMenu:(NSMenu*)menu
NSMenu* submenu = [[NSMenu alloc] initWithTitle:label];
[item setSubmenu:submenu];
[NSApp setServicesMenu:submenu];
} else if (role == base::ASCIIToUTF16("sharemenu")) {
SharingItem sharing_item;
model->GetSharingItemAt(index, &sharing_item);
[item setTarget:nil];
[item setAction:nil];
[item setSubmenu:[self createShareMenuForItem:sharing_item]];
} else if (type == electron::ElectronMenuModel::TYPE_SUBMENU &&
model->IsVisibleAt(index)) {
// We need to specifically check that the submenu top-level item has been
Expand Down Expand Up @@ -372,6 +425,8 @@ - (void)addItemToMenu:(NSMenu*)menu
// radio, etc) of each item in the menu.
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
SEL action = [item action];
if (action == @selector(performShare:))
return YES;
if (action != @selector(itemSelected:))
return NO;

Expand Down Expand Up @@ -405,14 +460,30 @@ - (void)itemSelected:(id)sender {
}
}

// Performs the share action using the sharing service represented by |sender|.
- (void)performShare:(NSMenuItem*)sender {
NSDictionary* object =
base::mac::ObjCCastStrict<NSDictionary>([sender representedObject]);
NSSharingService* service =
base::mac::ObjCCastStrict<NSSharingService>(object[@"service"]);
NSArray* items = base::mac::ObjCCastStrict<NSArray>(object[@"items"]);
[service setDelegate:self];
[service performWithItems:items];
}

- (NSMenu*)menu {
if (menu_)
return menu_.get();

menu_.reset([[NSMenu alloc] initWithTitle:@""]);
if (model_ && model_->GetSharingItem()) {
NSMenu* menu = [self createShareMenuForItem:*model_->GetSharingItem()];
menu_.reset([menu retain]);
} else {
menu_.reset([[NSMenu alloc] initWithTitle:@""]);
if (model_)
[self populateWithModel:model_.get()];
}
[menu_ setDelegate:self];
if (model_)
[self populateWithModel:model_.get()];
return menu_.get();
}

Expand All @@ -439,4 +510,18 @@ - (void)menuDidClose:(NSMenu*)menu {
}
}

// NSSharingServiceDelegate

- (NSWindow*)sharingService:(NSSharingService*)service
sourceWindowForShareItems:(NSArray*)items
sharingContentScope:(NSSharingContentScope*)scope {
// Return the current active window.
const auto& list = electron::WindowList::GetWindows();
for (electron::NativeWindow* window : list) {
if (window->IsFocused())
return window->GetNativeWindow().GetNativeNSWindow();
}
return nil;
}

@end
Loading

0 comments on commit 437afa3

Please sign in to comment.