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 <QuartzCore/QuartzCore.h>
35#import <WebCore/Document.h>
36#import <WebCore/EventHandler.h>
37#import <WebCore/FloatQuad.h>
38#import <WebCore/FocusController.h>
39#import <WebCore/FrameView.h>
40#import <WebCore/GapRects.h>
41#import <WebCore/GraphicsContext.h>
42#import <WebCore/GraphicsLayer.h>
43#import <WebCore/GraphicsLayerCA.h>
44#import <WebCore/MainFrame.h>
45#import <WebCore/PlatformCAAnimationMac.h>
46#import <WebCore/SoftLinking.h>
47
48#if __has_include(<DataDetectors/DDHighlightDrawing.h>)
49#import <DataDetectors/DDHighlightDrawing.h>
50#else
51typedef void* DDHighlightRef;
52#endif
53
54#if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
55#import <DataDetectors/DDHighlightDrawing_Private.h>
56#endif
57
58const float highlightFadeAnimationDuration = 0.3;
59
60typedef NSUInteger DDHighlightStyle;
61static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
62static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
63
64SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
65SOFT_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))
66SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
67SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
68SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
69
70using namespace WebCore;
71
72namespace WebKit {
73
74PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
75{
76    return adoptRef(new Highlight(controller, Type::Selection, ddHighlight, range));
77}
78
79PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
80{
81    return adoptRef(new Highlight(controller, Type::TelephoneNumber, ddHighlight, range));
82}
83
84ServicesOverlayController::Highlight::Highlight(ServicesOverlayController& controller, Type type, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<WebCore::Range> range)
85    : m_range(range)
86    , m_type(type)
87    , m_controller(&controller)
88{
89    ASSERT(ddHighlight);
90    ASSERT(m_range);
91
92    DrawingArea* drawingArea = controller.webPage().drawingArea();
93    m_graphicsLayer = GraphicsLayer::create(drawingArea ? drawingArea->graphicsLayerFactory() : nullptr, *this);
94    m_graphicsLayer->setDrawsContent(true);
95
96    setDDHighlight(ddHighlight.get());
97
98    // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
99    // in our animations, we get the right initial value regardless of flush timing.
100    toGraphicsLayerCA(layer())->platformCALayer()->setOpacity(0);
101
102    controller.didCreateHighlight(this);
103}
104
105ServicesOverlayController::Highlight::~Highlight()
106{
107    if (m_controller)
108        m_controller->willDestroyHighlight(this);
109}
110
111void ServicesOverlayController::Highlight::setDDHighlight(DDHighlightRef highlight)
112{
113    if (!m_controller)
114        return;
115
116    m_ddHighlight = highlight;
117
118    if (!m_ddHighlight)
119        return;
120
121    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(m_ddHighlight.get());
122    m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
123    m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
124
125    m_graphicsLayer->setNeedsDisplay();
126}
127
128void ServicesOverlayController::Highlight::invalidate()
129{
130    layer()->removeFromParent();
131    m_controller = nullptr;
132}
133
134void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
135{
136    if (!m_controller)
137        return;
138
139    if (DrawingArea* drawingArea = m_controller->webPage().drawingArea())
140        drawingArea->scheduleCompositingLayerFlush();
141}
142
143void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect& inClip)
144{
145    CGContextRef cgContext = graphicsContext.platformContext();
146
147    CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
148    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
149    highlightBoundingRect.origin = CGPointZero;
150
151    CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
152}
153
154float ServicesOverlayController::Highlight::deviceScaleFactor() const
155{
156    if (!m_controller)
157        return 1;
158
159    return m_controller->webPage().deviceScaleFactor();
160}
161
162void ServicesOverlayController::Highlight::fadeIn()
163{
164    RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
165    [animation setDuration:highlightFadeAnimationDuration];
166    [animation setFillMode:kCAFillModeForwards];
167    [animation setRemovedOnCompletion:false];
168    [animation setToValue:@1];
169
170    RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
171    toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightIn", platformAnimation.get());
172}
173
174void ServicesOverlayController::Highlight::fadeOut()
175{
176    RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
177    [animation setDuration:highlightFadeAnimationDuration];
178    [animation setFillMode:kCAFillModeForwards];
179    [animation setRemovedOnCompletion:false];
180    [animation setToValue:@0];
181
182    RefPtr<Highlight> retainedSelf = this;
183    [CATransaction begin];
184    [CATransaction setCompletionBlock:[retainedSelf] () {
185        retainedSelf->didFinishFadeOutAnimation();
186    }];
187
188    RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
189    toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightOut", platformAnimation.get());
190    [CATransaction commit];
191}
192
193void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
194{
195    if (!m_controller)
196        return;
197
198    if (m_controller->activeHighlight() == this)
199        return;
200
201    layer()->removeFromParent();
202}
203
204static IntRect textQuadsToBoundingRectForRange(Range& range)
205{
206    Vector<FloatQuad> textQuads;
207    range.textQuads(textQuads);
208    FloatRect boundingRect;
209    for (auto& quad : textQuads)
210        boundingRect.unite(quad.boundingBox());
211    return enclosingIntRect(boundingRect);
212}
213
214ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
215    : m_webPage(webPage)
216    , m_servicesOverlay(nullptr)
217    , m_isTextOnly(false)
218    , m_determineActiveHighlightTimer(this, &ServicesOverlayController::determineActiveHighlightTimerFired)
219{
220}
221
222ServicesOverlayController::~ServicesOverlayController()
223{
224    for (auto& highlight : m_highlights)
225        highlight->invalidate();
226}
227
228void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
229{
230    // Before the overlay is destroyed, it should have moved out of the WebPage,
231    // at which point we already cleared our back pointer.
232    ASSERT(!m_servicesOverlay);
233}
234
235void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
236{
237    if (webPage)
238        return;
239
240    ASSERT(m_servicesOverlay);
241    m_servicesOverlay = nullptr;
242}
243
244void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
245{
246}
247
248static const uint8_t AlignmentNone = 0;
249static const uint8_t AlignmentLeft = 1 << 0;
250static const uint8_t AlignmentRight = 1 << 1;
251
252static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
253{
254    if (!gap.left().isEmpty()) {
255        LayoutUnit leftEdge = gap.left().x();
256        for (unsigned i = 0; i < rects.size(); ++i) {
257            if (alignments[i] & AlignmentLeft)
258                rects[i].shiftXEdgeTo(leftEdge);
259        }
260    }
261
262    if (!gap.right().isEmpty()) {
263        LayoutUnit rightEdge = gap.right().maxX();
264        for (unsigned i = 0; i < rects.size(); ++i) {
265            if (alignments[i] & AlignmentRight)
266                rects[i].shiftMaxXEdgeTo(rightEdge);
267        }
268    }
269}
270
271static inline void stitchRects(Vector<LayoutRect>& rects)
272{
273    if (rects.size() <= 1)
274        return;
275
276    Vector<LayoutRect> newRects;
277
278    // FIXME: Need to support vertical layout.
279    // First stitch together all the rects on the first line of the selection.
280    size_t indexFromStart = 0;
281    LayoutUnit firstTop = rects[indexFromStart].y();
282    LayoutRect& currentRect = rects[indexFromStart];
283    while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
284        currentRect.unite(rects[indexFromStart++]);
285
286    newRects.append(currentRect);
287    if (indexFromStart == rects.size()) {
288        // All the rects are on one line. There is nothing else to do.
289        rects.swap(newRects);
290        return;
291    }
292
293    // Next stitch together all the rects on the last line of the selection.
294    size_t indexFromEnd = rects.size() - 1;
295    LayoutUnit lastTop = rects[indexFromEnd].y();
296    LayoutRect lastRect = rects[indexFromEnd];
297    while (indexFromEnd >= indexFromStart && rects[indexFromEnd].y() == lastTop)
298        lastRect.unite(rects[indexFromEnd--]);
299
300    // indexFromStart is the index of the first rectangle on the second line.
301    // indexFromEnd is the index of the last rectangle on the second to the last line.
302    // if they are equal, there is one additional rectangle for the line in the middle.
303    if (indexFromEnd == indexFromStart)
304        newRects.append(rects[indexFromStart]);
305
306    if (indexFromEnd <= indexFromStart) {
307        // There are no more rects to stitch. Just append the last line.
308        newRects.append(lastRect);
309        rects.swap(newRects);
310        return;
311    }
312
313    // Stitch together all the rects after the first line until the second to the last included.
314    currentRect = rects[indexFromStart];
315    while (indexFromStart != indexFromEnd)
316        currentRect.unite(rects[++indexFromStart]);
317
318    newRects.append(currentRect);
319    newRects.append(lastRect);
320
321    rects.swap(newRects);
322}
323
324static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
325{
326    stitchRects(rects);
327
328    // FIXME: The following alignments are correct for LTR text.
329    // We should also account for RTL.
330    uint8_t alignments[3];
331    if (rects.size() == 1) {
332        alignments[0] = AlignmentLeft | AlignmentRight;
333        alignments[1] = AlignmentNone;
334        alignments[2] = AlignmentNone;
335    } else if (rects.size() == 2) {
336        alignments[0] = AlignmentRight;
337        alignments[1] = AlignmentLeft;
338        alignments[2] = AlignmentNone;
339    } else {
340        alignments[0] = AlignmentRight;
341        alignments[1] = AlignmentLeft | AlignmentRight;
342        alignments[2] = AlignmentLeft;
343    }
344
345    // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
346    for (auto& gap : gapRects)
347        expandForGap(rects, alignments, gap);
348
349    // If we have 3 rects we might need one final GapRects to align the edges.
350    if (rects.size() == 3) {
351        LayoutRect left;
352        LayoutRect right;
353        for (unsigned i = 0; i < 3; ++i) {
354            if (alignments[i] & AlignmentLeft) {
355                if (left.isEmpty())
356                    left = rects[i];
357                else if (rects[i].x() < left.x())
358                    left = rects[i];
359            }
360            if (alignments[i] & AlignmentRight) {
361                if (right.isEmpty())
362                    right = rects[i];
363                else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
364                    right = rects[i];
365            }
366        }
367
368        if (!left.isEmpty() || !right.isEmpty()) {
369            GapRects gap;
370            gap.uniteLeft(left);
371            gap.uniteRight(right);
372            expandForGap(rects, alignments, gap);
373        }
374    }
375}
376
377void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
378{
379#if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
380    m_currentSelectionRects = rects;
381    m_isTextOnly = isTextOnly;
382
383    m_lastSelectionChangeTime = std::chrono::steady_clock::now();
384
385    compactRectsWithGapRects(m_currentSelectionRects, gapRects);
386
387    // DataDetectors needs these reversed in order to place the arrow in the right location.
388    m_currentSelectionRects.reverse();
389
390    LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
391
392    buildSelectionHighlight();
393#else
394    UNUSED_PARAM(rects);
395#endif
396}
397
398void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
399{
400#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
401    LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
402    buildPhoneNumberHighlights();
403#else
404    UNUSED_PARAM(ranges);
405#endif
406}
407
408bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
409{
410    Boolean onButton;
411    bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
412    mouseIsOverButton = onButton;
413    return hovered;
414}
415
416std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
417{
418    if (!highlight)
419        return std::chrono::milliseconds::zero();
420
421    auto minimumTimeUntilHighlightShouldBeShown = 200_ms;
422    if (m_webPage.corePage()->focusController().focusedOrMainFrame().selection().selection().isContentEditable())
423        minimumTimeUntilHighlightShouldBeShown = 1000_ms;
424
425    bool mousePressed = false;
426    if (Frame* mainFrame = m_webPage.mainFrame())
427        mousePressed = mainFrame->eventHandler().mousePressed();
428
429    // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
430    // by virtue of being expanded to include the entire telephone number. However, we will still avoid highlighting
431    // telephone numbers while the mouse is down.
432    if (highlight->type() == Highlight::Type::TelephoneNumber)
433        return mousePressed ? minimumTimeUntilHighlightShouldBeShown : 0_ms;
434
435    auto now = std::chrono::steady_clock::now();
436    auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
437    auto timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
438    auto timeSinceLastMouseUp = mousePressed ? 0_ms : now - m_lastMouseUpTime;
439
440    auto remainingDelay = minimumTimeUntilHighlightShouldBeShown - std::min(std::min(timeSinceLastSelectionChange, timeSinceHighlightBecameActive), timeSinceLastMouseUp);
441    return std::chrono::duration_cast<std::chrono::milliseconds>(remainingDelay);
442}
443
444void ServicesOverlayController::determineActiveHighlightTimerFired(Timer<ServicesOverlayController>&)
445{
446    bool mouseIsOverButton;
447    determineActiveHighlight(mouseIsOverButton);
448}
449
450void ServicesOverlayController::drawRect(PageOverlay* overlay, GraphicsContext& graphicsContext, const IntRect& dirtyRect)
451{
452}
453
454void ServicesOverlayController::clearActiveHighlight()
455{
456    if (!m_activeHighlight)
457        return;
458
459    if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
460        m_currentMouseDownOnButtonHighlight = nullptr;
461    m_activeHighlight = nullptr;
462}
463
464void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
465{
466    Vector<RefPtr<Highlight>> highlightsToRemove;
467    for (auto& highlight : m_potentialHighlights) {
468        if (highlight->type() == type)
469            highlightsToRemove.append(highlight);
470    }
471
472    while (!highlightsToRemove.isEmpty())
473        m_potentialHighlights.remove(highlightsToRemove.takeLast());
474}
475
476void ServicesOverlayController::buildPhoneNumberHighlights()
477{
478    if (!DataDetectorsLibrary())
479        return;
480
481    HashSet<RefPtr<Highlight>> newPotentialHighlights;
482
483    Frame* mainFrame = m_webPage.mainFrame();
484    FrameView& mainFrameView = *mainFrame->view();
485
486    for (Frame* frame = mainFrame; frame; frame = frame->tree().traverseNext()) {
487        auto& ranges = frame->editor().detectedTelephoneNumberRanges();
488        for (auto& range : ranges) {
489            // FIXME: This will choke if the range wraps around the edge of the view.
490            // What should we do in that case?
491            IntRect rect = textQuadsToBoundingRectForRange(*range);
492
493            // Convert to the main document's coordinate space.
494            // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
495            // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
496            // not equivalent to what we need when you have a topContentInset or a header banner.
497            FrameView* viewForRange = range->ownerDocument().view();
498            if (!viewForRange)
499                continue;
500            rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
501
502            CGRect cgRect = rect;
503            RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
504
505            newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range));
506        }
507    }
508
509    replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::Type::TelephoneNumber);
510
511    didRebuildPotentialHighlights();
512}
513
514void ServicesOverlayController::buildSelectionHighlight()
515{
516    if (!DataDetectorsLibrary())
517        return;
518
519    HashSet<RefPtr<Highlight>> newPotentialHighlights;
520
521    Vector<CGRect> cgRects;
522    cgRects.reserveCapacity(m_currentSelectionRects.size());
523
524    RefPtr<Range> selectionRange = m_webPage.corePage()->selection().firstRange();
525    if (selectionRange) {
526        Frame* mainFrame = m_webPage.mainFrame();
527        FrameView& mainFrameView = *mainFrame->view();
528        FrameView* viewForRange = selectionRange->ownerDocument().view();
529
530        for (auto& rect : m_currentSelectionRects) {
531            IntRect currentRect = pixelSnappedIntRect(rect);
532            currentRect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(currentRect.location())));
533            cgRects.append((CGRect)currentRect);
534        }
535
536        if (!cgRects.isEmpty()) {
537            CGRect visibleRect = m_webPage.corePage()->mainFrame().view()->visibleContentRect();
538            RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
539
540            newPotentialHighlights.add(Highlight::createForSelection(*this, ddHighlight, selectionRange));
541        }
542    }
543
544    replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::Type::Selection);
545
546    didRebuildPotentialHighlights();
547}
548
549void ServicesOverlayController::replaceHighlightsOfTypePreservingEquivalentHighlights(HashSet<RefPtr<Highlight>>& newPotentialHighlights, Highlight::Type type)
550{
551    // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
552    // one so that any metadata is retained.
553    HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
554
555    for (auto& oldHighlight : m_potentialHighlights) {
556        if (oldHighlight->type() != type)
557            continue;
558
559        for (auto& newHighlight : newPotentialHighlights) {
560            if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
561                oldHighlight->setDDHighlight(newHighlight->ddHighlight());
562
563                reusedPotentialHighlights.add(oldHighlight);
564                newPotentialHighlights.remove(newHighlight);
565
566                break;
567            }
568        }
569    }
570
571    removeAllPotentialHighlightsOfType(type);
572
573    m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
574    m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
575}
576
577bool ServicesOverlayController::hasRelevantSelectionServices()
578{
579    return (m_isTextOnly && WebProcess::shared().hasSelectionServices()) || WebProcess::shared().hasRichContentServices();
580}
581
582void ServicesOverlayController::didRebuildPotentialHighlights()
583{
584    if (m_potentialHighlights.isEmpty()) {
585        if (m_servicesOverlay)
586            m_webPage.uninstallPageOverlay(m_servicesOverlay);
587        return;
588    }
589
590    if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
591        return;
592
593    createOverlayIfNeeded();
594
595    bool mouseIsOverButton;
596    determineActiveHighlight(mouseIsOverButton);
597}
598
599void ServicesOverlayController::createOverlayIfNeeded()
600{
601    if (m_servicesOverlay)
602        return;
603
604    RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
605    m_servicesOverlay = overlay.get();
606    m_webPage.installPageOverlay(overlay.release(), PageOverlay::FadeMode::DoNotFade);
607}
608
609Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
610{
611    Page* page = m_webPage.corePage();
612    if (!page)
613        return Vector<RefPtr<Range>>();
614
615    return page->focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
616}
617
618bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
619{
620    if (a == b)
621        return true;
622
623    if (!a || !b)
624        return false;
625
626    if (a->type() == b->type() && areRangesEqual(a->range(), b->range()))
627        return true;
628
629    return false;
630}
631
632ServicesOverlayController::Highlight* ServicesOverlayController::findTelephoneNumberHighlightContainingSelectionHighlight(Highlight& selectionHighlight)
633{
634    if (selectionHighlight.type() != Highlight::Type::Selection)
635        return nullptr;
636
637    const VisibleSelection& selection = m_webPage.corePage()->selection();
638    if (!selection.isRange())
639        return nullptr;
640
641    RefPtr<Range> activeSelectionRange = selection.toNormalizedRange();
642    if (!activeSelectionRange)
643        return nullptr;
644
645    for (auto& highlight : m_potentialHighlights) {
646        if (highlight->type() != Highlight::Type::TelephoneNumber)
647            continue;
648
649        if (highlight->range()->contains(*activeSelectionRange))
650            return highlight.get();
651    }
652
653    return nullptr;
654}
655
656void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
657{
658    mouseIsOverActiveHighlightButton = false;
659
660    RefPtr<Highlight> newActiveHighlight;
661
662    for (auto& highlight : m_potentialHighlights) {
663        if (highlight->type() == Highlight::Type::Selection) {
664            // If we've already found a new active highlight, and it's
665            // a telephone number highlight, prefer that over this selection highlight.
666            if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::TelephoneNumber)
667                continue;
668
669            // If this highlight has no compatible services, it can't be active.
670            if (!hasRelevantSelectionServices())
671                continue;
672        }
673
674        // If this highlight isn't hovered, it can't be active.
675        bool mouseIsOverButton;
676        if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
677            continue;
678
679        newActiveHighlight = highlight;
680        mouseIsOverActiveHighlightButton = mouseIsOverButton;
681    }
682
683    // If our new active highlight is a selection highlight that is completely contained
684    // by one of the phone number highlights, we'll make the phone number highlight active even if it's not hovered.
685    if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::Selection) {
686        if (Highlight* containedTelephoneNumberHighlight = findTelephoneNumberHighlightContainingSelectionHighlight(*newActiveHighlight)) {
687            newActiveHighlight = containedTelephoneNumberHighlight;
688
689            // We will always initially choose the telephone number highlight over the selection highlight if the
690            // mouse is over the telephone number highlight's button, so we know that it's not hovered if we got here.
691            mouseIsOverActiveHighlightButton = false;
692        }
693    }
694
695    if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
696        // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
697        // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
698        // highlight, and only reset the active highlight hysteresis when that changes.
699        if (m_nextActiveHighlight != newActiveHighlight) {
700            m_nextActiveHighlight = newActiveHighlight;
701            m_nextActiveHighlightChangeTime = std::chrono::steady_clock::now();
702        }
703
704        m_currentMouseDownOnButtonHighlight = nullptr;
705
706        if (m_activeHighlight) {
707            m_activeHighlight->fadeOut();
708            m_activeHighlight = nullptr;
709        }
710
711        auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
712        if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
713            m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
714            return;
715        }
716
717        m_activeHighlight = m_nextActiveHighlight.release();
718
719        if (m_activeHighlight) {
720            m_servicesOverlay->layer()->addChild(m_activeHighlight->layer());
721            m_activeHighlight->fadeIn();
722        }
723    }
724}
725
726bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
727{
728    m_mousePosition = m_webPage.corePage()->mainFrame().view()->rootViewToContents(event.position());
729
730    bool mouseIsOverActiveHighlightButton = false;
731    determineActiveHighlight(mouseIsOverActiveHighlightButton);
732
733    // Cancel the potential click if any button other than the left button changes state, and ignore the event.
734    if (event.button() != WebMouseEvent::LeftButton) {
735        m_currentMouseDownOnButtonHighlight = nullptr;
736        return false;
737    }
738
739    // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
740    if (event.type() == WebEvent::MouseUp) {
741        RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
742        m_currentMouseDownOnButtonHighlight = nullptr;
743
744        m_lastMouseUpTime = std::chrono::steady_clock::now();
745
746        if (mouseIsOverActiveHighlightButton && mouseDownHighlight) {
747            handleClick(m_mousePosition, *mouseDownHighlight);
748            return true;
749        }
750
751        return false;
752    }
753
754    // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
755    if (event.type() == WebEvent::MouseMove) {
756        if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
757            return true;
758
759        m_currentMouseDownOnButtonHighlight = nullptr;
760        return false;
761    }
762
763    // If the mouse went down over the active highlight's button, track this as a potential click.
764    if (event.type() == WebEvent::MouseDown) {
765        if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
766            m_currentMouseDownOnButtonHighlight = m_activeHighlight;
767            return true;
768        }
769
770        return false;
771    }
772
773    return false;
774}
775
776void ServicesOverlayController::didScrollFrame(PageOverlay*, Frame* frame)
777{
778    if (frame->isMainFrame())
779        return;
780
781    buildPhoneNumberHighlights();
782    buildSelectionHighlight();
783
784    bool mouseIsOverActiveHighlightButton;
785    determineActiveHighlight(mouseIsOverActiveHighlightButton);
786}
787
788void ServicesOverlayController::handleClick(const IntPoint& clickPoint, Highlight& highlight)
789{
790    FrameView* frameView = m_webPage.mainFrameView();
791    if (!frameView)
792        return;
793
794    IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
795
796    if (highlight.type() == Highlight::Type::Selection) {
797        auto telephoneNumberRanges = telephoneNumberRangesForFocusedFrame();
798        Vector<String> selectedTelephoneNumbers;
799        selectedTelephoneNumbers.reserveCapacity(telephoneNumberRanges.size());
800        for (auto& range : telephoneNumberRanges)
801            selectedTelephoneNumbers.append(range->text());
802
803        m_webPage.handleSelectionServiceClick(m_webPage.corePage()->focusController().focusedOrMainFrame().selection(), selectedTelephoneNumbers, windowPoint);
804    } else if (highlight.type() == Highlight::Type::TelephoneNumber)
805        m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
806}
807
808void ServicesOverlayController::didCreateHighlight(Highlight* highlight)
809{
810    ASSERT(!m_highlights.contains(highlight));
811    m_highlights.add(highlight);
812}
813
814void ServicesOverlayController::willDestroyHighlight(Highlight* highlight)
815{
816    ASSERT(m_highlights.contains(highlight));
817    m_highlights.remove(highlight);
818}
819
820} // namespace WebKit
821
822#endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)
823