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)
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>
48 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
49 #import <DataDetectors/DDHighlightDrawing.h>
51 typedef void* DDHighlightRef;
54 #if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
55 #import <DataDetectors/DDHighlightDrawing_Private.h>
58 const float highlightFadeAnimationDuration = 0.3;
60 typedef NSUInteger DDHighlightStyle;
61 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
62 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
64 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
65 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))
66 SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
67 SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
68 SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
70 using namespace WebCore;
74 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight)
76 return adoptRef(new Highlight(controller, Type::Selection, ddHighlight, nullptr));
79 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
81 return adoptRef(new Highlight(controller, Type::TelephoneNumber, ddHighlight, range));
84 ServicesOverlayController::Highlight::Highlight(ServicesOverlayController& controller, Type type, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<WebCore::Range> range)
85 : m_ddHighlight(ddHighlight)
88 , m_controller(&controller)
90 ASSERT(m_ddHighlight);
91 ASSERT(type != Type::TelephoneNumber || m_range);
93 DrawingArea* drawingArea = controller.webPage().drawingArea();
94 m_graphicsLayer = GraphicsLayer::create(drawingArea ? drawingArea->graphicsLayerFactory() : nullptr, *this);
95 m_graphicsLayer->setDrawsContent(true);
96 m_graphicsLayer->setNeedsDisplay();
98 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight.get());
99 m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
100 m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
102 // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
103 // in our animations, we get the right initial value regardless of flush timing.
104 toGraphicsLayerCA(layer())->platformCALayer()->setOpacity(0);
106 controller.didCreateHighlight(this);
109 ServicesOverlayController::Highlight::~Highlight()
112 m_controller->willDestroyHighlight(this);
115 void ServicesOverlayController::Highlight::invalidate()
117 layer()->removeFromParent();
118 m_controller = nullptr;
121 void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
126 if (DrawingArea* drawingArea = m_controller->webPage().drawingArea())
127 drawingArea->scheduleCompositingLayerFlush();
130 void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect& inClip)
132 CGContextRef cgContext = graphicsContext.platformContext();
134 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
135 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
136 highlightBoundingRect.origin = CGPointZero;
138 CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
141 float ServicesOverlayController::Highlight::deviceScaleFactor() const
146 return m_controller->webPage().deviceScaleFactor();
149 void ServicesOverlayController::Highlight::fadeIn()
151 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
152 [animation setDuration:highlightFadeAnimationDuration];
153 [animation setFillMode:kCAFillModeForwards];
154 [animation setRemovedOnCompletion:false];
155 [animation setToValue:@1];
157 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
158 toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightIn", platformAnimation.get());
161 void ServicesOverlayController::Highlight::fadeOut()
163 RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
164 [animation setDuration:highlightFadeAnimationDuration];
165 [animation setFillMode:kCAFillModeForwards];
166 [animation setRemovedOnCompletion:false];
167 [animation setToValue:@0];
169 RefPtr<Highlight> retainedSelf = this;
170 [CATransaction begin];
171 [CATransaction setCompletionBlock:[retainedSelf] () {
172 retainedSelf->didFinishFadeOutAnimation();
175 RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
176 toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightOut", platformAnimation.get());
177 [CATransaction commit];
180 void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
185 if (m_controller->activeHighlight() == this)
188 layer()->removeFromParent();
191 static IntRect textQuadsToBoundingRectForRange(Range& range)
193 Vector<FloatQuad> textQuads;
194 range.textQuads(textQuads);
195 FloatRect boundingRect;
196 for (auto& quad : textQuads)
197 boundingRect.unite(quad.boundingBox());
198 return enclosingIntRect(boundingRect);
201 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
203 , m_servicesOverlay(nullptr)
204 , m_isTextOnly(false)
205 , m_determineActiveHighlightTimer(this, &ServicesOverlayController::determineActiveHighlightTimerFired)
209 ServicesOverlayController::~ServicesOverlayController()
211 for (auto& highlight : m_highlights)
212 highlight->invalidate();
215 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
217 // Before the overlay is destroyed, it should have moved out of the WebPage,
218 // at which point we already cleared our back pointer.
219 ASSERT(!m_servicesOverlay);
222 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
227 ASSERT(m_servicesOverlay);
228 m_servicesOverlay = nullptr;
231 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
235 static const uint8_t AlignmentNone = 0;
236 static const uint8_t AlignmentLeft = 1 << 0;
237 static const uint8_t AlignmentRight = 1 << 1;
239 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
241 if (!gap.left().isEmpty()) {
242 LayoutUnit leftEdge = gap.left().x();
243 for (unsigned i = 0; i < rects.size(); ++i) {
244 if (alignments[i] & AlignmentLeft)
245 rects[i].shiftXEdgeTo(leftEdge);
249 if (!gap.right().isEmpty()) {
250 LayoutUnit rightEdge = gap.right().maxX();
251 for (unsigned i = 0; i < rects.size(); ++i) {
252 if (alignments[i] & AlignmentRight)
253 rects[i].shiftMaxXEdgeTo(rightEdge);
258 static inline void stitchRects(Vector<LayoutRect>& rects)
260 if (rects.size() <= 1)
263 Vector<LayoutRect> newRects;
265 // FIXME: Need to support vertical layout.
266 // First stitch together all the rects on the first line of the selection.
267 size_t indexFromStart = 0;
268 LayoutUnit firstTop = rects[indexFromStart].y();
269 LayoutRect& currentRect = rects[indexFromStart++];
270 while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
271 currentRect.unite(rects[indexFromStart++]);
273 newRects.append(currentRect);
274 if (indexFromStart == rects.size()) {
275 // All the rects are on one line. There is nothing else to do.
276 rects.swap(newRects);
280 // Next stitch together all the rects on the last line of the selection.
281 size_t indexFromEnd = rects.size() - 1;
282 LayoutUnit lastTop = rects[indexFromEnd].y();
283 LayoutRect lastRect = rects[indexFromEnd];
284 while (indexFromEnd != indexFromStart && rects[--indexFromEnd].y() == lastTop)
285 lastRect.unite(rects[indexFromEnd]);
287 if (indexFromEnd == indexFromStart) {
288 // All the rects are on two lines only. There is nothing else to do.
289 newRects.append(lastRect);
290 rects.swap(newRects);
294 // indexFromStart is the index of the first rectangle on the second line.
295 // indexFromEnd is the index of the last rectangle on the second to the last line.
296 // Stitch together all the rects after the first line until the second to the last included.
297 currentRect = rects[indexFromStart];
298 while (indexFromStart != indexFromEnd)
299 currentRect.unite(rects[++indexFromStart]);
301 newRects.append(currentRect);
302 newRects.append(lastRect);
304 rects.swap(newRects);
307 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
311 // FIXME: The following alignments are correct for LTR text.
312 // We should also account for RTL.
313 uint8_t alignments[3];
314 if (rects.size() == 1) {
315 alignments[0] = AlignmentLeft | AlignmentRight;
316 alignments[1] = AlignmentNone;
317 alignments[2] = AlignmentNone;
318 } else if (rects.size() == 2) {
319 alignments[0] = AlignmentRight;
320 alignments[1] = AlignmentLeft;
321 alignments[2] = AlignmentNone;
323 alignments[0] = AlignmentRight;
324 alignments[1] = AlignmentLeft | AlignmentRight;
325 alignments[2] = AlignmentLeft;
328 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
329 for (auto& gap : gapRects)
330 expandForGap(rects, alignments, gap);
332 // If we have 3 rects we might need one final GapRects to align the edges.
333 if (rects.size() == 3) {
336 for (unsigned i = 0; i < 3; ++i) {
337 if (alignments[i] & AlignmentLeft) {
340 else if (rects[i].x() < left.x())
343 if (alignments[i] & AlignmentRight) {
346 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
351 if (!left.isEmpty() || !right.isEmpty()) {
354 gap.uniteRight(right);
355 expandForGap(rects, alignments, gap);
360 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
362 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
363 m_currentSelectionRects = rects;
364 m_isTextOnly = isTextOnly;
366 m_lastSelectionChangeTime = std::chrono::steady_clock::now();
368 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
370 // DataDetectors needs these reversed in order to place the arrow in the right location.
371 m_currentSelectionRects.reverse();
373 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
375 buildSelectionHighlight();
381 void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
383 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
384 LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
385 buildPhoneNumberHighlights();
387 UNUSED_PARAM(ranges);
391 bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
394 bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
395 mouseIsOverButton = onButton;
399 std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
402 return std::chrono::milliseconds::zero();
404 auto minimumTimeUntilHighlightShouldBeShown = 200_ms;
405 if (m_webPage.corePage()->focusController().focusedOrMainFrame().selection().selection().isContentEditable())
406 minimumTimeUntilHighlightShouldBeShown = 1000_ms;
408 bool mousePressed = false;
409 if (Frame* mainFrame = m_webPage.mainFrame())
410 mousePressed = mainFrame->eventHandler().mousePressed();
412 // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
413 // by virtue of being expanded to include the entire telephone number. However, we will still avoid highlighting
414 // telephone numbers while the mouse is down.
415 if (highlight->type() == Highlight::Type::TelephoneNumber)
416 return mousePressed ? minimumTimeUntilHighlightShouldBeShown : 0_ms;
418 auto now = std::chrono::steady_clock::now();
419 auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
420 auto timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
421 auto timeSinceLastMouseUp = mousePressed ? 0_ms : now - m_lastMouseUpTime;
423 auto remainingDelay = minimumTimeUntilHighlightShouldBeShown - std::min(std::min(timeSinceLastSelectionChange, timeSinceHighlightBecameActive), timeSinceLastMouseUp);
424 return std::chrono::duration_cast<std::chrono::milliseconds>(remainingDelay);
427 void ServicesOverlayController::determineActiveHighlightTimerFired(Timer<ServicesOverlayController>&)
429 bool mouseIsOverButton;
430 determineActiveHighlight(mouseIsOverButton);
433 void ServicesOverlayController::drawRect(PageOverlay* overlay, GraphicsContext& graphicsContext, const IntRect& dirtyRect)
437 void ServicesOverlayController::clearActiveHighlight()
439 if (!m_activeHighlight)
442 if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
443 m_currentMouseDownOnButtonHighlight = nullptr;
444 m_activeHighlight = nullptr;
447 void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
449 Vector<RefPtr<Highlight>> highlightsToRemove;
450 for (auto& highlight : m_potentialHighlights) {
451 if (highlight->type() == type)
452 highlightsToRemove.append(highlight);
455 while (!highlightsToRemove.isEmpty())
456 m_potentialHighlights.remove(highlightsToRemove.takeLast());
459 void ServicesOverlayController::buildPhoneNumberHighlights()
461 HashSet<RefPtr<Highlight>> newPotentialHighlights;
463 Frame* mainFrame = m_webPage.mainFrame();
464 FrameView& mainFrameView = *mainFrame->view();
466 for (Frame* frame = mainFrame; frame; frame = frame->tree().traverseNext()) {
467 auto& ranges = frame->editor().detectedTelephoneNumberRanges();
468 for (auto& range : ranges) {
469 // FIXME: This will choke if the range wraps around the edge of the view.
470 // What should we do in that case?
471 IntRect rect = textQuadsToBoundingRectForRange(*range);
473 // Convert to the main document's coordinate space.
474 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
475 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
476 // not equivalent to what we need when you have a topContentInset or a header banner.
477 FrameView* viewForRange = range->ownerDocument().view();
480 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
482 CGRect cgRect = rect;
483 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
485 newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range));
489 // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
490 // one so that any metadata is retained.
491 HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
493 for (auto& oldHighlight : m_potentialHighlights) {
494 if (oldHighlight->type() != Highlight::Type::TelephoneNumber)
497 for (auto& newHighlight : newPotentialHighlights) {
498 if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
499 reusedPotentialHighlights.add(oldHighlight);
500 newPotentialHighlights.remove(newHighlight);
506 removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
508 m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
509 m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
511 didRebuildPotentialHighlights();
514 void ServicesOverlayController::buildSelectionHighlight()
516 removeAllPotentialHighlightsOfType(Highlight::Type::Selection);
518 Vector<CGRect> cgRects;
519 cgRects.reserveCapacity(m_currentSelectionRects.size());
521 RefPtr<Range> selectionRange = m_webPage.corePage()->selection().firstRange();
522 if (selectionRange) {
523 Frame* mainFrame = m_webPage.mainFrame();
524 FrameView& mainFrameView = *mainFrame->view();
525 FrameView* viewForRange = selectionRange->ownerDocument().view();
527 for (auto& rect : m_currentSelectionRects) {
528 IntRect currentRect = pixelSnappedIntRect(rect);
529 currentRect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(currentRect.location())));
530 cgRects.append((CGRect)currentRect);
533 if (!cgRects.isEmpty()) {
534 CGRect visibleRect = m_webPage.corePage()->mainFrame().view()->visibleContentRect();
535 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
537 m_potentialHighlights.add(Highlight::createForSelection(*this, ddHighlight));
541 didRebuildPotentialHighlights();
544 bool ServicesOverlayController::hasRelevantSelectionServices()
546 return (m_isTextOnly && WebProcess::shared().hasSelectionServices()) || WebProcess::shared().hasRichContentServices();
549 void ServicesOverlayController::didRebuildPotentialHighlights()
551 if (m_potentialHighlights.isEmpty()) {
552 if (m_servicesOverlay)
553 m_webPage.uninstallPageOverlay(m_servicesOverlay);
557 if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
560 createOverlayIfNeeded();
562 bool mouseIsOverButton;
563 determineActiveHighlight(mouseIsOverButton);
566 void ServicesOverlayController::createOverlayIfNeeded()
568 if (m_servicesOverlay)
571 RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
572 m_servicesOverlay = overlay.get();
573 m_webPage.installPageOverlay(overlay.release(), PageOverlay::FadeMode::DoNotFade);
576 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
578 Page* page = m_webPage.corePage();
580 return Vector<RefPtr<Range>>();
582 return page->focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
585 bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
593 if (a->type() == Highlight::Type::TelephoneNumber && b->type() == Highlight::Type::TelephoneNumber && areRangesEqual(a->range(), b->range()))
599 ServicesOverlayController::Highlight* ServicesOverlayController::findTelephoneNumberHighlightContainingSelectionHighlight(Highlight& selectionHighlight)
601 if (selectionHighlight.type() != Highlight::Type::Selection)
604 const VisibleSelection& selection = m_webPage.corePage()->selection();
605 if (!selection.isRange())
608 RefPtr<Range> activeSelectionRange = selection.toNormalizedRange();
609 if (!activeSelectionRange)
612 for (auto& highlight : m_potentialHighlights) {
613 if (highlight->type() != Highlight::Type::TelephoneNumber)
616 if (highlight->range()->contains(*activeSelectionRange))
617 return highlight.get();
623 void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
625 mouseIsOverActiveHighlightButton = false;
627 RefPtr<Highlight> newActiveHighlight;
629 for (auto& highlight : m_potentialHighlights) {
630 if (highlight->type() == Highlight::Type::Selection) {
631 // If we've already found a new active highlight, and it's
632 // a telephone number highlight, prefer that over this selection highlight.
633 if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::TelephoneNumber)
636 // If this highlight has no compatible services, it can't be active.
637 if (!hasRelevantSelectionServices())
641 // If this highlight isn't hovered, it can't be active.
642 bool mouseIsOverButton;
643 if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
646 newActiveHighlight = highlight;
647 mouseIsOverActiveHighlightButton = mouseIsOverButton;
650 // If our new active highlight is a selection highlight that is completely contained
651 // by one of the phone number highlights, we'll make the phone number highlight active even if it's not hovered.
652 if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::Selection) {
653 if (Highlight* containedTelephoneNumberHighlight = findTelephoneNumberHighlightContainingSelectionHighlight(*newActiveHighlight)) {
654 newActiveHighlight = containedTelephoneNumberHighlight;
656 // We will always initially choose the telephone number highlight over the selection highlight if the
657 // mouse is over the telephone number highlight's button, so we know that it's not hovered if we got here.
658 mouseIsOverActiveHighlightButton = false;
662 if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
663 // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
664 // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
665 // highlight, and only reset the active highlight hysteresis when that changes.
666 if (m_nextActiveHighlight != newActiveHighlight) {
667 m_nextActiveHighlight = newActiveHighlight;
668 m_nextActiveHighlightChangeTime = std::chrono::steady_clock::now();
671 m_currentMouseDownOnButtonHighlight = nullptr;
673 if (m_activeHighlight) {
674 m_activeHighlight->fadeOut();
675 m_activeHighlight = nullptr;
678 auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
679 if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
680 m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
684 m_activeHighlight = m_nextActiveHighlight.release();
686 if (m_activeHighlight) {
687 m_servicesOverlay->layer()->addChild(m_activeHighlight->layer());
688 m_activeHighlight->fadeIn();
693 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
695 m_mousePosition = m_webPage.corePage()->mainFrame().view()->rootViewToContents(event.position());
697 bool mouseIsOverActiveHighlightButton = false;
698 determineActiveHighlight(mouseIsOverActiveHighlightButton);
700 // Cancel the potential click if any button other than the left button changes state, and ignore the event.
701 if (event.button() != WebMouseEvent::LeftButton) {
702 m_currentMouseDownOnButtonHighlight = nullptr;
706 // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
707 if (event.type() == WebEvent::MouseUp) {
708 RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
709 m_currentMouseDownOnButtonHighlight = nullptr;
711 m_lastMouseUpTime = std::chrono::steady_clock::now();
713 if (mouseIsOverActiveHighlightButton && mouseDownHighlight) {
714 handleClick(m_mousePosition, *mouseDownHighlight);
721 // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
722 if (event.type() == WebEvent::MouseMove) {
723 if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
726 m_currentMouseDownOnButtonHighlight = nullptr;
730 // If the mouse went down over the active highlight's button, track this as a potential click.
731 if (event.type() == WebEvent::MouseDown) {
732 if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
733 m_currentMouseDownOnButtonHighlight = m_activeHighlight;
743 void ServicesOverlayController::handleClick(const IntPoint& clickPoint, Highlight& highlight)
745 FrameView* frameView = m_webPage.mainFrameView();
749 IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
751 if (highlight.type() == Highlight::Type::Selection) {
752 auto telephoneNumberRanges = telephoneNumberRangesForFocusedFrame();
753 Vector<String> selectedTelephoneNumbers;
754 selectedTelephoneNumbers.reserveCapacity(telephoneNumberRanges.size());
755 for (auto& range : telephoneNumberRanges)
756 selectedTelephoneNumbers.append(range->text());
758 m_webPage.handleSelectionServiceClick(m_webPage.corePage()->focusController().focusedOrMainFrame().selection(), selectedTelephoneNumbers, windowPoint);
759 } else if (highlight.type() == Highlight::Type::TelephoneNumber)
760 m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
763 void ServicesOverlayController::didCreateHighlight(Highlight* highlight)
765 ASSERT(!m_highlights.contains(highlight));
766 m_highlights.add(highlight);
769 void ServicesOverlayController::willDestroyHighlight(Highlight* highlight)
771 ASSERT(m_highlights.contains(highlight));
772 m_highlights.remove(highlight);
775 } // namespace WebKit
777 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)