Invoke the context menu delegate callback right before we return the menu
[WebKit-https.git] / Source / WebKit / mac / WebCoreSupport / WebContextMenuClient.mm
1 /*
2  * Copyright (C) 2006, 2007, 2008, 2015 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #if !PLATFORM(IOS)
30
31 #import "WebContextMenuClient.h"
32
33 #import "WebDelegateImplementationCaching.h"
34 #import "WebElementDictionary.h"
35 #import "WebFrame.h"
36 #import "WebFrameInternal.h"
37 #import "WebHTMLView.h"
38 #import "WebHTMLViewInternal.h"
39 #import "WebKitVersionChecks.h"
40 #import "WebNSPasteboardExtras.h"
41 #import "WebSharingServicePickerController.h"
42 #import "WebUIDelegate.h"
43 #import "WebUIDelegatePrivate.h"
44 #import "WebView.h"
45 #import "WebViewInternal.h"
46 #import <WebCore/BitmapImage.h>
47 #import <WebCore/ContextMenu.h>
48 #import <WebCore/ContextMenuController.h>
49 #import <WebCore/Document.h>
50 #import <WebCore/Frame.h>
51 #import <WebCore/FrameView.h>
52 #import <WebCore/GraphicsContext.h>
53 #import <WebCore/ImageBuffer.h>
54 #import <WebCore/LocalizedStrings.h>
55 #import <WebCore/NSSharingServicePickerSPI.h>
56 #import <WebCore/Page.h>
57 #import <WebCore/RenderBox.h>
58 #import <WebCore/RenderObject.h>
59 #import <WebCore/SharedBuffer.h>
60 #import <WebCore/RuntimeApplicationChecks.h>
61 #import <WebCore/URL.h>
62 #import <WebKitLegacy/DOMPrivate.h>
63
64 using namespace WebCore;
65
66 @interface NSApplication (AppKitSecretsIKnowAbout)
67 - (void)speakString:(NSString *)string;
68 @end
69
70 WebContextMenuClient::WebContextMenuClient(WebView *webView)
71 #if ENABLE(SERVICE_CONTROLS)
72     : WebSharingServicePickerClient(webView)
73 #else
74     : m_webView(webView)
75 #endif
76 {
77 }
78
79 WebContextMenuClient::~WebContextMenuClient()
80 {
81 #if ENABLE(SERVICE_CONTROLS)
82     if (m_sharingServicePickerController)
83         [m_sharingServicePickerController clear];
84 #endif
85 }
86
87 void WebContextMenuClient::contextMenuDestroyed()
88 {
89     delete this;
90 }
91
92 NSMutableArray* WebContextMenuClient::getCustomMenuFromDefaultItems(ContextMenu* defaultMenu)
93 {
94     return defaultMenu->platformDescription();
95 }
96
97 void WebContextMenuClient::contextMenuItemSelected(ContextMenuItem* item, const ContextMenu* parentMenu)
98 {
99     id delegate = [m_webView UIDelegate];
100     SEL selector = @selector(webView:contextMenuItemSelected:forElement:);
101     if ([delegate respondsToSelector:selector]) {
102         NSDictionary *element = [[WebElementDictionary alloc] initWithHitTestResult:[m_webView page]->contextMenuController().hitTestResult()];
103
104         CallUIDelegate(m_webView, selector, item->platformDescription(), element);
105
106         [element release];
107     }
108 }
109
110 void WebContextMenuClient::downloadURL(const URL& url)
111 {
112     [m_webView _downloadURL:url];
113 }
114
115 void WebContextMenuClient::searchWithSpotlight()
116 {
117     [m_webView _searchWithSpotlightFromMenu:nil];
118 }
119
120 void WebContextMenuClient::searchWithGoogle(const Frame*)
121 {
122     [m_webView _searchWithGoogleFromMenu:nil];
123 }
124
125 void WebContextMenuClient::lookUpInDictionary(Frame* frame)
126 {
127     WebHTMLView* htmlView = (WebHTMLView*)[[kit(frame) frameView] documentView];
128     if(![htmlView isKindOfClass:[WebHTMLView class]])
129         return;
130     [htmlView _lookUpInDictionaryFromMenu:nil];
131 }
132
133 bool WebContextMenuClient::isSpeaking()
134 {
135     return [NSApp isSpeaking];
136 }
137
138 void WebContextMenuClient::speak(const String& string)
139 {
140     [NSApp speakString:[[(NSString*)string copy] autorelease]];
141 }
142
143 void WebContextMenuClient::stopSpeaking()
144 {
145     [NSApp stopSpeaking:nil];
146 }
147
148 ContextMenuItem WebContextMenuClient::shareMenuItem(const HitTestResult& hitTestResult)
149 {
150     if (![[NSMenuItem class] respondsToSelector:@selector(standardShareMenuItemWithItems:)])
151         return ContextMenuItem();
152
153     Node* node = hitTestResult.innerNonSharedNode();
154     if (!node)
155         return ContextMenuItem();
156
157     Frame* frame = node->document().frame();
158     if (!frame)
159         return ContextMenuItem();
160
161     URL downloadableMediaURL;
162     if (!hitTestResult.absoluteMediaURL().isEmpty() && hitTestResult.isDownloadableMedia())
163         downloadableMediaURL = hitTestResult.absoluteMediaURL();
164
165     RetainPtr<NSImage> nsImage;
166     if (Image* image = hitTestResult.image()) {
167         if (RefPtr<SharedBuffer> buffer = image->data())
168             nsImage = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:buffer->data() length:buffer->size()]]);
169     }
170
171     return ContextMenuItem::shareMenuItem(hitTestResult.absoluteLinkURL(), downloadableMediaURL, nsImage.get(), hitTestResult.selectedText());
172 }
173
174 bool WebContextMenuClient::clientFloatRectForNode(Node& node, FloatRect& rect) const
175 {
176     RenderObject* renderer = node.renderer();
177     if (!renderer) {
178         // This method shouldn't be called in cases where the controlled node hasn't rendered.
179         ASSERT_NOT_REACHED();
180         return false;
181     }
182
183     if (!is<RenderBox>(*renderer))
184         return false;
185     auto& renderBox = downcast<RenderBox>(*renderer);
186
187     LayoutRect layoutRect = renderBox.clientBoxRect();
188     FloatQuad floatQuad = renderBox.localToAbsoluteQuad(FloatQuad(layoutRect));
189     rect = floatQuad.boundingBox();
190
191     return true;
192 }
193
194 #if ENABLE(SERVICE_CONTROLS)
195 void WebContextMenuClient::sharingServicePickerWillBeDestroyed(WebSharingServicePickerController &)
196 {
197     m_sharingServicePickerController = nil;
198 }
199
200 WebCore::FloatRect WebContextMenuClient::screenRectForCurrentSharingServicePickerItem(WebSharingServicePickerController &)
201 {
202     Page* page = [m_webView page];
203     if (!page)
204         return NSZeroRect;
205
206     Node* node = page->contextMenuController().context().hitTestResult().innerNode();
207     if (!node)
208         return NSZeroRect;
209
210     FrameView* frameView = node->document().view();
211     if (!frameView) {
212         // This method shouldn't be called in cases where the controlled node isn't in a rendered view.
213         ASSERT_NOT_REACHED();
214         return NSZeroRect;
215     }
216
217     FloatRect rect;
218     if (!clientFloatRectForNode(*node, rect))
219         return NSZeroRect;
220
221     // FIXME: https://webkit.org/b/132915
222     // Ideally we'd like to convert the content rect to screen coordinates without the lossy float -> int conversion.
223     // Creating a rounded int rect works well in practice, but might still lead to off-by-one-pixel problems in edge cases.
224     IntRect intRect = roundedIntRect(rect);
225     return frameView->contentsToScreen(intRect);
226 }
227
228 RetainPtr<NSImage> WebContextMenuClient::imageForCurrentSharingServicePickerItem(WebSharingServicePickerController &)
229 {
230     Page* page = [m_webView page];
231     if (!page)
232         return nil;
233
234     Node* node = page->contextMenuController().context().hitTestResult().innerNode();
235     if (!node)
236         return nil;
237
238     FrameView* frameView = node->document().view();
239     if (!frameView) {
240         // This method shouldn't be called in cases where the controlled node isn't in a rendered view.
241         ASSERT_NOT_REACHED();
242         return nil;
243     }
244
245     FloatRect rect;
246     if (!clientFloatRectForNode(*node, rect))
247         return nil;
248
249     // This is effectively a snapshot, and will be painted in an unaccelerated fashion in line with FrameSnapshotting.
250     std::unique_ptr<ImageBuffer> buffer = ImageBuffer::create(rect.size(), Unaccelerated);
251     if (!buffer)
252         return nil;
253
254     VisibleSelection oldSelection = frameView->frame().selection().selection();
255     RefPtr<Range> range = Range::create(node->document(), Position(node, Position::PositionIsBeforeAnchor), Position(node, Position::PositionIsAfterAnchor));
256     frameView->frame().selection().setSelection(VisibleSelection(*range), FrameSelection::DoNotSetFocus);
257
258     PaintBehavior oldPaintBehavior = frameView->paintBehavior();
259     frameView->setPaintBehavior(PaintBehaviorSelectionOnly);
260
261     buffer->context().translate(-toFloatSize(rect.location()));
262     frameView->paintContents(buffer->context(), roundedIntRect(rect));
263
264     frameView->frame().selection().setSelection(oldSelection);
265     frameView->setPaintBehavior(oldPaintBehavior);
266
267     RefPtr<Image> image = buffer->copyImage(DontCopyBackingStore);
268     if (!image)
269         return nil;
270
271     return [[image->getNSImage() retain] autorelease];
272 }
273 #endif
274
275 NSMenu *WebContextMenuClient::contextMenuForEvent(NSEvent *event, NSView *view, bool& isServicesMenu)
276 {
277     isServicesMenu = false;
278
279     Page* page = [m_webView page];
280     if (!page)
281         return nil;
282
283 #if ENABLE(SERVICE_CONTROLS) && defined(__LP64__)
284     if (Image* image = page->contextMenuController().context().controlledImage()) {
285         ASSERT(page->contextMenuController().context().hitTestResult().innerNode());
286
287         RetainPtr<NSItemProvider> itemProvider = adoptNS([[NSItemProvider alloc] initWithItem:image->getNSImage() typeIdentifier:@"public.image"]);
288
289         bool isContentEditable = page->contextMenuController().context().hitTestResult().innerNode()->isContentEditable();
290         m_sharingServicePickerController = adoptNS([[WebSharingServicePickerController alloc] initWithItems:@[ itemProvider.get() ] includeEditorServices:isContentEditable client:this style:NSSharingServicePickerStyleRollover]);
291
292         isServicesMenu = true;
293         return [m_sharingServicePickerController menu];
294     }
295 #endif
296
297     return [view menuForEvent:event];
298 }
299
300 void WebContextMenuClient::showContextMenu()
301 {
302     Page* page = [m_webView page];
303     if (!page)
304         return;
305     Frame* frame = page->contextMenuController().hitTestResult().innerNodeFrame();
306     if (!frame)
307         return;
308     FrameView* frameView = frame->view();
309     if (!frameView)
310         return;
311
312     NSView* view = frameView->documentView();
313     IntPoint point = frameView->contentsToWindow(page->contextMenuController().hitTestResult().roundedPointInInnerNodeFrame());
314     NSEvent* event = [NSEvent mouseEventWithType:NSRightMouseDown location:point modifierFlags:0 timestamp:0 windowNumber:[[view window] windowNumber] context:0 eventNumber:0 clickCount:1 pressure:1];
315
316     // Show the contextual menu for this event.
317     bool isServicesMenu;
318     if (NSMenu *menu = contextMenuForEvent(event, view, isServicesMenu)) {
319         if (isServicesMenu)
320             [menu popUpMenuPositioningItem:nil atLocation:[view convertPoint:point toView:nil] inView:view];
321         else
322             [NSMenu popUpContextMenu:menu withEvent:event forView:view];
323     }
324 }
325
326 #endif // !PLATFORM(IOS)