1/* 2 * Copyright (C) 2010 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26#import "config.h" 27#import "WebContextMenuProxyMac.h" 28 29#if PLATFORM(MAC) 30 31#import "DataReference.h" 32#import "MenuUtilities.h" 33#import "PageClientImpl.h" 34#import "ServicesController.h" 35#import "ShareableBitmap.h" 36#import "StringUtilities.h" 37#import "WebContext.h" 38#import "WebContextMenuItemData.h" 39#import "WebProcessProxy.h" 40#import "WKView.h" 41#import <WebCore/GraphicsContext.h> 42#import <WebCore/IntRect.h> 43#import <WebKitSystemInterface.h> 44#import <wtf/RetainPtr.h> 45 46#if ENABLE(SERVICE_CONTROLS) 47#import <AppKit/NSSharingService.h> 48 49#if __has_include(<AppKit/NSSharingService_Private.h>) 50#import <AppKit/NSSharingService_Private.h> 51#else 52typedef enum { 53 NSSharingServicePickerStyleMenu = 0, 54 NSSharingServicePickerStyleRollover = 1, 55 NSSharingServicePickerStyleTextSelection = 2 56} NSSharingServicePickerStyle; 57#endif 58 59@interface NSSharingServicePicker (Details) 60@property NSSharingServicePickerStyle style; 61- (NSMenu *)menu; 62@end 63 64#endif // ENABLE(SERVICE_CONTROLS) 65 66using namespace WebCore; 67 68@interface WKUserDataWrapper : NSObject { 69 RefPtr<API::Object> _webUserData; 70} 71- (id)initWithUserData:(API::Object*)userData; 72- (API::Object*)userData; 73@end 74 75@implementation WKUserDataWrapper 76 77- (id)initWithUserData:(API::Object*)userData 78{ 79 self = [super init]; 80 if (!self) 81 return nil; 82 83 _webUserData = userData; 84 return self; 85} 86 87- (API::Object*)userData 88{ 89 return _webUserData.get(); 90} 91 92@end 93 94@interface WKSelectionHandlerWrapper : NSObject { 95 std::function<void()> _selectionHandler; 96} 97- (id)initWithSelectionHandler:(std::function<void()>)selectionHandler; 98- (void)executeSelectionHandler; 99@end 100 101@implementation WKSelectionHandlerWrapper 102- (id)initWithSelectionHandler:(std::function<void()>)selectionHandler 103{ 104 self = [super init]; 105 if (!self) 106 return nil; 107 108 _selectionHandler = selectionHandler; 109 return self; 110} 111 112- (void)executeSelectionHandler 113{ 114 if (_selectionHandler) 115 _selectionHandler(); 116} 117@end 118 119@interface WKMenuTarget : NSObject { 120 WebKit::WebContextMenuProxyMac* _menuProxy; 121} 122+ (WKMenuTarget *)sharedMenuTarget; 123- (WebKit::WebContextMenuProxyMac*)menuProxy; 124- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy; 125- (void)forwardContextMenuAction:(id)sender; 126@end 127 128@implementation WKMenuTarget 129 130+ (WKMenuTarget*)sharedMenuTarget 131{ 132 static WKMenuTarget* target = [[WKMenuTarget alloc] init]; 133 return target; 134} 135 136- (WebKit::WebContextMenuProxyMac*)menuProxy 137{ 138 return _menuProxy; 139} 140 141- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy 142{ 143 _menuProxy = menuProxy; 144} 145 146- (void)forwardContextMenuAction:(id)sender 147{ 148 id representedObject = [sender representedObject]; 149 150 // NSMenuItems with a represented selection handler belong solely to the UI process 151 // and don't need any further processing after the selection handler is called. 152 if ([representedObject isKindOfClass:[WKSelectionHandlerWrapper class]]) { 153 [representedObject executeSelectionHandler]; 154 return; 155 } 156 157 WebKit::WebContextMenuItemData item(ActionType, static_cast<ContextMenuAction>([sender tag]), [sender title], [sender isEnabled], [sender state] == NSOnState); 158 if (representedObject) { 159 ASSERT([representedObject isKindOfClass:[WKUserDataWrapper class]]); 160 item.setUserData([static_cast<WKUserDataWrapper *>(representedObject) userData]); 161 } 162 163 _menuProxy->contextMenuItemSelected(item); 164} 165 166@end 167 168#if ENABLE(SERVICE_CONTROLS) 169@interface WKSharingServicePickerDelegate : NSObject <NSSharingServiceDelegate, NSSharingServicePickerDelegate> { 170 WebKit::WebContextMenuProxyMac* _menuProxy; 171 RetainPtr<NSSharingServicePicker> _picker; 172 BOOL _includeEditorServices; 173} 174 175+ (WKSharingServicePickerDelegate *)sharedSharingServicePickerDelegate; 176- (WebKit::WebContextMenuProxyMac*)menuProxy; 177- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy; 178- (void)setPicker:(NSSharingServicePicker *)picker; 179- (void)setIncludeEditorServices:(BOOL)includeEditorServices; 180@end 181 182// FIXME: We probably need to hang on the picker itself until the context menu operation is done, and this object will probably do that. 183@implementation WKSharingServicePickerDelegate 184+ (WKSharingServicePickerDelegate*)sharedSharingServicePickerDelegate 185{ 186 static WKSharingServicePickerDelegate* delegate = [[WKSharingServicePickerDelegate alloc] init]; 187 return delegate; 188} 189 190- (WebKit::WebContextMenuProxyMac*)menuProxy 191{ 192 return _menuProxy; 193} 194 195- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy 196{ 197 _menuProxy = menuProxy; 198} 199 200- (void)setPicker:(NSSharingServicePicker *)picker 201{ 202 _picker = picker; 203} 204 205- (void)setIncludeEditorServices:(BOOL)includeEditorServices 206{ 207 _includeEditorServices = includeEditorServices; 208} 209 210- (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices 211{ 212 if (_includeEditorServices) 213 return proposedServices; 214 215 NSMutableArray *services = [[NSMutableArray alloc] initWithCapacity:[proposedServices count]]; 216 217 for (NSSharingService *service in proposedServices) { 218 if (service.type != NSSharingServiceTypeEditor) 219 [services addObject:service]; 220 } 221 222 return services; 223} 224 225- (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService 226{ 227 return self; 228} 229 230- (void)sharingService:(NSSharingService *)sharingService willShareItems:(NSArray *)items 231{ 232 _menuProxy->clearServicesMenu(); 233} 234 235- (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items 236{ 237 // We only care about what item was shared if we were interested in editor services 238 // (i.e., if we plan on replacing the selection with the returned item) 239 if (!_includeEditorServices) 240 return; 241 242 Vector<String> types; 243 IPC::DataReference dataReference; 244 245 id item = [items objectAtIndex:0]; 246 247 if ([item isKindOfClass:[NSAttributedString class]]) { 248 NSData *data = [item RTFDFromRange:NSMakeRange(0, [item length]) documentAttributes:nil]; 249 dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]); 250 251 types.append(NSPasteboardTypeRTFD); 252 types.append(NSRTFDPboardType); 253 } else if ([item isKindOfClass:[NSData class]]) { 254 NSData *data = (NSData *)item; 255 RetainPtr<CGImageSourceRef> source = adoptCF(CGImageSourceCreateWithData((CFDataRef)data, NULL)); 256 RetainPtr<CGImageRef> image = adoptCF(CGImageSourceCreateImageAtIndex(source.get(), 0, NULL)); 257 258 if (!image) 259 return; 260 261 dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]); 262 types.append(NSPasteboardTypeTIFF); 263 } else { 264 LOG_ERROR("sharingService:didShareItems: - Unknown item type returned\n"); 265 return; 266 } 267 268 _menuProxy->page().replaceSelectionWithPasteboardData(types, dataReference); 269} 270 271- (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope 272{ 273 return _menuProxy->window(); 274} 275 276@end 277 278#endif 279 280namespace WebKit { 281 282WebContextMenuProxyMac::WebContextMenuProxyMac(WKView* webView, WebPageProxy* page) 283 : m_webView(webView) 284 , m_page(page) 285{ 286 ASSERT(m_page); 287} 288 289WebContextMenuProxyMac::~WebContextMenuProxyMac() 290{ 291 if (m_popup) 292 [m_popup setControlView:nil]; 293} 294 295void WebContextMenuProxyMac::contextMenuItemSelected(const WebContextMenuItemData& item) 296{ 297#if ENABLE(SERVICE_CONTROLS) 298 clearServicesMenu(); 299#endif 300 301 m_page->contextMenuItemSelected(item); 302} 303 304static void populateNSMenu(NSMenu* menu, const Vector<RetainPtr<NSMenuItem>>& menuItemVector) 305{ 306 for (unsigned i = 0; i < menuItemVector.size(); ++i) { 307 NSInteger oldState = [menuItemVector[i].get() state]; 308 [menu addItem:menuItemVector[i].get()]; 309 [menuItemVector[i].get() setState:oldState]; 310 } 311} 312 313static Vector<RetainPtr<NSMenuItem>> nsMenuItemVector(const Vector<WebContextMenuItemData>& items) 314{ 315 Vector<RetainPtr<NSMenuItem>> result; 316 317 unsigned size = items.size(); 318 result.reserveCapacity(size); 319 for (unsigned i = 0; i < size; i++) { 320 switch (items[i].type()) { 321 case ActionType: 322 case CheckableActionType: { 323 NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""]; 324 [menuItem setTag:items[i].action()]; 325 [menuItem setEnabled:items[i].enabled()]; 326 [menuItem setState:items[i].checked() ? NSOnState : NSOffState]; 327 328 if (std::function<void()> selectionHandler = items[i].selectionHandler()) { 329 WKSelectionHandlerWrapper *wrapper = [[WKSelectionHandlerWrapper alloc] initWithSelectionHandler:selectionHandler]; 330 [menuItem setRepresentedObject:wrapper]; 331 [wrapper release]; 332 } else if (items[i].userData()) { 333 WKUserDataWrapper *wrapper = [[WKUserDataWrapper alloc] initWithUserData:items[i].userData()]; 334 [menuItem setRepresentedObject:wrapper]; 335 [wrapper release]; 336 } 337 338 result.append(adoptNS(menuItem)); 339 break; 340 } 341 case SeparatorType: 342 result.append([NSMenuItem separatorItem]); 343 break; 344 case SubmenuType: { 345 NSMenu* menu = [[NSMenu alloc] initWithTitle:nsStringFromWebCoreString(items[i].title())]; 346 [menu setAutoenablesItems:NO]; 347 populateNSMenu(menu, nsMenuItemVector(items[i].submenu())); 348 349 NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""]; 350 [menuItem setEnabled:items[i].enabled()]; 351 [menuItem setSubmenu:menu]; 352 [menu release]; 353 354 result.append(adoptNS(menuItem)); 355 356 break; 357 } 358 default: 359 ASSERT_NOT_REACHED(); 360 } 361 } 362 363 WKMenuTarget* target = [WKMenuTarget sharedMenuTarget]; 364 for (unsigned i = 0; i < size; ++i) 365 [result[i].get() setTarget:target]; 366 367 return result; 368} 369 370#if ENABLE(SERVICE_CONTROLS) 371 372void WebContextMenuProxyMac::setupServicesMenu(const ContextMenuContextData& context) 373{ 374 bool includeEditorServices = context.controlledDataIsEditable(); 375 bool hasControlledImage = !context.controlledImageHandle().isNull(); 376 NSArray *items = nil; 377 if (hasControlledImage) { 378 RefPtr<ShareableBitmap> image = ShareableBitmap::create(context.controlledImageHandle()); 379 if (!image) 380 return; 381 382 RetainPtr<CGImageRef> cgImage = image->makeCGImage(); 383 RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]); 384 items = @[ nsImage.get() ]; 385 } else if (!context.controlledSelectionData().isEmpty()) { 386 RetainPtr<NSData> selectionData = adoptNS([[NSData alloc] initWithBytes:(void*)context.controlledSelectionData().data() length:context.controlledSelectionData().size()]); 387 RetainPtr<NSAttributedString> selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]); 388 389 items = @[ selection.get() ]; 390 } else { 391 LOG_ERROR("No service controlled item represented in the context"); 392 return; 393 } 394 395 RetainPtr<NSSharingServicePicker> picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]); 396 [picker setStyle:hasControlledImage ? NSSharingServicePickerStyleRollover : NSSharingServicePickerStyleTextSelection]; 397 [picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]]; 398 [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()]; 399 [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setIncludeEditorServices:includeEditorServices]; 400 401 m_servicesMenu = adoptNS([[picker menu] copy]); 402 403 if (!hasControlledImage) 404 [m_servicesMenu setShowsStateColumn:YES]; 405 406 // Explicitly add a menu item for each telephone number that is in the selection. 407 const Vector<String>& selectedTelephoneNumbers = context.selectedTelephoneNumbers(); 408 Vector<RetainPtr<NSMenuItem>> telephoneNumberMenuItems; 409 for (auto& telephoneNumber : selectedTelephoneNumbers) { 410 if (NSMenuItem *item = menuItemForTelephoneNumber(telephoneNumber)) { 411 [item setIndentationLevel:1]; 412 telephoneNumberMenuItems.append(item); 413 } 414 } 415 416 if (!telephoneNumberMenuItems.isEmpty()) { 417 if (m_servicesMenu) 418 [m_servicesMenu insertItem:[NSMenuItem separatorItem] atIndex:0]; 419 else 420 m_servicesMenu = adoptNS([[NSMenu alloc] init]); 421 int itemPosition = 0; 422 NSMenuItem *groupEntry = [[NSMenuItem alloc] initWithTitle:menuItemTitleForTelephoneNumberGroup() action:nil keyEquivalent:@""]; 423 [groupEntry setEnabled:NO]; 424 [m_servicesMenu insertItem:groupEntry atIndex:itemPosition++]; 425 for (auto& menuItem : telephoneNumberMenuItems) 426 [m_servicesMenu insertItem:menuItem.get() atIndex:itemPosition++]; 427 } 428 429 // If there is no services menu, then the existing services on the system have changed, so refresh that list of services. 430 // If <rdar://problem/17954709> is resolved then we can more accurately keep the list up to date without this call. 431 if (!m_servicesMenu) 432 ServicesController::shared().refreshExistingServices(); 433} 434 435void WebContextMenuProxyMac::clearServicesMenu() 436{ 437 [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr]; 438 m_servicesMenu = nullptr; 439} 440#endif 441 442void WebContextMenuProxyMac::populate(const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context) 443{ 444#if ENABLE(SERVICE_CONTROLS) 445 if (context.needsServicesMenu()) { 446 setupServicesMenu(context); 447 return; 448 } 449#endif 450 451 if (m_popup) 452 [m_popup removeAllItems]; 453 else { 454 m_popup = adoptNS([[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]); 455 [m_popup setUsesItemFromMenu:NO]; 456 [m_popup setAutoenablesItems:NO]; 457 } 458 459 NSMenu* menu = [m_popup menu]; 460 populateNSMenu(menu, nsMenuItemVector(items)); 461} 462 463void WebContextMenuProxyMac::showContextMenu(const IntPoint& menuLocation, const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context) 464{ 465#if ENABLE(SERVICE_CONTROLS) 466 if (items.isEmpty() && !context.needsServicesMenu()) 467 return; 468#else 469 if (items.isEmpty()) 470 return; 471#endif 472 473 populate(items, context); 474 475 [[WKMenuTarget sharedMenuTarget] setMenuProxy:this]; 476 477 NSRect menuRect = NSMakeRect(menuLocation.x(), menuLocation.y(), 0, 0); 478 479#if ENABLE(SERVICE_CONTROLS) 480 if (context.needsServicesMenu()) 481 [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this]; 482 483 if (!m_servicesMenu) 484 [m_popup attachPopUpWithFrame:menuRect inView:m_webView]; 485 486 NSMenu *menu = m_servicesMenu ? m_servicesMenu.get() : [m_popup menu]; 487 488 // Telephone number and service menus must use the [NSMenu popUpMenuPositioningItem:atLocation:inView:] API. 489 // FIXME: That API is better than WKPopupContextMenu. In the future all menus should use either it 490 // or the [NSMenu popUpContextMenu:withEvent:forView:] API, depending on the menu type. 491 // Then we could get rid of NSPopUpButtonCell, custom metrics, and WKPopupContextMenu. 492 if (context.isTelephoneNumberContext() || context.needsServicesMenu()) { 493 [menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:m_webView]; 494 hideContextMenu(); 495 return; 496 } 497 498#else 499 [m_popup attachPopUpWithFrame:menuRect inView:m_webView]; 500 501 NSMenu *menu = [m_popup menu]; 502#endif 503 504 // These values were borrowed from AppKit to match their placement of the menu. 505 NSRect titleFrame = [m_popup titleRectForBounds:menuRect]; 506 if (titleFrame.size.width <= 0 || titleFrame.size.height <= 0) 507 titleFrame = menuRect; 508 float vertOffset = roundf((NSMaxY(menuRect) - NSMaxY(titleFrame)) + NSHeight(titleFrame)); 509 NSPoint location = NSMakePoint(NSMinX(menuRect), NSMaxY(menuRect) - vertOffset); 510 511 location = [m_webView convertPoint:location toView:nil]; 512#pragma clang diagnostic push 513#pragma clang diagnostic ignored "-Wdeprecated-declarations" 514 location = [m_webView.window convertBaseToScreen:location]; 515#pragma clang diagnostic pop 516 517 WKPopupContextMenu(menu, location); 518 519 hideContextMenu(); 520} 521 522void WebContextMenuProxyMac::hideContextMenu() 523{ 524 [m_popup dismissPopUp]; 525} 526 527NSWindow *WebContextMenuProxyMac::window() const 528{ 529 return [m_webView window]; 530} 531 532} // namespace WebKit 533 534#endif // PLATFORM(MAC) 535