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