| /* |
| * Copyright (C) 2014 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "ServicesOverlayController.h" |
| |
| #if (ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION)) && PLATFORM(MAC) |
| |
| #import "Chrome.h" |
| #import "ChromeClient.h" |
| #import "Document.h" |
| #import "Editor.h" |
| #import "EventHandler.h" |
| #import "FloatQuad.h" |
| #import "FocusController.h" |
| #import "Frame.h" |
| #import "FrameSelection.h" |
| #import "FrameView.h" |
| #import "GapRects.h" |
| #import "GraphicsContext.h" |
| #import "GraphicsLayer.h" |
| #import "GraphicsLayerCA.h" |
| #import "Logging.h" |
| #import "Page.h" |
| #import "PageOverlayController.h" |
| #import "Settings.h" |
| #import "TextIterator.h" |
| #import <QuartzCore/QuartzCore.h> |
| #import <pal/mac/DataDetectorsSoftLink.h> |
| |
| namespace WebCore { |
| |
| ServicesOverlayController::ServicesOverlayController(Page& page) |
| : m_page(page) |
| , m_determineActiveHighlightTimer(*this, &ServicesOverlayController::determineActiveHighlightTimerFired) |
| , m_buildHighlightsTimer(*this, &ServicesOverlayController::buildPotentialHighlightsIfNeeded) |
| { |
| } |
| |
| ServicesOverlayController::~ServicesOverlayController() |
| { |
| for (auto& highlight : m_highlights) |
| highlight.invalidate(); |
| } |
| |
| void ServicesOverlayController::willMoveToPage(PageOverlay&, Page* page) |
| { |
| if (page) |
| return; |
| |
| ASSERT(m_servicesOverlay); |
| m_servicesOverlay = nullptr; |
| } |
| |
| void ServicesOverlayController::didMoveToPage(PageOverlay&, Page*) |
| { |
| } |
| |
| static const uint8_t AlignmentNone = 0; |
| static const uint8_t AlignmentLeft = 1 << 0; |
| static const uint8_t AlignmentRight = 1 << 1; |
| |
| static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap) |
| { |
| if (!gap.left().isEmpty()) { |
| LayoutUnit leftEdge = gap.left().x(); |
| for (unsigned i = 0; i < rects.size(); ++i) { |
| if (alignments[i] & AlignmentLeft) |
| rects[i].shiftXEdgeTo(leftEdge); |
| } |
| } |
| |
| if (!gap.right().isEmpty()) { |
| LayoutUnit rightEdge = gap.right().maxX(); |
| for (unsigned i = 0; i < rects.size(); ++i) { |
| if (alignments[i] & AlignmentRight) |
| rects[i].shiftMaxXEdgeTo(rightEdge); |
| } |
| } |
| } |
| |
| static inline void stitchRects(Vector<LayoutRect>& rects) |
| { |
| if (rects.size() <= 1) |
| return; |
| |
| Vector<LayoutRect> newRects; |
| |
| // FIXME: Need to support vertical layout. |
| // First stitch together all the rects on the first line of the selection. |
| size_t indexFromStart = 0; |
| LayoutUnit firstTop = rects[indexFromStart].y(); |
| LayoutRect& currentRect = rects[indexFromStart]; |
| while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop) |
| currentRect.unite(rects[indexFromStart++]); |
| |
| newRects.append(currentRect); |
| if (indexFromStart == rects.size()) { |
| // All the rects are on one line. There is nothing else to do. |
| rects.swap(newRects); |
| return; |
| } |
| |
| // Next stitch together all the rects on the last line of the selection. |
| size_t indexFromEnd = rects.size() - 1; |
| LayoutUnit lastTop = rects[indexFromEnd].y(); |
| LayoutRect lastRect = rects[indexFromEnd]; |
| while (indexFromEnd >= indexFromStart && rects[indexFromEnd].y() == lastTop) |
| lastRect.unite(rects[indexFromEnd--]); |
| |
| // indexFromStart is the index of the first rectangle on the second line. |
| // indexFromEnd is the index of the last rectangle on the second to the last line. |
| // if they are equal, there is one additional rectangle for the line in the middle. |
| if (indexFromEnd == indexFromStart) |
| newRects.append(rects[indexFromStart]); |
| |
| if (indexFromEnd <= indexFromStart) { |
| // There are no more rects to stitch. Just append the last line. |
| newRects.append(lastRect); |
| rects.swap(newRects); |
| return; |
| } |
| |
| // Stitch together all the rects after the first line until the second to the last included. |
| currentRect = rects[indexFromStart]; |
| while (indexFromStart != indexFromEnd) |
| currentRect.unite(rects[++indexFromStart]); |
| |
| newRects.append(currentRect); |
| newRects.append(lastRect); |
| |
| rects.swap(newRects); |
| } |
| |
| static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects) |
| { |
| stitchRects(rects); |
| |
| // FIXME: The following alignments are correct for LTR text. |
| // We should also account for RTL. |
| uint8_t alignments[3]; |
| if (rects.size() == 1) { |
| alignments[0] = AlignmentLeft | AlignmentRight; |
| alignments[1] = AlignmentNone; |
| alignments[2] = AlignmentNone; |
| } else if (rects.size() == 2) { |
| alignments[0] = AlignmentRight; |
| alignments[1] = AlignmentLeft; |
| alignments[2] = AlignmentNone; |
| } else { |
| alignments[0] = AlignmentRight; |
| alignments[1] = AlignmentLeft | AlignmentRight; |
| alignments[2] = AlignmentLeft; |
| } |
| |
| // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap. |
| for (auto& gap : gapRects) |
| expandForGap(rects, alignments, gap); |
| |
| // If we have 3 rects we might need one final GapRects to align the edges. |
| if (rects.size() == 3) { |
| LayoutRect left; |
| LayoutRect right; |
| for (unsigned i = 0; i < 3; ++i) { |
| if (alignments[i] & AlignmentLeft) { |
| if (left.isEmpty()) |
| left = rects[i]; |
| else if (rects[i].x() < left.x()) |
| left = rects[i]; |
| } |
| if (alignments[i] & AlignmentRight) { |
| if (right.isEmpty()) |
| right = rects[i]; |
| else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width())) |
| right = rects[i]; |
| } |
| } |
| |
| if (!left.isEmpty() || !right.isEmpty()) { |
| GapRects gap; |
| gap.uniteLeft(left); |
| gap.uniteRight(right); |
| expandForGap(rects, alignments, gap); |
| } |
| } |
| } |
| |
| void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly) |
| { |
| m_currentSelectionRects = rects; |
| m_isTextOnly = isTextOnly; |
| |
| m_lastSelectionChangeTime = MonotonicTime::now(); |
| |
| compactRectsWithGapRects(m_currentSelectionRects, gapRects); |
| |
| // DataDetectors needs these reversed in order to place the arrow in the right location. |
| m_currentSelectionRects.reverse(); |
| |
| LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size()); |
| invalidateHighlightsOfType(DataDetectorHighlight::Type::Selection); |
| } |
| |
| void ServicesOverlayController::selectedTelephoneNumberRangesChanged() |
| { |
| LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n"); |
| invalidateHighlightsOfType(DataDetectorHighlight::Type::TelephoneNumber); |
| } |
| |
| void ServicesOverlayController::invalidateHighlightsOfType(DataDetectorHighlight::Type type) |
| { |
| if (!m_page.settings().serviceControlsEnabled()) |
| return; |
| |
| m_dirtyHighlightTypes.add(type); |
| m_buildHighlightsTimer.startOneShot(0_s); |
| } |
| |
| void ServicesOverlayController::buildPotentialHighlightsIfNeeded() |
| { |
| if (m_dirtyHighlightTypes.isEmpty()) |
| return; |
| |
| if (m_dirtyHighlightTypes.contains(DataDetectorHighlight::Type::TelephoneNumber)) |
| buildPhoneNumberHighlights(); |
| |
| if (m_dirtyHighlightTypes.contains(DataDetectorHighlight::Type::Selection)) |
| buildSelectionHighlight(); |
| |
| m_dirtyHighlightTypes = { }; |
| |
| if (m_potentialHighlights.isEmpty()) { |
| if (m_servicesOverlay) |
| m_page.pageOverlayController().uninstallPageOverlay(*m_servicesOverlay, PageOverlay::FadeMode::DoNotFade); |
| return; |
| } |
| |
| if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices()) |
| return; |
| |
| createOverlayIfNeeded(); |
| |
| bool mouseIsOverButton; |
| determineActiveHighlight(mouseIsOverButton); |
| } |
| |
| bool ServicesOverlayController::mouseIsOverHighlight(DataDetectorHighlight& highlight, bool& mouseIsOverButton) const |
| { |
| if (!PAL::isDataDetectorsFrameworkAvailable()) |
| return false; |
| |
| Boolean onButton; |
| bool hovered = PAL::softLink_DataDetectors_DDHighlightPointIsOnHighlight(highlight.highlight(), (CGPoint)m_mousePosition, &onButton); |
| mouseIsOverButton = onButton; |
| return hovered; |
| } |
| |
| Seconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(DataDetectorHighlight* highlight) const |
| { |
| if (!highlight) |
| return 0_s; |
| |
| Seconds minimumTimeUntilHighlightShouldBeShown = 200_ms; |
| if (CheckedRef(m_page.focusController())->focusedOrMainFrame().selection().selection().isContentEditable()) |
| minimumTimeUntilHighlightShouldBeShown = 1_s; |
| |
| bool mousePressed = mainFrame().eventHandler().mousePressed(); |
| |
| // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable |
| // by virtue of being expanded to include the entire telephone number. However, we will still avoid highlighting |
| // telephone numbers while the mouse is down. |
| if (highlight->type() == DataDetectorHighlight::Type::TelephoneNumber) |
| return mousePressed ? minimumTimeUntilHighlightShouldBeShown : 0_s; |
| |
| MonotonicTime now = MonotonicTime::now(); |
| Seconds timeSinceLastSelectionChange = now - m_lastSelectionChangeTime; |
| Seconds timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime; |
| Seconds timeSinceLastMouseUp = mousePressed ? 0_s : now - m_lastMouseUpTime; |
| |
| return minimumTimeUntilHighlightShouldBeShown - std::min(std::min(timeSinceLastSelectionChange, timeSinceHighlightBecameActive), timeSinceLastMouseUp); |
| } |
| |
| void ServicesOverlayController::determineActiveHighlightTimerFired() |
| { |
| bool mouseIsOverButton; |
| determineActiveHighlight(mouseIsOverButton); |
| } |
| |
| void ServicesOverlayController::drawRect(PageOverlay&, GraphicsContext&, const IntRect&) |
| { |
| } |
| |
| void ServicesOverlayController::clearActiveHighlight() |
| { |
| if (!m_activeHighlight) |
| return; |
| |
| if (m_currentMouseDownOnButtonHighlight == m_activeHighlight) |
| m_currentMouseDownOnButtonHighlight = nullptr; |
| m_activeHighlight = nullptr; |
| } |
| |
| void ServicesOverlayController::removeAllPotentialHighlightsOfType(DataDetectorHighlight::Type type) |
| { |
| Vector<RefPtr<DataDetectorHighlight>> highlightsToRemove; |
| for (auto& highlight : m_potentialHighlights) { |
| if (highlight->type() == type) |
| highlightsToRemove.append(highlight); |
| } |
| |
| while (!highlightsToRemove.isEmpty()) |
| m_potentialHighlights.remove(highlightsToRemove.takeLast()); |
| } |
| |
| void ServicesOverlayController::buildPhoneNumberHighlights() |
| { |
| Vector<SimpleRange> phoneNumberRanges; |
| for (Frame* frame = &mainFrame(); frame; frame = frame->tree().traverseNext()) |
| phoneNumberRanges.appendVector(frame->editor().detectedTelephoneNumberRanges()); |
| |
| if (phoneNumberRanges.isEmpty()) { |
| removeAllPotentialHighlightsOfType(DataDetectorHighlight::Type::TelephoneNumber); |
| return; |
| } |
| |
| if (!PAL::isDataDetectorsFrameworkAvailable()) |
| return; |
| |
| HashSet<RefPtr<DataDetectorHighlight>> newPotentialHighlights; |
| |
| FrameView& mainFrameView = *mainFrame().view(); |
| |
| for (auto& range : phoneNumberRanges) { |
| // FIXME: This makes a big rect if the range extends from the end of one line to the start of the next. Handle that case better? |
| auto rect = enclosingIntRect(unitedBoundingBoxes(RenderObject::absoluteTextQuads(range))); |
| |
| // Convert to the main document's coordinate space. |
| // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space. |
| // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is |
| // not equivalent to what we need when you have a topContentInset or a header banner. |
| auto* viewForRange = range.start.document().view(); |
| if (!viewForRange) |
| continue; |
| |
| rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location()))); |
| |
| CGRect cgRect = rect; |
| #if HAVE(DD_HIGHLIGHT_CREATE_WITH_SCALE) |
| auto ddHighlight = adoptCF(PAL::softLink_DataDetectors_DDHighlightCreateWithRectsInVisibleRectWithStyleScaleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), static_cast<DDHighlightStyle>(DDHighlightStyleBubbleStandard) | static_cast<DDHighlightStyle>(DDHighlightStyleStandardIconArrow), YES, NSWritingDirectionNatural, NO, YES, 0)); |
| #else |
| auto ddHighlight = adoptCF(PAL::softLink_DataDetectors_DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), static_cast<DDHighlightStyle>(DDHighlightStyleBubbleStandard) | static_cast<DDHighlightStyle>(DDHighlightStyleStandardIconArrow), YES, NSWritingDirectionNatural, NO, YES)); |
| #endif |
| auto highlight = DataDetectorHighlight::createForTelephoneNumber(m_page, *this, WTFMove(ddHighlight), WTFMove(range)); |
| m_highlights.add(highlight.get()); |
| newPotentialHighlights.add(WTFMove(highlight)); |
| } |
| |
| replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, DataDetectorHighlight::Type::TelephoneNumber); |
| } |
| |
| void ServicesOverlayController::buildSelectionHighlight() |
| { |
| if (m_currentSelectionRects.isEmpty()) { |
| removeAllPotentialHighlightsOfType(DataDetectorHighlight::Type::Selection); |
| return; |
| } |
| |
| if (!PAL::isDataDetectorsFrameworkAvailable()) |
| return; |
| |
| HashSet<RefPtr<DataDetectorHighlight>> newPotentialHighlights; |
| |
| if (auto selectionRange = m_page.selection().firstRange()) { |
| FrameView* mainFrameView = mainFrame().view(); |
| if (!mainFrameView) |
| return; |
| |
| RefPtr viewForRange = selectionRange->start.document().view(); |
| if (!viewForRange) |
| return; |
| |
| Vector<CGRect> cgRects; |
| cgRects.reserveInitialCapacity(m_currentSelectionRects.size()); |
| |
| for (auto& rect : m_currentSelectionRects) { |
| IntRect currentRect = snappedIntRect(rect); |
| currentRect.setLocation(mainFrameView->windowToContents(viewForRange->contentsToWindow(currentRect.location()))); |
| cgRects.uncheckedAppend(currentRect); |
| } |
| |
| if (!cgRects.isEmpty()) { |
| CGRect visibleRect = mainFrameView->visibleContentRect(); |
| #if HAVE(DD_HIGHLIGHT_CREATE_WITH_SCALE) |
| auto ddHighlight = adoptCF(PAL::softLink_DataDetectors_DDHighlightCreateWithRectsInVisibleRectWithStyleScaleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, static_cast<DDHighlightStyle>(DDHighlightStyleBubbleNone) | static_cast<DDHighlightStyle>(DDHighlightStyleStandardIconArrow) | static_cast<DDHighlightStyle>(DDHighlightStyleButtonShowAlways), YES, NSWritingDirectionNatural, NO, YES, 0)); |
| #else |
| auto ddHighlight = adoptCF(PAL::softLink_DataDetectors_DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, static_cast<DDHighlightStyle>(DDHighlightStyleBubbleNone) | static_cast<DDHighlightStyle>(DDHighlightStyleStandardIconArrow) | static_cast<DDHighlightStyle>(DDHighlightStyleButtonShowAlways), YES, NSWritingDirectionNatural, NO, YES)); |
| #endif |
| auto highlight = DataDetectorHighlight::createForSelection(m_page, *this, WTFMove(ddHighlight), WTFMove(*selectionRange)); |
| m_highlights.add(highlight.get()); |
| newPotentialHighlights.add(WTFMove(highlight)); |
| } |
| } |
| |
| replaceHighlightsOfTypePreservingEquivalentHighlights(newPotentialHighlights, DataDetectorHighlight::Type::Selection); |
| } |
| |
| void ServicesOverlayController::replaceHighlightsOfTypePreservingEquivalentHighlights(HashSet<RefPtr<DataDetectorHighlight>>& newPotentialHighlights, DataDetectorHighlight::Type type) |
| { |
| // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old |
| // one so that any metadata is retained. |
| HashSet<RefPtr<DataDetectorHighlight>> reusedPotentialHighlights; |
| |
| for (auto& oldHighlight : m_potentialHighlights) { |
| if (oldHighlight->type() != type) |
| continue; |
| |
| for (auto& newHighlight : newPotentialHighlights) { |
| if (areEquivalent(oldHighlight.get(), newHighlight.get())) { |
| oldHighlight->setHighlight(newHighlight->highlight()); |
| |
| reusedPotentialHighlights.add(oldHighlight); |
| newPotentialHighlights.remove(newHighlight); |
| |
| break; |
| } |
| } |
| } |
| |
| removeAllPotentialHighlightsOfType(type); |
| |
| m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end()); |
| m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end()); |
| } |
| |
| bool ServicesOverlayController::hasRelevantSelectionServices() |
| { |
| return m_page.chrome().client().hasRelevantSelectionServices(m_isTextOnly); |
| } |
| |
| void ServicesOverlayController::createOverlayIfNeeded() |
| { |
| if (m_servicesOverlay) |
| return; |
| |
| if (!m_page.settings().serviceControlsEnabled()) |
| return; |
| |
| auto overlay = PageOverlay::create(*this, PageOverlay::OverlayType::Document); |
| m_servicesOverlay = overlay.ptr(); |
| m_page.pageOverlayController().installPageOverlay(WTFMove(overlay), PageOverlay::FadeMode::DoNotFade); |
| } |
| |
| Vector<SimpleRange> ServicesOverlayController::telephoneNumberRangesForFocusedFrame() |
| { |
| return CheckedRef(m_page.focusController())->focusedOrMainFrame().editor().detectedTelephoneNumberRanges(); |
| } |
| |
| DataDetectorHighlight* ServicesOverlayController::findTelephoneNumberHighlightContainingSelectionHighlight(DataDetectorHighlight& selectionHighlight) |
| { |
| if (selectionHighlight.type() != DataDetectorHighlight::Type::Selection) |
| return nullptr; |
| |
| auto selectionRange = m_page.selection().toNormalizedRange(); |
| if (!selectionRange) |
| return nullptr; |
| |
| for (auto& highlight : m_potentialHighlights) { |
| if (highlight->type() == DataDetectorHighlight::Type::TelephoneNumber && contains<ComposedTree>(highlight->range(), *selectionRange)) |
| return highlight.get(); |
| } |
| |
| return nullptr; |
| } |
| |
| void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton) |
| { |
| buildPotentialHighlightsIfNeeded(); |
| |
| mouseIsOverActiveHighlightButton = false; |
| |
| RefPtr<DataDetectorHighlight> newActiveHighlight; |
| |
| for (auto& highlight : m_potentialHighlights) { |
| if (highlight->type() == DataDetectorHighlight::Type::Selection) { |
| // If we've already found a new active highlight, and it's |
| // a telephone number highlight, prefer that over this selection highlight. |
| if (newActiveHighlight && newActiveHighlight->type() == DataDetectorHighlight::Type::TelephoneNumber) |
| continue; |
| |
| // If this highlight has no compatible services, it can't be active. |
| if (!hasRelevantSelectionServices()) |
| continue; |
| } |
| |
| // If this highlight isn't hovered, it can't be active. |
| bool mouseIsOverButton; |
| if (!mouseIsOverHighlight(*highlight, mouseIsOverButton)) |
| continue; |
| |
| newActiveHighlight = highlight; |
| mouseIsOverActiveHighlightButton = mouseIsOverButton; |
| } |
| |
| // If our new active highlight is a selection highlight that is completely contained |
| // by one of the phone number highlights, we'll make the phone number highlight active even if it's not hovered. |
| if (newActiveHighlight && newActiveHighlight->type() == DataDetectorHighlight::Type::Selection) { |
| if (DataDetectorHighlight* containedTelephoneNumberHighlight = findTelephoneNumberHighlightContainingSelectionHighlight(*newActiveHighlight)) { |
| newActiveHighlight = containedTelephoneNumberHighlight; |
| |
| // We will always initially choose the telephone number highlight over the selection highlight if the |
| // mouse is over the telephone number highlight's button, so we know that it's not hovered if we got here. |
| mouseIsOverActiveHighlightButton = false; |
| } |
| } |
| |
| if (!areEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) { |
| // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times |
| // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active |
| // highlight, and only reset the active highlight hysteresis when that changes. |
| if (m_nextActiveHighlight != newActiveHighlight) { |
| m_nextActiveHighlight = newActiveHighlight; |
| m_nextActiveHighlightChangeTime = MonotonicTime::now(); |
| } |
| |
| m_currentMouseDownOnButtonHighlight = nullptr; |
| |
| if (m_activeHighlight) { |
| m_activeHighlight->fadeOut(); |
| m_activeHighlight = nullptr; |
| } |
| |
| auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get()); |
| if (remainingTimeUntilHighlightShouldBeShown > 0_s) { |
| m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown); |
| return; |
| } |
| |
| m_activeHighlight = WTFMove(m_nextActiveHighlight); |
| |
| if (m_activeHighlight) { |
| Ref<GraphicsLayer> highlightLayer = m_activeHighlight->layer(); |
| m_servicesOverlay->layer().addChild(WTFMove(highlightLayer)); |
| m_activeHighlight->fadeIn(); |
| } |
| } |
| } |
| |
| bool ServicesOverlayController::mouseEvent(PageOverlay&, const PlatformMouseEvent& event) |
| { |
| m_mousePosition = mainFrame().view()->windowToContents(event.position()); |
| |
| bool mouseIsOverActiveHighlightButton = false; |
| determineActiveHighlight(mouseIsOverActiveHighlightButton); |
| |
| // Cancel the potential click if any button other than the left button changes state, and ignore the event. |
| if (event.button() != MouseButton::LeftButton) { |
| m_currentMouseDownOnButtonHighlight = nullptr; |
| return false; |
| } |
| |
| // If the mouse lifted while still over the highlight button that it went down on, then that is a click. |
| if (event.type() == PlatformEvent::MouseReleased) { |
| auto mouseDownHighlight = m_currentMouseDownOnButtonHighlight.copyRef(); |
| m_currentMouseDownOnButtonHighlight = nullptr; |
| |
| m_lastMouseUpTime = MonotonicTime::now(); |
| |
| if (mouseIsOverActiveHighlightButton && mouseDownHighlight) { |
| handleClick(m_mousePosition, *mouseDownHighlight); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // If the mouse moved outside of the button tracking a potential click, stop tracking the click. |
| if (event.type() == PlatformEvent::MouseMoved) { |
| if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton) |
| return true; |
| |
| m_currentMouseDownOnButtonHighlight = nullptr; |
| return false; |
| } |
| |
| // If the mouse went down over the active highlight's button, track this as a potential click. |
| if (event.type() == PlatformEvent::MousePressed) { |
| if (m_activeHighlight && mouseIsOverActiveHighlightButton) { |
| m_currentMouseDownOnButtonHighlight = m_activeHighlight; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| return false; |
| } |
| |
| void ServicesOverlayController::didScrollFrame(PageOverlay&, Frame& frame) |
| { |
| if (frame.isMainFrame()) |
| return; |
| |
| invalidateHighlightsOfType(DataDetectorHighlight::Type::TelephoneNumber); |
| invalidateHighlightsOfType(DataDetectorHighlight::Type::Selection); |
| buildPotentialHighlightsIfNeeded(); |
| |
| bool mouseIsOverActiveHighlightButton; |
| determineActiveHighlight(mouseIsOverActiveHighlightButton); |
| } |
| |
| void ServicesOverlayController::handleClick(const IntPoint& clickPoint, DataDetectorHighlight& highlight) |
| { |
| FrameView* frameView = mainFrame().view(); |
| if (!frameView) |
| return; |
| |
| IntPoint windowPoint = frameView->contentsToWindow(clickPoint); |
| |
| if (highlight.type() == DataDetectorHighlight::Type::Selection) { |
| auto selectedTelephoneNumbers = telephoneNumberRangesForFocusedFrame().map([](auto& range) { |
| return plainText(range); |
| }); |
| |
| m_page.chrome().client().handleSelectionServiceClick(CheckedRef(m_page.focusController())->focusedOrMainFrame().selection(), WTFMove(selectedTelephoneNumbers), windowPoint); |
| } else if (highlight.type() == DataDetectorHighlight::Type::TelephoneNumber) |
| m_page.chrome().client().handleTelephoneNumberClick(plainText(highlight.range()), windowPoint, frameView->contentsToWindow(CheckedRef(m_page.focusController())->focusedOrMainFrame().editor().firstRectForRange(highlight.range()))); |
| } |
| |
| Frame& ServicesOverlayController::mainFrame() const |
| { |
| return m_page.mainFrame(); |
| } |
| |
| } // namespace WebKit |
| |
| #endif // (ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION)) && PLATFORM(MAC) |