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_controller(&controller)
70 , m_range(WTFMove(range))
71 , m_graphicsLayer(GraphicsLayer::create(controller.page().chrome().client().graphicsLayerFactory(), *this))
76 m_graphicsLayer->setDrawsContent(true);
78 setDDHighlight(ddHighlight.get());
80 // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
81 // in our animations, we get the right initial value regardless of flush timing.
82 downcast<GraphicsLayerCA>(layer()).platformCALayer()->setOpacity(0);
84 controller.didCreateHighlight(this);
87 ServicesOverlayController::Highlight::~Highlight()
90 m_controller->willDestroyHighlight(this);
93 void ServicesOverlayController::Highlight::setDDHighlight(DDHighlightRef highlight)
95 if (!DataDetectorsLibrary())
101 m_ddHighlight = highlight;
106 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(m_ddHighlight.get());
107 m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
108 m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
110 m_graphicsLayer->setNeedsDisplay();
113 void ServicesOverlayController::Highlight::invalidate()
115 layer().removeFromParent();
116 m_controller = nullptr;
119 void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
124 m_controller->page().chrome().client().scheduleCompositingLayerFlush();
127 void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect&, GraphicsLayerPaintBehavior)
129 if (!DataDetectorsLibrary())
132 CGContextRef cgContext = graphicsContext.platformContext();
134 ALLOW_DEPRECATED_DECLARATIONS_BEGIN
135 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
136 ALLOW_DEPRECATED_DECLARATIONS_END
137 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
138 highlightBoundingRect.origin = CGPointZero;
140 CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
143 float ServicesOverlayController::Highlight::deviceScaleFactor() const
148 return m_controller->page().deviceScaleFactor();
151 void ServicesOverlayController::Highlight::fadeIn()
153 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
154 [animation setDuration:highlightFadeAnimationDuration];
155 [animation setFillMode:kCAFillModeForwards];
156 [animation setRemovedOnCompletion:false];
157 [animation setToValue:@1];
159 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationCocoa::create(animation.get());
160 downcast<GraphicsLayerCA>(layer()).platformCALayer()->addAnimationForKey("FadeHighlightIn", *platformAnimation);
163 void ServicesOverlayController::Highlight::fadeOut()
165 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
166 [animation setDuration:highlightFadeAnimationDuration];
167 [animation setFillMode:kCAFillModeForwards];
168 [animation setRemovedOnCompletion:false];
169 [animation setToValue:@0];
171 RefPtr<Highlight> retainedSelf = this;
172 [CATransaction begin];
173 [CATransaction setCompletionBlock:[retainedSelf] () {
174 retainedSelf->didFinishFadeOutAnimation();
177 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationCocoa::create(animation.get());
178 downcast<GraphicsLayerCA>(layer()).platformCALayer()->addAnimationForKey("FadeHighlightOut", *platformAnimation);
179 [CATransaction commit];
182 void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
187 if (m_controller->activeHighlight() == this)
190 layer().removeFromParent();
193 static IntRect textQuadsToBoundingRectForRange(Range& range)
195 Vector<FloatQuad> textQuads;
196 range.absoluteTextQuads(textQuads);
197 FloatRect boundingRect;
198 for (auto& quad : textQuads)
199 boundingRect.unite(quad.boundingBox());
200 return enclosingIntRect(boundingRect);
203 ServicesOverlayController::ServicesOverlayController(Page& page)
205 , m_determineActiveHighlightTimer(*this, &ServicesOverlayController::determineActiveHighlightTimerFired)
206 , m_buildHighlightsTimer(*this, &ServicesOverlayController::buildPotentialHighlightsIfNeeded)
210 ServicesOverlayController::~ServicesOverlayController()
212 for (auto& highlight : m_highlights)
213 highlight->invalidate();
216 void ServicesOverlayController::willMoveToPage(PageOverlay&, Page* page)
221 ASSERT(m_servicesOverlay);
222 m_servicesOverlay = nullptr;
225 void ServicesOverlayController::didMoveToPage(PageOverlay&, Page*)
229 static const uint8_t AlignmentNone = 0;
230 static const uint8_t AlignmentLeft = 1 << 0;
231 static const uint8_t AlignmentRight = 1 << 1;
233 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
235 if (!gap.left().isEmpty()) {
236 LayoutUnit leftEdge = gap.left().x();
237 for (unsigned i = 0; i < rects.size(); ++i) {
238 if (alignments[i] & AlignmentLeft)
239 rects[i].shiftXEdgeTo(leftEdge);
243 if (!gap.right().isEmpty()) {
244 LayoutUnit rightEdge = gap.right().maxX();
245 for (unsigned i = 0; i < rects.size(); ++i) {
246 if (alignments[i] & AlignmentRight)
247 rects[i].shiftMaxXEdgeTo(rightEdge);
252 static inline void stitchRects(Vector<LayoutRect>& rects)
254 if (rects.size() <= 1)
257 Vector<LayoutRect> newRects;
259 // FIXME: Need to support vertical layout.
260 // First stitch together all the rects on the first line of the selection.
261 size_t indexFromStart = 0;
262 LayoutUnit firstTop = rects[indexFromStart].y();
263 LayoutRect& currentRect = rects[indexFromStart];
264 while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
265 currentRect.unite(rects[indexFromStart++]);
267 newRects.append(currentRect);
268 if (indexFromStart == rects.size()) {
269 // All the rects are on one line. There is nothing else to do.
270 rects.swap(newRects);
274 // Next stitch together all the rects on the last line of the selection.
275 size_t indexFromEnd = rects.size() - 1;
276 LayoutUnit lastTop = rects[indexFromEnd].y();
277 LayoutRect lastRect = rects[indexFromEnd];
278 while (indexFromEnd >= indexFromStart && rects[indexFromEnd].y() == lastTop)
279 lastRect.unite(rects[indexFromEnd--]);
281 // indexFromStart is the index of the first rectangle on the second line.
282 // indexFromEnd is the index of the last rectangle on the second to the last line.
283 // if they are equal, there is one additional rectangle for the line in the middle.
284 if (indexFromEnd == indexFromStart)
285 newRects.append(rects[indexFromStart]);
287 if (indexFromEnd <= indexFromStart) {
288 // There are no more rects to stitch. Just append the last line.
289 newRects.append(lastRect);
290 rects.swap(newRects);
294 // Stitch together all the rects after the first line until the second to the last included.
295 currentRect = rects[indexFromStart];
296 while (indexFromStart != indexFromEnd)
297 currentRect.unite(rects[++indexFromStart]);
299 newRects.append(currentRect);
300 newRects.append(lastRect);
302 rects.swap(newRects);
305 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
309 // FIXME: The following alignments are correct for LTR text.
310 // We should also account for RTL.
311 uint8_t alignments[3];
312 if (rects.size() == 1) {
313 alignments[0] = AlignmentLeft | AlignmentRight;
314 alignments[1] = AlignmentNone;
315 alignments[2] = AlignmentNone;
316 } else if (rects.size() == 2) {
317 alignments[0] = AlignmentRight;
318 alignments[1] = AlignmentLeft;
319 alignments[2] = AlignmentNone;
321 alignments[0] = AlignmentRight;
322 alignments[1] = AlignmentLeft | AlignmentRight;
323 alignments[2] = AlignmentLeft;
326 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
327 for (auto& gap : gapRects)
328 expandForGap(rects, alignments, gap);
330 // If we have 3 rects we might need one final GapRects to align the edges.
331 if (rects.size() == 3) {
334 for (unsigned i = 0; i < 3; ++i) {
335 if (alignments[i] & AlignmentLeft) {
338 else if (rects[i].x() < left.x())
341 if (alignments[i] & AlignmentRight) {
344 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
349 if (!left.isEmpty() || !right.isEmpty()) {
352 gap.uniteRight(right);
353 expandForGap(rects, alignments, gap);
358 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
360 m_currentSelectionRects = rects;
361 m_isTextOnly = isTextOnly;
363 m_lastSelectionChangeTime = MonotonicTime::now();
365 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
367 // DataDetectors needs these reversed in order to place the arrow in the right location.
368 m_currentSelectionRects.reverse();
370 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
371 invalidateHighlightsOfType(Highlight::SelectionType);
374 void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
376 LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
377 invalidateHighlightsOfType(Highlight::TelephoneNumberType);
380 void ServicesOverlayController::invalidateHighlightsOfType(Highlight::Type type)
382 if (!m_page.settings().serviceControlsEnabled())
385 m_dirtyHighlightTypes |= type;
386 m_buildHighlightsTimer.startOneShot(0_s);
389 void ServicesOverlayController::buildPotentialHighlightsIfNeeded()
391 if (!m_dirtyHighlightTypes)
394 if (m_dirtyHighlightTypes & Highlight::TelephoneNumberType)
395 buildPhoneNumberHighlights();
397 if (m_dirtyHighlightTypes & Highlight::SelectionType)
398 buildSelectionHighlight();
400 m_dirtyHighlightTypes = 0;
402 if (m_potentialHighlights.isEmpty()) {
403 if (m_servicesOverlay)
404 m_page.pageOverlayController().uninstallPageOverlay(*m_servicesOverlay, PageOverlay::FadeMode::DoNotFade);
408 if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
411 createOverlayIfNeeded();
413 bool mouseIsOverButton;
414 determineActiveHighlight(mouseIsOverButton);
417 bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
419 if (!DataDetectorsLibrary())
423 bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
424 mouseIsOverButton = onButton;
428 Seconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
433 Seconds minimumTimeUntilHighlightShouldBeShown = 200_ms;
434 if (m_page.focusController().focusedOrMainFrame().selection().selection().isContentEditable())
435 minimumTimeUntilHighlightShouldBeShown = 1_s;
437 bool mousePressed = mainFrame().eventHandler().mousePressed();
439 // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
440 // by virtue of being expanded to include the entire telephone number. However, we will still avoid highlighting
441 // telephone numbers while the mouse is down.
442 if (highlight->type() == Highlight::TelephoneNumberType)
443 return mousePressed ? minimumTimeUntilHighlightShouldBeShown : 0_s;
445 MonotonicTime now = MonotonicTime::now();
446 Seconds timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
447 Seconds timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
448 Seconds timeSinceLastMouseUp = mousePressed ? 0_s : now - m_lastMouseUpTime;
450 return minimumTimeUntilHighlightShouldBeShown - std::min(std::min(timeSinceLastSelectionChange, timeSinceHighlightBecameActive), timeSinceLastMouseUp);
453 void ServicesOverlayController::determineActiveHighlightTimerFired()
455 bool mouseIsOverButton;
456 determineActiveHighlight(mouseIsOverButton);
459 void ServicesOverlayController::drawRect(PageOverlay&, GraphicsContext&, const IntRect&)
463 void ServicesOverlayController::clearActiveHighlight()
465 if (!m_activeHighlight)
468 if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
469 m_currentMouseDownOnButtonHighlight = nullptr;
470 m_activeHighlight = nullptr;
473 void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
475 Vector<RefPtr<Highlight>> highlightsToRemove;
476 for (auto& highlight : m_potentialHighlights) {
477 if (highlight->type() == type)
478 highlightsToRemove.append(highlight);
481 while (!highlightsToRemove.isEmpty())
482 m_potentialHighlights.remove(highlightsToRemove.takeLast());
485 void ServicesOverlayController::buildPhoneNumberHighlights()
487 Vector<RefPtr<Range>> phoneNumberRanges;
488 for (Frame* frame = &mainFrame(); frame; frame = frame->tree().traverseNext())
489 phoneNumberRanges.appendVector(frame->editor().detectedTelephoneNumberRanges());
491 if (phoneNumberRanges.isEmpty()) {
492 removeAllPotentialHighlightsOfType(Highlight::TelephoneNumberType);
496 if (!DataDetectorsLibrary())
499 HashSet<RefPtr<Highlight>> newPotentialHighlights;
501 FrameView& mainFrameView = *mainFrame().view();
503 for (auto& range : phoneNumberRanges) {
504 // FIXME: This will choke if the range wraps around the edge of the view.
505 // What should we do in that case?
506 IntRect rect = textQuadsToBoundingRectForRange(*range);
508 // Convert to the main document's coordinate space.
509 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
510 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
511 // not equivalent to what we need when you have a topContentInset or a header banner.
512 FrameView* viewForRange = range->ownerDocument().view();
515 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
517 CGRect cgRect = rect;
518 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightStyleBubbleStandard | DDHighlightStyleStandardIconArrow, YES, NSWritingDirectionNatural, NO, YES));
520 newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range.releaseNonNull()));
523 replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::TelephoneNumberType);
526 void ServicesOverlayController::buildSelectionHighlight()
528 if (m_currentSelectionRects.isEmpty()) {
529 removeAllPotentialHighlightsOfType(Highlight::SelectionType);
533 if (!DataDetectorsLibrary())
536 HashSet<RefPtr<Highlight>> newPotentialHighlights;
538 Vector<CGRect> cgRects;
539 cgRects.reserveCapacity(m_currentSelectionRects.size());
541 RefPtr<Range> selectionRange = m_page.selection().firstRange();
542 if (selectionRange) {
543 FrameView* mainFrameView = mainFrame().view();
547 FrameView* viewForRange = selectionRange->ownerDocument().view();
549 for (auto& rect : m_currentSelectionRects) {
550 IntRect currentRect = snappedIntRect(rect);
551 currentRect.setLocation(mainFrameView->windowToContents(viewForRange->contentsToWindow(currentRect.location())));
552 cgRects.append((CGRect)currentRect);
555 if (!cgRects.isEmpty()) {
556 CGRect visibleRect = mainFrameView->visibleContentRect();
557 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightStyleBubbleNone | DDHighlightStyleStandardIconArrow | DDHighlightStyleButtonShowAlways, YES, NSWritingDirectionNatural, NO, YES));
559 newPotentialHighlights.add(Highlight::createForSelection(*this, ddHighlight, selectionRange.releaseNonNull()));
563 replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, Highlight::SelectionType);
566 void ServicesOverlayController::replaceHighlightsOfTypePreservingEquivalentHighlights(HashSet<RefPtr<Highlight>>& newPotentialHighlights, Highlight::Type type)
568 // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
569 // one so that any metadata is retained.
570 HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
572 for (auto& oldHighlight : m_potentialHighlights) {
573 if (oldHighlight->type() != type)
576 for (auto& newHighlight : newPotentialHighlights) {
577 if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
578 oldHighlight->setDDHighlight(newHighlight->ddHighlight());
580 reusedPotentialHighlights.add(oldHighlight);
581 newPotentialHighlights.remove(newHighlight);
588 removeAllPotentialHighlightsOfType(type);
590 m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
591 m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
594 bool ServicesOverlayController::hasRelevantSelectionServices()
596 return m_page.chrome().client().hasRelevantSelectionServices(m_isTextOnly);
599 void ServicesOverlayController::createOverlayIfNeeded()
601 if (m_servicesOverlay)
604 if (!m_page.settings().serviceControlsEnabled())
607 auto overlay = PageOverlay::create(*this, PageOverlay::OverlayType::Document);
608 m_servicesOverlay = overlay.ptr();
609 m_page.pageOverlayController().installPageOverlay(WTFMove(overlay), PageOverlay::FadeMode::DoNotFade);
612 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
614 return m_page.focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
617 bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
623 return a->type() == b->type() && areRangesEqual(&a->range(), &b->range());
626 ServicesOverlayController::Highlight* ServicesOverlayController::findTelephoneNumberHighlightContainingSelectionHighlight(Highlight& selectionHighlight)
628 if (selectionHighlight.type() != Highlight::SelectionType)
631 const VisibleSelection& selection = m_page.selection();
632 if (!selection.isRange())
635 RefPtr<Range> activeSelectionRange = selection.toNormalizedRange();
636 if (!activeSelectionRange)
639 for (auto& highlight : m_potentialHighlights) {
640 if (highlight->type() != Highlight::TelephoneNumberType)
643 if (highlight->range().contains(*activeSelectionRange))
644 return highlight.get();
650 void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
652 buildPotentialHighlightsIfNeeded();
654 mouseIsOverActiveHighlightButton = false;
656 RefPtr<Highlight> newActiveHighlight;
658 for (auto& highlight : m_potentialHighlights) {
659 if (highlight->type() == Highlight::SelectionType) {
660 // If we've already found a new active highlight, and it's
661 // a telephone number highlight, prefer that over this selection highlight.
662 if (newActiveHighlight && newActiveHighlight->type() == Highlight::TelephoneNumberType)
665 // If this highlight has no compatible services, it can't be active.
666 if (!hasRelevantSelectionServices())
670 // If this highlight isn't hovered, it can't be active.
671 bool mouseIsOverButton;
672 if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
675 newActiveHighlight = highlight;
676 mouseIsOverActiveHighlightButton = mouseIsOverButton;
679 // If our new active highlight is a selection highlight that is completely contained
680 // by one of the phone number highlights, we'll make the phone number highlight active even if it's not hovered.
681 if (newActiveHighlight && newActiveHighlight->type() == Highlight::SelectionType) {
682 if (Highlight* containedTelephoneNumberHighlight = findTelephoneNumberHighlightContainingSelectionHighlight(*newActiveHighlight)) {
683 newActiveHighlight = containedTelephoneNumberHighlight;
685 // We will always initially choose the telephone number highlight over the selection highlight if the
686 // mouse is over the telephone number highlight's button, so we know that it's not hovered if we got here.
687 mouseIsOverActiveHighlightButton = false;
691 if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
692 // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
693 // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
694 // highlight, and only reset the active highlight hysteresis when that changes.
695 if (m_nextActiveHighlight != newActiveHighlight) {
696 m_nextActiveHighlight = newActiveHighlight;
697 m_nextActiveHighlightChangeTime = MonotonicTime::now();
700 m_currentMouseDownOnButtonHighlight = nullptr;
702 if (m_activeHighlight) {
703 m_activeHighlight->fadeOut();
704 m_activeHighlight = nullptr;
707 auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
708 if (remainingTimeUntilHighlightShouldBeShown > 0_s) {
709 m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
713 m_activeHighlight = WTFMove(m_nextActiveHighlight);
715 if (m_activeHighlight) {
716 Ref<GraphicsLayer> highlightLayer = m_activeHighlight->layer();
717 m_servicesOverlay->layer().addChild(WTFMove(highlightLayer));
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)