Phone number highlights should always be visible if the mouse hovers over.
[WebKit-https.git] / Source / WebKit2 / WebProcess / WebPage / mac / ServicesOverlayController.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 "config.h"
27 #import "ServicesOverlayController.h"
28
29 #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)
30
31 #import "Logging.h"
32 #import "WebPage.h"
33 #import "WebProcess.h"
34 #import <WebCore/Document.h>
35 #import <WebCore/FloatQuad.h>
36 #import <WebCore/FrameView.h>
37 #import <WebCore/GapRects.h>
38 #import <WebCore/GraphicsContext.h>
39 #import <WebCore/MainFrame.h>
40 #import <WebCore/SoftLinking.h>
41
42 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
43 #import <DataDetectors/DDHighlightDrawing.h>
44 #else
45 typedef void* DDHighlightRef;
46 #endif
47
48 #if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
49 #import <DataDetectors/DDHighlightDrawing_Private.h>
50 #endif
51
52 typedef NSUInteger DDHighlightStyle;
53 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
54 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
55
56 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
57 SOFT_LINK(DataDetectors, DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection, DDHighlightRef, (CFAllocatorRef allocator, CGRect* rects, CFIndex count, CGRect globalVisibleRect, DDHighlightStyle style, Boolean withArrow, NSWritingDirection writingDirection, Boolean endsWithEOL, Boolean flipped), (allocator, rects, count, globalVisibleRect, style, withArrow, writingDirection, endsWithEOL, flipped))
58 SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
59 SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
60 SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
61
62 using namespace WebCore;
63
64 namespace WebKit {
65
66 static IntRect textQuadsToBoundingRectForRange(Range& range)
67 {
68     Vector<FloatQuad> textQuads;
69     range.textQuads(textQuads);
70     FloatRect boundingRect;
71     for (auto& quad : textQuads)
72         boundingRect.unite(quad.boundingBox());
73     return enclosingIntRect(boundingRect);
74 }
75
76 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
77     : m_webPage(&webPage)
78     , m_servicesOverlay(nullptr)
79 {
80 }
81
82 ServicesOverlayController::~ServicesOverlayController()
83 {
84     if (m_servicesOverlay) {
85         ASSERT(m_webPage);
86         m_webPage->uninstallPageOverlay(m_servicesOverlay, PageOverlay::FadeMode::DoNotFade);
87     }
88 }
89
90 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
91 {
92     // Before the overlay is destroyed, it should have moved out of the WebPage,
93     // at which point we already cleared our back pointer.
94     ASSERT(!m_servicesOverlay);
95 }
96
97 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
98 {
99     if (webPage)
100         return;
101
102     ASSERT(m_servicesOverlay);
103     m_servicesOverlay = nullptr;
104
105     ASSERT(m_webPage);
106     m_webPage = nullptr;
107 }
108
109 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
110 {
111 }
112
113 void ServicesOverlayController::createOverlayIfNeeded()
114 {
115     if (m_servicesOverlay) {
116         m_servicesOverlay->setNeedsDisplay();
117         return;
118     }
119
120     if (m_currentTelephoneNumberRanges.isEmpty() && (!WebProcess::shared().hasSelectionServices() || m_currentSelectionRects.isEmpty()))
121         return;
122
123     RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
124     m_servicesOverlay = overlay.get();
125     m_webPage->installPageOverlay(overlay.release(), PageOverlay::FadeMode::Fade);
126     m_servicesOverlay->setNeedsDisplay();
127 }
128
129 static const uint8_t AlignmentNone = 0;
130 static const uint8_t AlignmentLeft = 1 << 0;
131 static const uint8_t AlignmentRight = 1 << 1;
132
133 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
134 {
135     if (!gap.left().isEmpty()) {
136         LayoutUnit leftEdge = gap.left().x();
137         for (unsigned i = 0; i < 3; ++i) {
138             if (alignments[i] & AlignmentLeft)
139                 rects[i].shiftXEdgeTo(leftEdge);
140         }
141     }
142
143     if (!gap.right().isEmpty()) {
144         LayoutUnit rightEdge = gap.right().maxX();
145         for (unsigned i = 0; i < 3; ++i) {
146             if (alignments[i] & AlignmentRight)
147                 rects[i].shiftMaxXEdgeTo(rightEdge);
148         }
149     }
150 }
151
152 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
153 {
154     if (rects.isEmpty())
155         return;
156
157     // All of the middle rects - everything but the first and last - can be unioned together.
158     if (rects.size() > 3) {
159         LayoutRect united;
160         for (unsigned i = 1; i < rects.size() - 1; ++i)
161             united.unite(rects[i]);
162
163         rects[1] = united;
164         rects[2] = rects.last();
165         rects.shrink(3);
166     }
167
168     // FIXME: The following alignments are correct for LTR text.
169     // We should also account for RTL.
170     uint8_t alignments[3];
171     if (rects.size() == 1) {
172         alignments[0] = AlignmentLeft | AlignmentRight;
173         alignments[1] = AlignmentNone;
174         alignments[2] = AlignmentNone;
175     } else if (rects.size() == 2) {
176         alignments[0] = AlignmentRight;
177         alignments[1] = AlignmentLeft;
178         alignments[2] = AlignmentNone;
179     } else {
180         alignments[0] = AlignmentRight;
181         alignments[1] = AlignmentLeft | AlignmentRight;
182         alignments[2] = AlignmentLeft;
183     }
184
185     // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
186     for (auto& gap : gapRects)
187         expandForGap(rects, alignments, gap);
188
189     // If we have 3 rects we might need one final GapRects to align the edges.
190     if (rects.size() == 3) {
191         LayoutRect left;
192         LayoutRect right;
193         for (unsigned i = 0; i < 3; ++i) {
194             if (alignments[i] & AlignmentLeft) {
195                 if (left.isEmpty())
196                     left = rects[i];
197                 else if (rects[i].x() < left.x())
198                     left = rects[i];
199             }
200             if (alignments[i] & AlignmentRight) {
201                 if (right.isEmpty())
202                     right = rects[i];
203                 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
204                     right = rects[i];
205             }
206         }
207
208         if (!left.isEmpty() || !right.isEmpty()) {
209             GapRects gap;
210             gap.uniteLeft(left);
211             gap.uniteRight(right);
212             expandForGap(rects, alignments, gap);
213         }
214     }
215 }
216
217 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
218 {
219 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
220     clearSelectionHighlight();
221     m_currentSelectionRects = rects;
222
223     compactRectsWithGapRects(m_currentSelectionRects, gapRects);
224
225     // DataDetectors needs these reversed in order to place the arrow in the right location.
226     m_currentSelectionRects.reverse();
227
228     LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
229
230     createOverlayIfNeeded();
231 #else
232     UNUSED_PARAM(rects);
233 #endif
234 }
235
236 void ServicesOverlayController::selectedTelephoneNumberRangesChanged(const Vector<RefPtr<Range>>& ranges)
237 {
238 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
239     LOG(Services, "ServicesOverlayController - Telephone number ranges changed - Had %lu, now have %lu\n", m_currentTelephoneNumberRanges.size(), ranges.size());
240     m_currentTelephoneNumberRanges = ranges;
241     m_telephoneNumberHighlights.clear();
242     m_telephoneNumberHighlights.resize(ranges.size());
243
244     createOverlayIfNeeded();
245 #else
246     UNUSED_PARAM(ranges);
247 #endif
248 }
249
250 void ServicesOverlayController::clearHighlightState()
251 {
252     clearSelectionHighlight();
253     clearHoveredTelephoneNumberHighlight();
254
255     m_telephoneNumberHighlights.clear();
256 }
257
258 void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
259 {
260     if (m_currentSelectionRects.isEmpty() && m_currentTelephoneNumberRanges.isEmpty()) {
261         clearHighlightState();
262         return;
263     }
264
265     if (drawTelephoneNumberHighlightIfVisible(graphicsContext, dirtyRect))
266         return;
267
268     drawSelectionHighlight(graphicsContext, dirtyRect);
269 }
270
271 void ServicesOverlayController::drawSelectionHighlight(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
272 {
273     // It's possible to end up drawing the selection highlight before we've actually received the selection rects.
274     // If that happens we'll end up here again once we have the rects.
275     if (m_currentSelectionRects.isEmpty())
276         return;
277
278     // If there are no installed selection services and we have no phone numbers detected, then we have nothing to draw.
279     if (!WebProcess::shared().hasSelectionServices() && m_currentTelephoneNumberRanges.isEmpty())
280         return;
281
282     if (!m_selectionHighlight)
283         maybeCreateSelectionHighlight();
284
285     if (m_selectionHighlight)
286         drawHighlight(m_selectionHighlight.get(), graphicsContext);
287 }
288
289 bool ServicesOverlayController::drawTelephoneNumberHighlightIfVisible(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
290 {
291     // Make sure the hovered telephone number highlight is still hovered.
292     if (m_hoveredTelephoneNumberData) {
293         Boolean onButton;
294         if (!DDHighlightPointIsOnHighlight(m_hoveredTelephoneNumberData->highlight.get(), (CGPoint)m_mousePosition, &onButton))
295             clearHoveredTelephoneNumberHighlight();
296
297         bool foundMatchingRange = false;
298
299         // Make sure the hovered highlight still corresponds to a current telephone number range.
300         for (auto& range : m_currentTelephoneNumberRanges) {
301             if (areRangesEqual(range.get(), m_hoveredTelephoneNumberData->range.get())) {
302                 foundMatchingRange = true;
303                 break;
304             }
305         }
306
307         if (!foundMatchingRange)
308             clearHoveredTelephoneNumberHighlight();
309     }
310
311     // Found out which - if any - telephone number is hovered.
312     if (!m_hoveredTelephoneNumberData) {
313         Boolean onButton;
314         establishHoveredTelephoneHighlight(onButton);
315     }
316
317     // If a telephone number is actually hovered, draw it.
318     if (m_hoveredTelephoneNumberData) {
319         drawHighlight(m_hoveredTelephoneNumberData->highlight.get(), graphicsContext);
320         return true;
321     }
322
323     return false;
324 }
325
326 void ServicesOverlayController::drawHighlight(DDHighlightRef highlight, WebCore::GraphicsContext& graphicsContext)
327 {
328     ASSERT(highlight);
329
330     Boolean onButton;
331     bool mouseIsOverHighlight = DDHighlightPointIsOnHighlight(highlight, (CGPoint)m_mousePosition, &onButton);
332
333     if (!mouseIsOverHighlight) {
334         LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
335         return;
336     }
337
338     CGContextRef cgContext = graphicsContext.platformContext();
339     
340     CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight, cgContext);
341     CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight);
342     
343     GraphicsContextStateSaver stateSaver(graphicsContext);
344
345     graphicsContext.translate(toFloatSize(highlightBoundingRect.origin));
346
347     CGRect highlightDrawRect = highlightBoundingRect;
348     highlightDrawRect.origin.x = 0;
349     highlightDrawRect.origin.y = 0;
350     
351     CGContextDrawLayerInRect(cgContext, highlightDrawRect, highlightLayer);
352 }
353
354 void ServicesOverlayController::clearSelectionHighlight()
355 {
356     if (!m_selectionHighlight)
357         return;
358
359     if (m_currentHoveredHighlight == m_selectionHighlight)
360         m_currentHoveredHighlight = nullptr;
361     if (m_currentMouseDownOnButtonHighlight == m_selectionHighlight)
362         m_currentMouseDownOnButtonHighlight = nullptr;
363     m_selectionHighlight = nullptr;
364 }
365
366 void ServicesOverlayController::clearHoveredTelephoneNumberHighlight()
367 {
368     if (!m_hoveredTelephoneNumberData)
369         return;
370
371     if (m_currentHoveredHighlight == m_hoveredTelephoneNumberData->highlight)
372         m_currentHoveredHighlight = nullptr;
373     if (m_currentMouseDownOnButtonHighlight == m_hoveredTelephoneNumberData->highlight)
374         m_currentMouseDownOnButtonHighlight = nullptr;
375     m_hoveredTelephoneNumberData = nullptr;
376 }
377
378 void ServicesOverlayController::establishHoveredTelephoneHighlight(Boolean& onButton)
379 {
380     ASSERT(m_currentTelephoneNumberRanges.size() == m_telephoneNumberHighlights.size());
381
382     for (unsigned i = 0; i < m_currentTelephoneNumberRanges.size(); ++i) {
383         if (!m_telephoneNumberHighlights[i]) {
384             // FIXME: This will choke if the range wraps around the edge of the view.
385             // What should we do in that case?
386             IntRect rect = textQuadsToBoundingRectForRange(*m_currentTelephoneNumberRanges[i]);
387
388             // Convert to the main document's coordinate space.
389             // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
390             // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
391             // not equivalent to what we need when you have a topContentInset or a header banner.
392             FrameView* viewForRange = m_currentTelephoneNumberRanges[i]->ownerDocument().view();
393             if (!viewForRange)
394                 continue;
395             FrameView& mainFrameView = *m_webPage->corePage()->mainFrame().view();
396             rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
397
398             CGRect cgRect = rect;
399             m_telephoneNumberHighlights[i] = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, viewForRange->boundsRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
400         }
401
402         if (!DDHighlightPointIsOnHighlight(m_telephoneNumberHighlights[i].get(), (CGPoint)m_mousePosition, &onButton))
403             continue;
404
405         if (!m_hoveredTelephoneNumberData || m_hoveredTelephoneNumberData->highlight != m_telephoneNumberHighlights[i])
406             m_hoveredTelephoneNumberData = std::make_unique<TelephoneNumberData>(m_telephoneNumberHighlights[i], m_currentTelephoneNumberRanges[i]);
407
408         m_servicesOverlay->setNeedsDisplay();
409         return;
410     }
411
412     clearHoveredTelephoneNumberHighlight();
413     onButton = false;
414 }
415
416 void ServicesOverlayController::maybeCreateSelectionHighlight()
417 {
418     ASSERT(!m_selectionHighlight);
419     ASSERT(m_servicesOverlay);
420
421     Vector<CGRect> cgRects;
422     cgRects.reserveCapacity(m_currentSelectionRects.size());
423
424     for (auto& rect : m_currentSelectionRects)
425         cgRects.append((CGRect)pixelSnappedIntRect(rect));
426
427     if (!cgRects.isEmpty()) {
428         CGRect bounds = m_webPage->corePage()->mainFrame().view()->boundsRect();
429         m_selectionHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), bounds, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
430
431         m_servicesOverlay->setNeedsDisplay();
432     }
433 }
434
435 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
436 {
437     m_mousePosition = m_webPage->corePage()->mainFrame().view()->rootViewToContents(event.position());
438
439     DDHighlightRef oldHoveredHighlight = m_currentHoveredHighlight.get();
440
441     Boolean onButton = false;
442     establishHoveredTelephoneHighlight(onButton);
443     if (m_hoveredTelephoneNumberData) {
444         ASSERT(m_hoveredTelephoneNumberData->highlight);
445         m_currentHoveredHighlight = m_hoveredTelephoneNumberData->highlight;
446     } else {
447         if (!m_selectionHighlight)
448             maybeCreateSelectionHighlight();
449
450         if (m_selectionHighlight && DDHighlightPointIsOnHighlight(m_selectionHighlight.get(), (CGPoint)m_mousePosition, &onButton))
451             m_currentHoveredHighlight = m_selectionHighlight;
452         else
453             m_currentHoveredHighlight = nullptr;
454     }
455
456     if (oldHoveredHighlight != m_currentHoveredHighlight)
457         m_servicesOverlay->setNeedsDisplay();
458
459     // If this event has nothing to do with the left button, it clears the current mouse down tracking and we're done processing it.
460     if (event.button() != WebMouseEvent::LeftButton) {
461         m_currentMouseDownOnButtonHighlight = nullptr;
462         return false;
463     }
464
465     // Check and see if the mouse went up and we have a current mouse down highlight button.
466     if (event.type() == WebEvent::MouseUp) {
467         RetainPtr<DDHighlightRef> mouseDownHighlight = std::move(m_currentMouseDownOnButtonHighlight);
468
469         // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
470         if (onButton && mouseDownHighlight) {
471             handleClick(m_mousePosition, mouseDownHighlight.get());
472             return true;
473         }
474         
475         return false;
476     }
477
478     // Check and see if the mouse moved within the confines of the DD highlight button.
479     if (event.type() == WebEvent::MouseMove) {
480         // Moving with the mouse button down is okay as long as the mouse never leaves the highlight button.
481         if (m_currentMouseDownOnButtonHighlight && onButton)
482             return true;
483
484         m_currentMouseDownOnButtonHighlight = nullptr;
485         return false;
486     }
487
488     // Check and see if the mouse went down over a DD highlight button.
489     if (event.type() == WebEvent::MouseDown) {
490         if (m_currentHoveredHighlight && onButton) {
491             m_currentMouseDownOnButtonHighlight = m_currentHoveredHighlight;
492             m_servicesOverlay->setNeedsDisplay();
493             return true;
494         }
495
496         return false;
497     }
498         
499     return false;
500 }
501
502 void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, DDHighlightRef highlight)
503 {
504     ASSERT(highlight);
505
506     FrameView* frameView = m_webPage->mainFrameView();
507     if (!frameView)
508         return;
509
510     IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
511
512     if (highlight == m_selectionHighlight) {
513         Vector<String> selectedTelephoneNumbers;
514         selectedTelephoneNumbers.reserveCapacity(m_currentTelephoneNumberRanges.size());
515         for (auto& range : m_currentTelephoneNumberRanges)
516             selectedTelephoneNumbers.append(range->text());
517
518         m_webPage->handleSelectionServiceClick(m_webPage->corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
519     } else if (m_hoveredTelephoneNumberData && m_hoveredTelephoneNumberData->highlight == highlight)
520         m_webPage->handleTelephoneNumberClick(m_hoveredTelephoneNumberData->range->text(), windowPoint);
521     else
522         ASSERT_NOT_REACHED();
523 }
524     
525 } // namespace WebKit
526
527 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)