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 <WebCore/Document.h>
35 #import <WebCore/FloatQuad.h>
36 #import <WebCore/FocusController.h>
37 #import <WebCore/FrameView.h>
38 #import <WebCore/GapRects.h>
39 #import <WebCore/GraphicsContext.h>
40 #import <WebCore/MainFrame.h>
41 #import <WebCore/SoftLinking.h>
43 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
44 #import <DataDetectors/DDHighlightDrawing.h>
46 typedef void* DDHighlightRef;
49 #if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
50 #import <DataDetectors/DDHighlightDrawing_Private.h>
53 typedef NSUInteger DDHighlightStyle;
54 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
55 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
57 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
58 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))
59 SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
60 SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
61 SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
63 using namespace WebCore;
67 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(RetainPtr<DDHighlightRef> ddHighlight)
69 return adoptRef(new Highlight(Type::Selection, ddHighlight, nullptr));
72 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
74 return adoptRef(new Highlight(Type::TelephoneNumber, ddHighlight, range));
77 static IntRect textQuadsToBoundingRectForRange(Range& range)
79 Vector<FloatQuad> textQuads;
80 range.textQuads(textQuads);
81 FloatRect boundingRect;
82 for (auto& quad : textQuads)
83 boundingRect.unite(quad.boundingBox());
84 return enclosingIntRect(boundingRect);
87 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
89 , m_servicesOverlay(nullptr)
91 , m_repaintHighlightTimer(this, &ServicesOverlayController::repaintHighlightTimerFired)
95 ServicesOverlayController::~ServicesOverlayController()
99 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
101 // Before the overlay is destroyed, it should have moved out of the WebPage,
102 // at which point we already cleared our back pointer.
103 ASSERT(!m_servicesOverlay);
106 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
111 ASSERT(m_servicesOverlay);
112 m_servicesOverlay = nullptr;
115 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
119 static const uint8_t AlignmentNone = 0;
120 static const uint8_t AlignmentLeft = 1 << 0;
121 static const uint8_t AlignmentRight = 1 << 1;
123 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
125 if (!gap.left().isEmpty()) {
126 LayoutUnit leftEdge = gap.left().x();
127 for (unsigned i = 0; i < 3; ++i) {
128 if (alignments[i] & AlignmentLeft)
129 rects[i].shiftXEdgeTo(leftEdge);
133 if (!gap.right().isEmpty()) {
134 LayoutUnit rightEdge = gap.right().maxX();
135 for (unsigned i = 0; i < 3; ++i) {
136 if (alignments[i] & AlignmentRight)
137 rects[i].shiftMaxXEdgeTo(rightEdge);
142 static inline void stitchRects(Vector<LayoutRect>& rects)
144 if (rects.size() <= 1)
147 Vector<LayoutRect> newRects;
149 // FIXME: Need to support vertical layout.
150 // First stitch together all the rects on the first line of the selection.
151 size_t indexFromStart = 0;
152 LayoutUnit firstTop = rects[indexFromStart].y();
153 LayoutRect& currentRect = rects[indexFromStart++];
154 while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
155 currentRect.unite(rects[indexFromStart++]);
157 newRects.append(currentRect);
158 if (indexFromStart == rects.size()) {
159 // All the rects are on one line. There is nothing else to do.
160 rects.swap(newRects);
164 // Next stitch together all the rects on the last line of the selection.
165 size_t indexFromEnd = rects.size() - 1;
166 LayoutUnit lastTop = rects[indexFromEnd].y();
167 LayoutRect lastRect = rects[indexFromEnd];
168 while (indexFromEnd != indexFromStart && rects[--indexFromEnd].y() == lastTop)
169 lastRect.unite(rects[indexFromEnd]);
171 if (indexFromEnd == indexFromStart) {
172 // All the rects are on two lines only. There is nothing else to do.
173 newRects.append(lastRect);
174 rects.swap(newRects);
178 // indexFromStart is the index of the first rectangle on the second line.
179 // indexFromEnd is the index of the last rectangle on the second to the last line.
180 // Stitch together all the rects after the first line until the second to the last included.
181 currentRect = rects[indexFromStart];
182 while (indexFromStart != indexFromEnd)
183 currentRect.unite(rects[++indexFromStart]);
185 newRects.append(currentRect);
186 newRects.append(lastRect);
188 rects.swap(newRects);
191 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
195 // FIXME: The following alignments are correct for LTR text.
196 // We should also account for RTL.
197 uint8_t alignments[3];
198 if (rects.size() == 1) {
199 alignments[0] = AlignmentLeft | AlignmentRight;
200 alignments[1] = AlignmentNone;
201 alignments[2] = AlignmentNone;
202 } else if (rects.size() == 2) {
203 alignments[0] = AlignmentRight;
204 alignments[1] = AlignmentLeft;
205 alignments[2] = AlignmentNone;
207 alignments[0] = AlignmentRight;
208 alignments[1] = AlignmentLeft | AlignmentRight;
209 alignments[2] = AlignmentLeft;
212 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
213 for (auto& gap : gapRects)
214 expandForGap(rects, alignments, gap);
216 // If we have 3 rects we might need one final GapRects to align the edges.
217 if (rects.size() == 3) {
220 for (unsigned i = 0; i < 3; ++i) {
221 if (alignments[i] & AlignmentLeft) {
224 else if (rects[i].x() < left.x())
227 if (alignments[i] & AlignmentRight) {
230 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
235 if (!left.isEmpty() || !right.isEmpty()) {
238 gap.uniteRight(right);
239 expandForGap(rects, alignments, gap);
244 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
246 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
247 m_currentSelectionRects = rects;
248 m_isTextOnly = isTextOnly;
250 m_lastSelectionChangeTime = std::chrono::steady_clock::now();
252 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
254 // DataDetectors needs these reversed in order to place the arrow in the right location.
255 m_currentSelectionRects.reverse();
257 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
259 buildSelectionHighlight();
265 void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
267 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
268 LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
269 buildPhoneNumberHighlights();
271 UNUSED_PARAM(ranges);
275 bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
278 bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
279 mouseIsOverButton = onButton;
283 std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown() const
285 // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
286 // by virtue of being expanded to include the entire telephone number.
287 if (m_activeHighlight->type() == Highlight::Type::TelephoneNumber)
288 return std::chrono::milliseconds::zero();
290 std::chrono::steady_clock::duration minimumTimeUntilHighlightShouldBeShown = 200_ms;
292 auto now = std::chrono::steady_clock::now();
293 auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
294 auto timeSinceHighlightBecameActive = now - m_lastActiveHighlightChangeTime;
296 return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceHighlightBecameActive));
299 void ServicesOverlayController::repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&)
301 if (m_servicesOverlay)
302 m_servicesOverlay->setNeedsDisplay();
305 void ServicesOverlayController::drawHighlight(Highlight& highlight, WebCore::GraphicsContext& graphicsContext)
307 bool mouseIsOverButton;
308 if (!mouseIsOverHighlight(highlight, mouseIsOverButton)) {
309 LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
313 auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown();
314 if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
315 m_repaintHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
319 CGContextRef cgContext = graphicsContext.platformContext();
321 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight.ddHighlight(), cgContext);
322 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight.ddHighlight());
324 CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
327 void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
329 bool mouseIsOverButton;
330 determineActiveHighlight(mouseIsOverButton);
332 if (m_activeHighlight)
333 drawHighlight(*m_activeHighlight, graphicsContext);
336 void ServicesOverlayController::clearActiveHighlight()
338 if (!m_activeHighlight)
341 if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
342 m_currentMouseDownOnButtonHighlight = nullptr;
343 m_activeHighlight = nullptr;
346 void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
348 Vector<RefPtr<Highlight>> highlightsToRemove;
349 for (auto& highlight : m_potentialHighlights) {
350 if (highlight->type() == type)
351 highlightsToRemove.append(highlight);
354 while (!highlightsToRemove.isEmpty())
355 m_potentialHighlights.remove(highlightsToRemove.takeLast());
358 void ServicesOverlayController::buildPhoneNumberHighlights()
360 removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
362 Frame* mainFrame = m_webPage.mainFrame();
363 FrameView& mainFrameView = *mainFrame->view();
365 for (Frame* frame = mainFrame; frame; frame = frame->tree().traverseNext()) {
366 auto& ranges = frame->editor().detectedTelephoneNumberRanges();
367 for (auto& range : ranges) {
368 // FIXME: This will choke if the range wraps around the edge of the view.
369 // What should we do in that case?
370 IntRect rect = textQuadsToBoundingRectForRange(*range);
372 // Convert to the main document's coordinate space.
373 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
374 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
375 // not equivalent to what we need when you have a topContentInset or a header banner.
376 FrameView* viewForRange = range->ownerDocument().view();
379 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
381 CGRect cgRect = rect;
382 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
384 m_potentialHighlights.add(Highlight::createForTelephoneNumber(ddHighlight, range));
388 didRebuildPotentialHighlights();
391 void ServicesOverlayController::buildSelectionHighlight()
393 removeAllPotentialHighlightsOfType(Highlight::Type::Selection);
395 Vector<CGRect> cgRects;
396 cgRects.reserveCapacity(m_currentSelectionRects.size());
398 for (auto& rect : m_currentSelectionRects)
399 cgRects.append((CGRect)pixelSnappedIntRect(rect));
401 if (!cgRects.isEmpty()) {
402 CGRect visibleRect = m_webPage.corePage()->mainFrame().view()->visibleContentRect();
403 RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
405 m_potentialHighlights.add(Highlight::createForSelection(ddHighlight));
408 didRebuildPotentialHighlights();
411 bool ServicesOverlayController::hasRelevantSelectionServices()
413 return (m_isTextOnly && WebProcess::shared().hasSelectionServices()) || WebProcess::shared().hasRichContentServices();
416 void ServicesOverlayController::didRebuildPotentialHighlights()
418 if (m_potentialHighlights.isEmpty()) {
419 if (m_servicesOverlay)
420 m_webPage.uninstallPageOverlay(m_servicesOverlay);
424 if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
427 createOverlayIfNeeded();
430 void ServicesOverlayController::createOverlayIfNeeded()
432 if (m_servicesOverlay) {
433 m_servicesOverlay->setNeedsDisplay();
437 RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
438 m_servicesOverlay = overlay.get();
439 m_webPage.installPageOverlay(overlay.release(), PageOverlay::FadeMode::DoNotFade);
440 m_servicesOverlay->setNeedsDisplay();
443 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
445 Page* page = m_webPage.corePage();
447 return Vector<RefPtr<Range>>();
449 return page->focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
452 bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
460 if (a->type() == Highlight::Type::TelephoneNumber && b->type() == Highlight::Type::TelephoneNumber && areRangesEqual(a->range(), b->range()))
466 void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
468 mouseIsOverActiveHighlightButton = false;
470 RefPtr<Highlight> oldActiveHighlight = m_activeHighlight.release();
472 for (auto& highlight : m_potentialHighlights) {
473 if (highlight->type() == Highlight::Type::Selection) {
474 // If we've already found a new active highlight, and it's
475 // a telephone number highlight, prefer that over this selection highlight.
476 if (m_activeHighlight && m_activeHighlight->type() == Highlight::Type::TelephoneNumber)
479 // If this highlight has no compatible services, it can't be active, unless we have telephone number highlights to show in the combined menu.
480 if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
484 // If this highlight isn't hovered, it can't be active.
485 bool mouseIsOverButton;
486 if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
489 m_activeHighlight = highlight;
490 mouseIsOverActiveHighlightButton = mouseIsOverButton;
493 if (!highlightsAreEquivalent(oldActiveHighlight.get(), m_activeHighlight.get())) {
494 m_lastActiveHighlightChangeTime = std::chrono::steady_clock::now();
495 m_servicesOverlay->setNeedsDisplay();
496 m_currentMouseDownOnButtonHighlight = nullptr;
500 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
502 m_mousePosition = m_webPage.corePage()->mainFrame().view()->rootViewToContents(event.position());
504 bool mouseIsOverActiveHighlightButton = false;
505 determineActiveHighlight(mouseIsOverActiveHighlightButton);
507 // Cancel the potential click if any button other than the left button changes state, and ignore the event.
508 if (event.button() != WebMouseEvent::LeftButton) {
509 m_currentMouseDownOnButtonHighlight = nullptr;
513 // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
514 if (event.type() == WebEvent::MouseUp) {
515 RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
516 m_currentMouseDownOnButtonHighlight = nullptr;
518 if (mouseIsOverActiveHighlightButton && mouseDownHighlight && remainingTimeUntilHighlightShouldBeShown() <= std::chrono::steady_clock::duration::zero()) {
519 handleClick(m_mousePosition, *mouseDownHighlight);
526 // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
527 if (event.type() == WebEvent::MouseMove) {
528 if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
531 m_currentMouseDownOnButtonHighlight = nullptr;
535 // If the mouse went down over the active highlight's button, track this as a potential click.
536 if (event.type() == WebEvent::MouseDown) {
537 if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
538 m_currentMouseDownOnButtonHighlight = m_activeHighlight;
539 m_servicesOverlay->setNeedsDisplay();
549 void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, Highlight& highlight)
551 FrameView* frameView = m_webPage.mainFrameView();
555 IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
557 if (highlight.type() == Highlight::Type::Selection) {
558 auto telephoneNumberRanges = telephoneNumberRangesForFocusedFrame();
559 Vector<String> selectedTelephoneNumbers;
560 selectedTelephoneNumbers.reserveCapacity(telephoneNumberRanges.size());
561 for (auto& range : telephoneNumberRanges)
562 selectedTelephoneNumbers.append(range->text());
564 m_webPage.handleSelectionServiceClick(m_webPage.corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
565 } else if (highlight.type() == Highlight::Type::TelephoneNumber)
566 m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
569 } // namespace WebKit
571 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)