feat: add support for share menu on macOS (#25629)
This commit is contained in:
parent
89c04b3c6c
commit
6b6ffbdd10
17 changed files with 316 additions and 6 deletions
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#import "shell/browser/ui/cocoa/electron_menu_controller.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "base/logging.h"
|
||||
|
@ -14,8 +15,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"
|
||||
|
@ -24,6 +28,7 @@
|
|||
#include "ui/strings/grit/ui_strings.h"
|
||||
|
||||
using content::BrowserThread;
|
||||
using SharingItem = electron::ElectronMenuModel::SharingItem;
|
||||
|
||||
namespace {
|
||||
|
||||
|
@ -86,6 +91,24 @@ NSMenu* MakeEmptySubmenu() {
|
|||
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
|
||||
|
@ -267,6 +290,31 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
|
|||
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
|
||||
|
@ -300,6 +348,12 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
|
|||
NSMenu* submenu = [[[NSMenu alloc] initWithTitle:label] autorelease];
|
||||
[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
|
||||
|
@ -372,6 +426,8 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
|
|||
// 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;
|
||||
|
||||
|
@ -405,14 +461,30 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
|
|||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
@ -439,4 +511,18 @@ static base::scoped_nsobject<NSMenu> recentDocumentsMenuSwap_;
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -4,10 +4,18 @@
|
|||
|
||||
#include "shell/browser/ui/electron_menu_model.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/stl_util.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
#if defined(OS_MAC)
|
||||
ElectronMenuModel::SharingItem::SharingItem() = default;
|
||||
ElectronMenuModel::SharingItem::SharingItem(SharingItem&&) = default;
|
||||
ElectronMenuModel::SharingItem::~SharingItem() = default;
|
||||
#endif
|
||||
|
||||
bool ElectronMenuModel::Delegate::GetAcceleratorForCommandId(
|
||||
int command_id,
|
||||
ui::Accelerator* accelerator) const {
|
||||
|
@ -79,6 +87,23 @@ bool ElectronMenuModel::WorksWhenHiddenAt(int index) const {
|
|||
return true;
|
||||
}
|
||||
|
||||
#if defined(OS_MAC)
|
||||
bool ElectronMenuModel::GetSharingItemAt(int index, SharingItem* item) const {
|
||||
if (delegate_)
|
||||
return delegate_->GetSharingItemForCommandId(GetCommandIdAt(index), item);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ElectronMenuModel::SetSharingItem(SharingItem item) {
|
||||
sharing_item_.emplace(std::move(item));
|
||||
}
|
||||
|
||||
const base::Optional<ElectronMenuModel::SharingItem>&
|
||||
ElectronMenuModel::GetSharingItem() const {
|
||||
return sharing_item_;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ElectronMenuModel::MenuWillClose() {
|
||||
ui::SimpleMenuModel::MenuWillClose();
|
||||
for (Observer& observer : observers_) {
|
||||
|
|
|
@ -6,16 +6,34 @@
|
|||
#define SHELL_BROWSER_UI_ELECTRON_MENU_MODEL_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/observer_list_types.h"
|
||||
#include "base/optional.h"
|
||||
#include "ui/base/models/simple_menu_model.h"
|
||||
#include "url/gurl.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronMenuModel : public ui::SimpleMenuModel {
|
||||
public:
|
||||
#if defined(OS_MAC)
|
||||
struct SharingItem {
|
||||
SharingItem();
|
||||
SharingItem(SharingItem&&);
|
||||
SharingItem(const SharingItem&) = delete;
|
||||
~SharingItem();
|
||||
|
||||
base::Optional<std::vector<std::string>> texts;
|
||||
base::Optional<std::vector<GURL>> urls;
|
||||
base::Optional<std::vector<base::FilePath>> file_paths;
|
||||
};
|
||||
#endif
|
||||
|
||||
class Delegate : public ui::SimpleMenuModel::Delegate {
|
||||
public:
|
||||
~Delegate() override {}
|
||||
|
@ -30,6 +48,11 @@ class ElectronMenuModel : public ui::SimpleMenuModel {
|
|||
|
||||
virtual bool ShouldCommandIdWorkWhenHidden(int command_id) const = 0;
|
||||
|
||||
#if defined(OS_MAC)
|
||||
virtual bool GetSharingItemForCommandId(int command_id,
|
||||
SharingItem* item) const = 0;
|
||||
#endif
|
||||
|
||||
private:
|
||||
// ui::SimpleMenuModel::Delegate:
|
||||
bool GetAcceleratorForCommandId(
|
||||
|
@ -65,6 +88,13 @@ class ElectronMenuModel : public ui::SimpleMenuModel {
|
|||
ui::Accelerator* accelerator) const;
|
||||
bool ShouldRegisterAcceleratorAt(int index) const;
|
||||
bool WorksWhenHiddenAt(int index) const;
|
||||
#if defined(OS_MAC)
|
||||
// Return the SharingItem of menu item.
|
||||
bool GetSharingItemAt(int index, SharingItem* item) const;
|
||||
// Set/Get the SharingItem of this menu.
|
||||
void SetSharingItem(SharingItem item);
|
||||
const base::Optional<SharingItem>& GetSharingItem() const;
|
||||
#endif
|
||||
|
||||
// ui::SimpleMenuModel:
|
||||
void MenuWillClose() override;
|
||||
|
@ -80,6 +110,10 @@ class ElectronMenuModel : public ui::SimpleMenuModel {
|
|||
private:
|
||||
Delegate* delegate_; // weak ref.
|
||||
|
||||
#if defined(OS_MAC)
|
||||
base::Optional<SharingItem> sharing_item_;
|
||||
#endif
|
||||
|
||||
std::map<int, base::string16> toolTips_; // command id -> tooltip
|
||||
std::map<int, base::string16> roles_; // command id -> role
|
||||
std::map<int, base::string16> sublabels_; // command id -> sublabel
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue