2 * Copyright (C) 2014 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #import "ServicesOverlayController.h"
29 #if (ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION)) && PLATFORM(MAC)
32 #import "ChromeClient.h"
35 #import "EventHandler.h"
37 #import "FocusController.h"
39 #import "FrameSelection.h"
42 #import "GraphicsContext.h"
43 #import "GraphicsLayer.h"
44 #import "GraphicsLayerCA.h"
47 #import "PageOverlayController.h"
48 #import "PlatformCAAnimationCocoa.h"
50 #import <QuartzCore/QuartzCore.h>
51 #import <pal/spi/mac/DataDetectorsSPI.h>
52 #import <wtf/SoftLinking.h>
54 const float highlightFadeAnimationDuration = 0.3;
58 Ref<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, Ref<Range>&& range)
60 return adoptRef(*new Highlight(controller, Highlight::SelectionType, ddHighlight, WTFMove(range)));
63 Ref<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, Ref<Range>&& range)
65 return adoptRef(*new Highlight(controller, Highlight::TelephoneNumberType, ddHighlight, WTFMove(range)));
68 ServicesOverlayController::Highlight::Highlight(ServicesOverlayController& controller, Type type, RetainPtr<DDHighlightRef> ddHighlight, Ref<WebCore::Range>&& range)
69 : m_range(WTFMove(range))
71 , m_controller(&controller)
75 auto& page = controller.page();
76 m_graphicsLayer = GraphicsLayer::create(page.chrome().client().graphicsLayerFactory(), *this);
77 m_graphicsLayer->setDrawsContent(true);
79 setDDHighlight(ddHighlight.get());
81 // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
82 // in our animations, we get the right initial value regardless of flush timing.
83 downcast<GraphicsLayerCA>(*layer()).platformCALayer()->setOpacity(0);
85 controller.didCreateHighlight(this);
88 ServicesOverlayController::Highlight::~Highlight()
91 m_controller->willDestroyHighlight(this);
94 void ServicesOverlayController::Highlight::setDDHighlight(DDHighlightRef highlight)
96 if (!DataDetectorsLibrary())
102 m_ddHighlight = highlight;
107 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(m_ddHighlight.get());
108 m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
109 m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
111 m_graphicsLayer->setNeedsDisplay();
114 void ServicesOverlayController::Highlight::invalidate()
116 layer()->removeFromParent();
117 m_controller = nullptr;
120 void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
125 m_controller->page().chrome().client().scheduleCompositingLayerFlush();
128 void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect&, GraphicsLayerPaintBehavior)
130 if (!DataDetectorsLibrary())
133 CGContextRef cgContext = graphicsContext.platformContext();
135 ALLOW_DEPRECATED_DECLARATIONS_BEGIN
136 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
137 ALLOW_DEPRECATED_DECLARATIONS_END
138 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
139 highlightBoundingRect.origin = CGPointZero;
141 CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
144 float ServicesOverlayController::Highlight::deviceScaleFactor() const
149 return m_controller->page().deviceScaleFactor();
152 void ServicesOverlayController::Highlight::fadeIn()
154 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
155 [animation setDuration:highlightFadeAnimationDuration];
156 [animation setFillMode:kCAFillModeForwards];
157 [animation setRemovedOnCompletion:false];
158 [animation setToValue:@1];
160 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationCocoa::create(animation.get());
161 downcast<GraphicsLayerCA>(*layer()).platformCALayer()->addAnimationForKey("FadeHighlightIn", *platformAnimation);
164 void ServicesOverlayController::Highlight::fadeOut()
166 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
167 [animation setDuration:highlightFadeAnimationDuration];
168 [animation setFillMode:kCAFillModeForwards];
169 [animation setRemovedOnCompletion:false];
170 [animation setToValue:@0];
172 RefPtr<Highlight> retainedSelf = this;
173 [CATransaction begin];
174 [CATransaction setCompletionBlock:[retainedSelf] () {
175 retainedSelf->didFinishFadeOutAnimation();
178 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationCocoa::create(animation.get());
179 downcast<GraphicsLayerCA>(*layer()).platformCALayer()->addAnimationForKey("FadeHighlightOut", *platformAnimation);
180 [CATransaction commit];
183 void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
188 if (m_controller->activeHighlight() == this)
191 layer()->removeFromParent();
194 static IntRect textQuadsToBoundingRectForRange(Range& range)
196 Vector<FloatQuad> textQuads;
197 range.absoluteTextQuads(textQuads);
198 FloatRect boundingRect;
199 for (auto& quad : textQuads)
200 boundingRect.unite(quad.boundingBox());
201 return enclosingIntRect(boundingRect);
204 ServicesOverlayController::ServicesOverlayController(Page& page)
206 , m_determineActiveHighlightTimer(*this, &ServicesOverlayController::determineActiveHighlightTimerFired)
207 , m_buildHighlightsTimer(*this, &ServicesOverlayController::buildPotentialHighlightsIfNeeded)
211 ServicesOverlayController::~ServicesOverlayController()
213 for (auto& highlight : m_highlights)
214 highlight->invalidate();
217 void ServicesOverlayController::willMoveToPage(PageOverlay&, Page* page)
222 ASSERT(m_servicesOverlay);
223 m_servicesOverlay = nullptr;
226 void ServicesOverlayController::didMoveToPage(PageOverlay&, Page*)
230 static const uint8_t AlignmentNone = 0;
231 static const uint8_t AlignmentLeft = 1 << 0;
232 static const uint8_t AlignmentRight = 1 << 1;
234 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
236 if (!gap.left().isEmpty()) {
237 LayoutUnit leftEdge = gap.left().x();
238 for (unsigned i = 0; i < rects.size(); ++i) {
239 if (alignments[i] & AlignmentLeft)
240 rects[i].shiftXEdgeTo(leftEdge);
244 if (!gap.right().isEmpty()) {
245 LayoutUnit rightEdge = gap.right().maxX();
246 for (unsigned i = 0; i < rects.size(); ++i) {
247 if (alignments[i] & AlignmentRight)
248 rects[i].shiftMaxXEdgeTo(rightEdge);
253 static inline void stitchRects(Vector<LayoutRect>& rects)
255 if (rects.size() <= 1)
258 Vector<LayoutRect> newRects;
260 // FIXME: Need to support vertical layout.
261 // First stitch together all the rects on the first line of the selection.
262 size_t indexFromStart = 0;
263 LayoutUnit firstTop = rects[indexFromStart].y();
264 LayoutRect& currentRect = rects[indexFromStart];
265 while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
266 currentRect.unite(rects[indexFromStart++]);
268 newRects.append(currentRect);
269 if (indexFromStart == rects.size()) {
270 // All the rects are on one line. There is nothing else to do.
271 rects.swap(newRects);
275 // Next stitch together all the rects on the last line of the selection.
276 size_t indexFromEnd = rects.size() - 1;
277 LayoutUnit lastTop = rects[indexFromEnd].y();
278 LayoutRect lastRect = rects[indexFromEnd];
279 while (indexFromEnd >= indexFromStart && rects[indexFromEnd].y() == lastTop)
280 lastRect.unite(rects[indexFromEnd--]);
282 // indexFromStart is the index of the first rectangle on the second line.
283 // indexFromEnd is the index of the last rectangle on the second to the last line.
284 // if they are equal, there is one additional rectangle for the line in the middle.
285 if (indexFromEnd == indexFromStart)
286 newRects.append(rects[indexFromStart]);
288 if (indexFromEnd <= indexFromStart) {
289 // There are no more rects to stitch. Just append the last line.
290 newRects.append(lastRect);
291 rects.swap(newRects);
295 // Stitch together all the rects after the first line until the second to the last included.
296 currentRect = rects[indexFromStart];
297 while (indexFromStart != indexFromEnd)
298 currentRect.unite(rects[++indexFromStart]);
300 newRects.append(currentRect);
301 newRects.append(lastRect);
303 rects.swap(newRects);
306 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
310 // FIXME: The following alignments are correct for LTR text.
311 // We should also account for RTL.
312 uint8_t alignments[3];
313 if (rects.size() == 1) {
314 alignments[0] = AlignmentLeft | AlignmentRight;
315 alignments[1] = AlignmentNone;
316 alignments[2] = AlignmentNone;
317 } else if (rects.size() == 2) {
318 alignments[0] = AlignmentRight;
319 alignments[1] = AlignmentLeft;
320 alignments[2] = AlignmentNone;
322 alignments[0] = AlignmentRight;
323 alignments[1] = AlignmentLeft | AlignmentRight;
324 alignments[2] = AlignmentLeft;
327 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
328 for (auto& gap : gapRects)
329 expandForGap(rects, alignments, gap);
331 // If we have 3 rects we might need one final GapRects to align the edges.
332 if (rects.size() == 3) {
335 for (unsigned i = 0; i < 3; ++i) {
336 if (alignments[i] & AlignmentLeft) {
339 else if (rects[i].x() < left.x())
342 if (alignments[i] & AlignmentRight) {
345 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
350 if (!left.isEmpty() || !right.isEmpty()) {
353 gap.uniteRight(right);
354 expandForGap(rects, alignments, gap);
359 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
361 m_currentSelectionRects = rects;
362 m_isTextOnly = isTextOnly;
364 m_lastSelectionChangeTime = MonotonicTime::now();
366 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
368 // DataDetectors needs these reversed in order to place the arrow in the right location.
369 m_currentSelectionRects.reverse();
371 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
372 invalidateHighlightsOfType(Highlight::SelectionType);
375 void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
377 LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
378 invalidateHighlightsOfType(Highlight::TelephoneNumberType);
381 void ServicesOverlayController::invalidateHighlightsOfType(Highlight::Type type)
383 if (!m_page.settings().serviceControlsEnabled())
386 m_dirtyHighlightTypes |= type;
387 m_buildHighlightsTimer.startOneShot(0_s);
390 void ServicesOverlayController::buildPotentialHighlightsIfNeeded()
392 if (!m_dirtyHighlightTypes)
395 if (m_dirtyHighlightTypes & Highlight::TelephoneNumberType)
396 buildPhoneNumberHighlights();
398 if (m_dirtyHighlightTypes & Highlight::SelectionType)
399 buildSelectionHighlight();
401 m_dirtyHighlightTypes = 0;
403 if (m_potentialHighlights.isEmpty()) {
404 if (m_servicesOverlay)
405 m_page.pageOverlayController().uninstallPageOverlay(*m_servicesOverlay, PageOverlay::FadeMode::DoNotFade);
409 if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
412 createOverlayIfNeeded();
414 bool mouseIsOverButton;
415 determineActiveHighlight(mouseIsOverButton);
418 bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
420 if (!DataDetectorsLibrary())
424 bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
425 mouseIsOverButton = onButton;
429 Seconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
434 Seconds minimumTimeUntilHighlightShouldBeShown = 200_ms;
435 if (m_page.focusController().focusedOrMainFrame().selection().selection().isContentEditable())
436 minimumTimeUntilHighlightShouldBeShown = 1_s;
438 bool mousePressed = mainFrame().eventHandler().mousePressed();
440 // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
441 // by virtue of being expanded to include the entire telephone number. However, we will still avoid highlighting
442 // telephone numbers while the mouse is down.
443 if (highlight->type() == Highlight::TelephoneNumberType)
444 return mousePressed ? minimumTimeUntilHighlightShouldBeShown : 0_s;
446 MonotonicTime now = MonotonicTime::now();
447 Seconds timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
448 Seconds timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
449 Seconds timeSinceLastMouseUp = mousePressed ? 0_s : now - m_lastMouseUpTime;
451 return minimumTimeUntilHighlightShouldBeShown - std::min(std::min(timeSinceLastSelectionChange, timeSinceHighlightBecameActive), timeSinceLastMouseUp);
454 void ServicesOverlayController::determineActiveHighlightTimerFired()
456 bool mouseIsOverButton;
457 determineActiveHighlight(mouseIsOverButton);
460 void ServicesOverlayController::drawRect(PageOverlay&, GraphicsContext&, const IntRect&)
464 void ServicesOverlayController::clearActiveHighlight()
466 if (!m_activeHighlight)
469 if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
470 m_currentMouseDownOnButtonHighlight = nullptr;
471 m_activeHighlight = nullptr;
474 void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
476 Vector<RefPtr<Highlight>> highlightsToRemove;
477 for (auto& highlight : m_potentialHighlights) {
478 if (highlight->type() == type)
479 highlightsToRemove.append(highlight);
482 while (!highlightsToRemove.isEmpty())
483 m_potentialHighlights.remove(highlightsToRemove.takeLast());
486 void ServicesOverlayController::buildPhoneNumberHighlights()
488 Vector<RefPtr<Range>> phoneNumberRanges;
489 for (Frame* frame = &mainFrame(); frame; frame = frame->tree().traverseNext())
490 phoneNumberRanges.appendVector(frame->editor().detectedTelephoneNumberRanges());
492 if (phoneNumberRanges.isEmpty()) {
493 removeAllPotentialHighlightsOfType(Highlight::TelephoneNumberType);
497 if (!DataDetectorsLibrary())
500 HashSet<RefPtr<Highlight>> newPotentialHighlights;
502 FrameView& mainFrameView = *mainFrame().view();
504 for (auto& range : phoneNumberRanges) {
505 // FIXME: This will choke if the range wraps around the edge of the view.
506 // What should we do in that case?
507 IntRect rect = textQuadsToBoundingRectForRange(*range);
509 // Convert to the main document's coordinate space.
510 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
511 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
512 // not equivalent to what we need when you have a topContentInset or a header banner.
513 FrameView* viewForRange = range->ownerDocument().view();
516 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
518 CGRect cgRect = rect;
519 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightStyleBubbleStandard | DDHighlightStyleStandardIconArrow, YES, NSWritingDirectionNatural, NO, YES));
521 newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range.releaseNonNull()));
524 replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::TelephoneNumberType);
527 void ServicesOverlayController::buildSelectionHighlight()
529 if (m_currentSelectionRects.isEmpty()) {
530 removeAllPotentialHighlightsOfType(Highlight::SelectionType);
534 if (!DataDetectorsLibrary())
537 HashSet<RefPtr<Highlight>> newPotentialHighlights;
539 Vector<CGRect> cgRects;
540 cgRects.reserveCapacity(m_currentSelectionRects.size());
542 RefPtr<Range> selectionRange = m_page.selection().firstRange();
543 if (selectionRange) {
544 FrameView* mainFrameView = mainFrame().view();
548 FrameView* viewForRange = selectionRange->ownerDocument().view();
550 for (auto& rect : m_currentSelectionRects) {
551 IntRect currentRect = snappedIntRect(rect);
552 currentRect.setLocation(mainFrameView->windowToContents(viewForRange->contentsToWindow(currentRect.location())));
553 cgRects.append((CGRect)currentRect);
556 if (!cgRects.isEmpty()) {
557 CGRect visibleRect = mainFrameView->visibleContentRect();
558 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightStyleBubbleNone | DDHighlightStyleStandardIconArrow | DDHighlightStyleButtonShowAlways, YES, NSWritingDirectionNatural, NO, YES));
560 newPotentialHighlights.add(Highlight::createForSelection(*this, ddHighlight, selectionRange.releaseNonNull()));
564 replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::SelectionType);
567 void ServicesOverlayController::replaceHighlightsOfTypePreservingEquivalentHighlights(HashSet<RefPtr<Highlight>>& newPotentialHighlights, Highlight::Type type)
569 // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
570 // one so that any metadata is retained.
571 HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
573 for (auto& oldHighlight : m_potentialHighlights) {
574 if (oldHighlight->type() != type)
577 for (auto& newHighlight : newPotentialHighlights) {
578 if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
579 oldHighlight->setDDHighlight(newHighlight->ddHighlight());
581 reusedPotentialHighlights.add(oldHighlight);
582 newPotentialHighlights.remove(newHighlight);
589 removeAllPotentialHighlightsOfType(type);
591 m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
592 m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
595 bool ServicesOverlayController::hasRelevantSelectionServices()
597 return m_page.chrome().client().hasRelevantSelectionServices(m_isTextOnly);
600 void ServicesOverlayController::createOverlayIfNeeded()
602 if (m_servicesOverlay)
605 if (!m_page.settings().serviceControlsEnabled())
608 auto overlay = PageOverlay::create(*this, PageOverlay::OverlayType::Document);
609 m_servicesOverlay = overlay.ptr();
610 m_page.pageOverlayController().installPageOverlay(WTFMove(overlay), PageOverlay::FadeMode::DoNotFade);
613 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
615 return m_page.focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
618 bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
624 return a->type() == b->type() && areRangesEqual(&a->range(), &b->range());
627 ServicesOverlayController::Highlight* ServicesOverlayController::findTelephoneNumberHighlightContainingSelectionHighlight(Highlight& selectionHighlight)
629 if (selectionHighlight.type() != Highlight::SelectionType)
632 const VisibleSelection& selection = m_page.selection();
633 if (!selection.isRange())
636 RefPtr<Range> activeSelectionRange = selection.toNormalizedRange();
637 if (!activeSelectionRange)
640 for (auto& highlight : m_potentialHighlights) {
641 if (highlight->type() != Highlight::TelephoneNumberType)
644 if (highlight->range().contains(*activeSelectionRange))
645 return highlight.get();
651 void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
653 buildPotentialHighlightsIfNeeded();
655 mouseIsOverActiveHighlightButton = false;
657 RefPtr<Highlight> newActiveHighlight;
659 for (auto& highlight : m_potentialHighlights) {
660 if (highlight->type() == Highlight::SelectionType) {
661 // If we've already found a new active highlight, and it's
662 // a telephone number highlight, prefer that over this selection highlight.
663 if (newActiveHighlight && newActiveHighlight->type() == Highlight::TelephoneNumberType)
666 // If this highlight has no compatible services, it can't be active.
667 if (!hasRelevantSelectionServices())
671 // If this highlight isn't hovered, it can't be active.
672 bool mouseIsOverButton;
673 if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
676 newActiveHighlight = highlight;
677 mouseIsOverActiveHighlightButton = mouseIsOverButton;
680 // If our new active highlight is a selection highlight that is completely contained
681 // by one of the phone number highlights, we'll make the phone number highlight active even if it's not hovered.
682 if (newActiveHighlight && newActiveHighlight->type() == Highlight::SelectionType) {
683 if (Highlight* containedTelephoneNumberHighlight = findTelephoneNumberHighlightContainingSelectionHighlight(*newActiveHighlight)) {
684 newActiveHighlight = containedTelephoneNumberHighlight;
686 // We will always initially choose the telephone number highlight over the selection highlight if the
687 // mouse is over the telephone number highlight's button, so we know that it's not hovered if we got here.
688 mouseIsOverActiveHighlightButton = false;
692 if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
693 // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
694 // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
695 // highlight, and only reset the active highlight hysteresis when that changes.
696 if (m_nextActiveHighlight != newActiveHighlight) {
697 m_nextActiveHighlight = newActiveHighlight;
698 m_nextActiveHighlightChangeTime = MonotonicTime::now();
701 m_currentMouseDownOnButtonHighlight = nullptr;
703 if (m_activeHighlight) {
704 m_activeHighlight->fadeOut();
705 m_activeHighlight = nullptr;
708 auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
709 if (remainingTimeUntilHighlightShouldBeShown > 0_s) {
710 m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
714 m_activeHighlight = WTFMove(m_nextActiveHighlight);
716 if (m_activeHighlight) {
717 m_servicesOverlay->layer().addChild(m_activeHighlight->layer());
718 m_activeHighlight->fadeIn();
723 bool ServicesOverlayController::mouseEvent(PageOverlay&, const PlatformMouseEvent& event)
725 m_mousePosition = mainFrame().view()->windowToContents(event.position());
727 bool mouseIsOverActiveHighlightButton = false;
728 determineActiveHighlight(mouseIsOverActiveHighlightButton);
730 // Cancel the potential click if any button other than the left button changes state, and ignore the event.
731 if (event.button() != MouseButton::LeftButton) {
732 m_currentMouseDownOnButtonHighlight = nullptr;
736 // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
737 if (event.type() == PlatformEvent::MouseReleased) {
738 RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
739 m_currentMouseDownOnButtonHighlight = nullptr;
741 m_lastMouseUpTime = MonotonicTime::now();
743 if (mouseIsOverActiveHighlightButton && mouseDownHighlight) {
744 handleClick(m_mousePosition, *mouseDownHighlight);
751 // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
752 if (event.type() == PlatformEvent::MouseMoved) {
753 if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
756 m_currentMouseDownOnButtonHighlight = nullptr;
760 // If the mouse went down over the active highlight's button, track this as a potential click.
761 if (event.type() == PlatformEvent::MousePressed) {
762 if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
763 m_currentMouseDownOnButtonHighlight = m_activeHighlight;
773 void ServicesOverlayController::didScrollFrame(PageOverlay&, Frame& frame)
775 if (frame.isMainFrame())
778 invalidateHighlightsOfType(Highlight::TelephoneNumberType);
779 invalidateHighlightsOfType(Highlight::SelectionType);
780 buildPotentialHighlightsIfNeeded();
782 bool mouseIsOverActiveHighlightButton;
783 determineActiveHighlight(mouseIsOverActiveHighlightButton);
786 void ServicesOverlayController::handleClick(const IntPoint& clickPoint, Highlight& highlight)
788 FrameView* frameView = mainFrame().view();
792 IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
794 if (highlight.type() == Highlight::SelectionType) {
795 auto telephoneNumberRanges = telephoneNumberRangesForFocusedFrame();
796 Vector<String> selectedTelephoneNumbers;
797 selectedTelephoneNumbers.reserveCapacity(telephoneNumberRanges.size());
798 for (auto& range : telephoneNumberRanges)
799 selectedTelephoneNumbers.append(range->text());
801 m_page.chrome().client().handleSelectionServiceClick(m_page.focusController().focusedOrMainFrame().selection(), selectedTelephoneNumbers, windowPoint);
802 } else if (highlight.type() == Highlight::TelephoneNumberType)
803 m_page.chrome().client().handleTelephoneNumberClick(highlight.range().text(), windowPoint);
806 Frame& ServicesOverlayController::mainFrame() const
808 return m_page.mainFrame();
811 void ServicesOverlayController::didCreateHighlight(Highlight* highlight)
813 ASSERT(!m_highlights.contains(highlight));
814 m_highlights.add(highlight);
817 void ServicesOverlayController::willDestroyHighlight(Highlight* highlight)
819 ASSERT(m_highlights.contains(highlight));
820 m_highlights.remove(highlight);
823 } // namespace WebKit
825 #endif // (ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION)) && PLATFORM(MAC)