WK1: Support default actions for editable whitespace
[WebKit-https.git] / Source / WebKit / mac / WebView / WebActionMenuController.mm
1 /*
2  * Copyright (C) 2014 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 "WebActionMenuController.h"
27
28 #import "DOMElementInternal.h"
29 #import "DOMNodeInternal.h"
30 #import "WebDocumentInternal.h"
31 #import "WebElementDictionary.h"
32 #import "WebFrameInternal.h"
33 #import "WebHTMLView.h"
34 #import "WebHTMLViewInternal.h"
35 #import "WebSystemInterface.h"
36 #import "WebUIDelegatePrivate.h"
37 #import "WebViewInternal.h"
38 #import <WebCore/DictionaryLookup.h>
39 #import <WebCore/Element.h>
40 #import <WebCore/EventHandler.h>
41 #import <WebCore/Frame.h>
42 #import <WebCore/FrameView.h>
43 #import <WebCore/HTMLConverter.h>
44 #import <WebCore/NSViewSPI.h>
45 #import <WebCore/Page.h>
46 #import <WebCore/Range.h>
47 #import <WebCore/RenderElement.h>
48 #import <WebCore/RenderObject.h>
49 #import <WebCore/SoftLinking.h>
50 #import <WebKitSystemInterface.h>
51 #import <objc/objc-class.h>
52 #import <objc/objc.h>
53
54 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
55 SOFT_LINK_CLASS(QuickLookUI, QLPreviewBubble)
56
57 @class QLPreviewBubble;
58 @interface NSObject (WKQLPreviewBubbleDetails)
59 @property (copy) NSArray * controls;
60 @property NSSize maximumSize;
61 @property NSRectEdge preferredEdge;
62 @property (retain) IBOutlet NSWindow* parentWindow;
63 - (void)showPreviewItem:(id)previewItem itemFrame:(NSRect)frame;
64 - (void)setAutomaticallyCloseWithMask:(NSEventMask)autocloseMask filterMask:(NSEventMask)filterMask block:(void (^)(void))block;
65 @end
66
67 using namespace WebCore;
68
69 struct DictionaryPopupInfo {
70     NSPoint origin;
71     RetainPtr<NSDictionary> options;
72     RetainPtr<NSAttributedString> attributedString;
73 };
74
75 @implementation WebActionMenuController
76
77 - (id)initWithWebView:(WebView *)webView
78 {
79     if (!(self = [super init]))
80         return nil;
81
82     _webView = webView;
83     _type = WebActionMenuNone;
84
85     return self;
86 }
87
88 - (void)webViewClosed
89 {
90     _webView = nil;
91 }
92
93 - (WebElementDictionary *)performHitTestAtPoint:(NSPoint)windowPoint
94 {
95     WebHTMLView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
96     NSPoint point = [documentView convertPoint:windowPoint fromView:nil];
97
98     Frame* coreFrame = core([documentView _frame]);
99     if (!coreFrame)
100         return nil;
101     HitTestRequest::HitTestRequestType hitType = HitTestRequest::ReadOnly | HitTestRequest::Active;
102     _hitTestResult = coreFrame->eventHandler().hitTestResultAtPoint(IntPoint(point), hitType);
103
104     return [[[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult] autorelease];
105 }
106
107 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
108 {
109     if (!_webView)
110         return;
111
112     NSMenu *actionMenu = _webView.actionMenu;
113     if (menu != actionMenu)
114         return;
115
116     [actionMenu removeAllItems];
117
118     WebElementDictionary *hitTestResult = [self performHitTestAtPoint:[_webView convertPoint:event.locationInWindow fromView:nil]];
119     NSArray *menuItems = [self _defaultMenuItemsForHitTestResult:hitTestResult];
120
121     // Allow clients to customize the menu items.
122     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionMenuItemsForHitTestResult:withType:defaultActionMenuItems:)])
123         menuItems = [[_webView UIDelegate] _webView:_webView actionMenuItemsForHitTestResult:hitTestResult withType:_type defaultActionMenuItems:menuItems];
124
125     for (NSMenuItem *item in menuItems)
126         [actionMenu addItem:item];
127 }
128
129 - (BOOL)isMenuForTextContent
130 {
131     return _type == WebActionMenuReadOnlyText || _type == WebActionMenuEditableText || _type == WebActionMenuWhitespaceInEditableArea;
132 }
133
134 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
135 {
136     if (menu != _webView.actionMenu)
137         return;
138
139     if (![self isMenuForTextContent]) {
140         [[_webView _selectedOrMainFrame] _clearSelection];
141         return;
142     }
143
144     // Action menus for text should highlight the text so that it is clear what the action menu actions
145     // will apply to. If the text is already selected, the menu will use the existing selection.
146     if (!_hitTestResult.isSelected())
147         [self _selectLookupText];
148 }
149
150 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
151 {
152     if (menu != _webView.actionMenu)
153         return;
154
155     _type = WebActionMenuNone;
156 }
157
158 #pragma mark Link actions
159
160 - (void)_openURLFromActionMenu:(id)sender
161 {
162     if (!_webView)
163         return;
164
165     NSURL *url = [sender representedObject];
166     if (!url)
167         return;
168
169     ASSERT([url isKindOfClass:[NSURL class]]);
170
171     [[NSWorkspace sharedWorkspace] openURL:url];
172 }
173
174 - (void)_addToReadingListFromActionMenu:(id)sender
175 {
176     if (!_webView)
177         return;
178
179     NSURL *url = [sender representedObject];
180     if (!url)
181         return;
182
183     ASSERT([url isKindOfClass:[NSURL class]]);
184
185     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
186     [service performWithItems:@[ url ]];
187 }
188
189 - (NSRect)_elementBoundingBoxFromDOMElement:(DOMElement *)domElement
190 {
191     if (!domElement)
192         return NSZeroRect;
193
194     Node* node = core(domElement);
195     Frame* frame = node->document().frame();
196     if (!frame)
197         return NSZeroRect;
198
199     FrameView* view = frame->view();
200     if (!view)
201         return NSZeroRect;
202
203     return view->contentsToWindow(node->pixelSnappedBoundingBox());
204 }
205
206 - (void)_quickLookURLFromActionMenu:(id)sender
207 {
208     if (!_webView)
209         return;
210
211     NSDictionary *hitTestResult = [sender representedObject];
212     if (!hitTestResult)
213         return;
214
215     ASSERT([hitTestResult isKindOfClass:[NSDictionary class]]);
216
217     NSURL *url = [hitTestResult objectForKey:WebElementLinkURLKey];
218     if (!url)
219         return;
220
221     DOMElement *domElement = [hitTestResult objectForKey:WebElementDOMNodeKey];
222     if (!domElement)
223         return;
224
225     NSRect itemFrame = [_webView convertRect:[self _elementBoundingBoxFromDOMElement:domElement] toView:nil];
226     NSSize maximumPreviewSize = NSMakeSize(_webView.bounds.size.width * 0.75, _webView.bounds.size.height * 0.75);
227
228     RetainPtr<QLPreviewBubble> bubble = adoptNS([[getQLPreviewBubbleClass() alloc] init]);
229     [bubble setParentWindow:_webView.window];
230     [bubble setMaximumSize:maximumPreviewSize];
231     [bubble setPreferredEdge:NSMaxYEdge];
232     [bubble setControls:@[ ]];
233     NSEventMask filterMask = NSAnyEventMask & ~(NSAppKitDefinedMask | NSSystemDefinedMask | NSApplicationDefinedMask | NSMouseEnteredMask | NSMouseExitedMask);
234     NSEventMask autocloseMask = NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyDownMask;
235     [bubble setAutomaticallyCloseWithMask:autocloseMask filterMask:filterMask block:[bubble] {
236         [bubble close];
237     }];
238     [bubble showPreviewItem:url itemFrame:itemFrame];
239 }
240
241 - (NSArray *)_defaultMenuItemsForLink:(WebElementDictionary *)hitTestResult
242 {
243     NSURL *url = [hitTestResult objectForKey:WebElementLinkURLKey];
244     if (!url)
245         return @[ ];
246
247     if (!WebCore::protocolIsInHTTPFamily([url absoluteString]))
248         return @[ ];
249
250     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:WebActionMenuItemTagOpenLinkInDefaultBrowser withHitTestResult:hitTestResult];
251     RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPreviewLink withHitTestResult:hitTestResult];
252     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:WebActionMenuItemTagAddLinkToSafariReadingList withHitTestResult:hitTestResult];
253
254     return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
255 }
256
257 #pragma mark Text actions
258
259 - (NSArray *)_defaultMenuItemsForText:(WebElementDictionary *)hitTestResult
260 {
261     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText withHitTestResult:hitTestResult];
262     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText withHitTestResult:hitTestResult];
263
264     return @[ copyTextItem.get(), lookupTextItem.get() ];
265 }
266
267 - (NSArray *)_defaultMenuItemsForEditableText:(WebElementDictionary *)hitTestResult
268 {
269     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText withHitTestResult:hitTestResult];
270     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText withHitTestResult:hitTestResult];
271     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste withHitTestResult:hitTestResult];
272
273     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
274 }
275
276 - (void)_copySelection:(id)sender
277 {
278     [_webView _executeCoreCommandByName:@"copy" value:nil];
279 }
280
281 - (void)_lookupText:(id)sender
282 {
283     Frame* frame = core([_webView _selectedOrMainFrame]);
284     if (!frame)
285         return;
286
287     DictionaryPopupInfo popupInfo = performDictionaryLookupForSelection(frame, frame->selection().selection());
288     if (!popupInfo.attributedString)
289         return;
290
291     NSPoint textBaselineOrigin = popupInfo.origin;
292
293     // Convert to screen coordinates.
294     textBaselineOrigin = [_webView convertPoint:textBaselineOrigin toView:nil];
295     textBaselineOrigin = [_webView.window convertRectToScreen:NSMakeRect(textBaselineOrigin.x, textBaselineOrigin.y, 0, 0)].origin;
296
297     WKShowWordDefinitionWindow(popupInfo.attributedString.get(), textBaselineOrigin, popupInfo.options.get());
298 }
299
300 - (void)_paste:(id)sender
301 {
302     [_webView _executeCoreCommandByName:@"paste" value:nil];
303 }
304
305 - (void)_selectLookupText
306 {
307     NSDictionary *options = nil;
308     RefPtr<Range> lookupRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
309     if (!lookupRange)
310         return;
311
312     Frame* frame = _hitTestResult.innerNode()->document().frame();
313     if (!frame)
314         return;
315
316     frame->selection().setSelectedRange(lookupRange.get(), DOWNSTREAM, true);
317     return;
318 }
319
320 static DictionaryPopupInfo performDictionaryLookupForSelection(Frame* frame, const VisibleSelection& selection)
321 {
322     NSDictionary *options = nil;
323     DictionaryPopupInfo popupInfo;
324     RefPtr<Range> selectedRange = rangeForDictionaryLookupForSelection(selection, &options);
325     if (selectedRange)
326         popupInfo = performDictionaryLookupForRange(frame, *selectedRange, options);
327     return popupInfo;
328 }
329
330 static DictionaryPopupInfo performDictionaryLookupForRange(Frame* frame, Range& range, NSDictionary *options)
331 {
332     DictionaryPopupInfo popupInfo;
333     if (range.text().stripWhiteSpace().isEmpty())
334         return popupInfo;
335     
336     RenderObject* renderer = range.startContainer()->renderer();
337     const RenderStyle& style = renderer->style();
338
339     Vector<FloatQuad> quads;
340     range.textQuads(quads);
341     if (quads.isEmpty())
342         return popupInfo;
343
344     IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
345
346     popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
347     popupInfo.options = options;
348
349     NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range);
350     RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
351     NSFontManager *fontManager = [NSFontManager sharedFontManager];
352
353     [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
354         RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
355
356         NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
357         if (font) {
358             font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
359             [scaledAttributes setObject:font forKey:NSFontAttributeName];
360         }
361
362         [scaledNSAttributedString addAttributes:scaledAttributes.get() range:range];
363     }];
364
365     popupInfo.attributedString = scaledNSAttributedString.get();
366     return popupInfo;
367 }
368
369 #pragma mark Whitespace actions
370
371 - (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea:(WebElementDictionary *)hitTestResult
372 {
373     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste withHitTestResult:hitTestResult];
374
375     return @[ [NSMenuItem separatorItem], [NSMenuItem separatorItem], pasteItem.get() ];
376 }
377
378 #pragma mark Menu Items
379
380 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag withHitTestResult:(WebElementDictionary *)hitTestResult
381 {
382     SEL selector = nullptr;
383     NSString *title = nil;
384     NSImage *image = nil;
385     id representedObject = nil;
386
387     // FIXME: We should localize these strings.
388     switch (tag) {
389     case WebActionMenuItemTagOpenLinkInDefaultBrowser:
390         selector = @selector(_openURLFromActionMenu:);
391         title = @"Open";
392         image = webKitBundleImageNamed(@"OpenInNewWindowTemplate");
393         representedObject = [hitTestResult objectForKey:WebElementLinkURLKey];
394         break;
395
396     case WebActionMenuItemTagPreviewLink:
397         selector = @selector(_quickLookURLFromActionMenu:);
398         title = @"Preview";
399         image = [NSImage imageNamed:@"NSActionMenuQuickLook"];
400         representedObject = hitTestResult;
401         break;
402
403     case WebActionMenuItemTagAddLinkToSafariReadingList:
404         selector = @selector(_addToReadingListFromActionMenu:);
405         title = @"Add to Safari Reading List";
406         image = [NSImage imageNamed:NSImageNameBookmarksTemplate];
407         representedObject = [hitTestResult objectForKey:WebElementLinkURLKey];
408         break;
409
410     case WebActionMenuItemTagCopyText:
411         selector = @selector(_copySelection:);
412         title = @"Copy";
413         image = [NSImage imageNamed:@"NSActionMenuCopy"];
414         break;
415
416     case WebActionMenuItemTagLookupText:
417         selector = @selector(_lookupText:);
418         title = @"Look Up";
419         image = [NSImage imageNamed:@"NSActionMenuLookup"];
420         break;
421
422     case WebActionMenuItemTagPaste:
423         selector = @selector(_paste:);
424         title = @"Paste";
425         image = [NSImage imageNamed:@"NSActionMenuPaste"];
426         break;
427
428     default:
429         ASSERT_NOT_REACHED();
430         return nil;
431     }
432
433     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
434     [item setImage:image];
435     [item setTarget:self];
436     [item setTag:tag];
437     [item setRepresentedObject:representedObject];
438     return item;
439 }
440
441 static NSImage *webKitBundleImageNamed(NSString *name)
442 {
443     return [[NSBundle bundleForClass:NSClassFromString(@"WKView")] imageForResource:name];
444 }
445
446 - (NSArray *)_defaultMenuItemsForHitTestResult:(WebElementDictionary *)hitTestResult
447 {
448     NSURL *url = [hitTestResult objectForKey:WebElementLinkURLKey];
449     if (url) {
450         _type = WebActionMenuLink;
451         return [self _defaultMenuItemsForLink:hitTestResult];
452     }
453
454     Node* node = _hitTestResult.innerNode();
455     if (node && node->isTextNode()) {
456         if (_hitTestResult.isContentEditable()) {
457             _type = WebActionMenuEditableText;
458             return [self _defaultMenuItemsForEditableText:hitTestResult];
459         }
460
461         _type = WebActionMenuReadOnlyText;
462         return [self _defaultMenuItemsForText:hitTestResult];
463     }
464
465     if (_hitTestResult.isContentEditable()) {
466         _type = WebActionMenuWhitespaceInEditableArea;
467         return [self _defaultMenuItemsForWhitespaceInEditableArea:hitTestResult];
468     }
469
470     _type = WebActionMenuNone;
471     return @[ ];
472 }
473
474 @end