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/FrameView.h>
37 #import <WebCore/GapRects.h>
38 #import <WebCore/GraphicsContext.h>
39 #import <WebCore/MainFrame.h>
40 #import <WebCore/SoftLinking.h>
42 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
43 #import <DataDetectors/DDHighlightDrawing.h>
45 typedef void* DDHighlightRef;
48 #if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
49 #import <DataDetectors/DDHighlightDrawing_Private.h>
52 typedef NSUInteger DDHighlightStyle;
53 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
54 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
56 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
57 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))
58 SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
59 SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
60 SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
62 using namespace WebCore;
66 static IntRect textQuadsToBoundingRectForRange(Range& range)
68 Vector<FloatQuad> textQuads;
69 range.textQuads(textQuads);
70 FloatRect boundingRect;
71 for (auto& quad : textQuads)
72 boundingRect.unite(quad.boundingBox());
73 return enclosingIntRect(boundingRect);
76 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
78 , m_servicesOverlay(nullptr)
80 , m_repaintHighlightTimer(this, &ServicesOverlayController::repaintHighlightTimerFired)
84 ServicesOverlayController::~ServicesOverlayController()
88 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
90 // Before the overlay is destroyed, it should have moved out of the WebPage,
91 // at which point we already cleared our back pointer.
92 ASSERT(!m_servicesOverlay);
95 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
100 ASSERT(m_servicesOverlay);
101 m_servicesOverlay = nullptr;
107 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
111 void ServicesOverlayController::createOverlayIfNeeded()
113 if (m_servicesOverlay) {
114 m_servicesOverlay->setNeedsDisplay();
118 if (m_currentTelephoneNumberRanges.isEmpty() && (!WebProcess::shared().hasSelectionServices() || m_currentSelectionRects.isEmpty()))
121 RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
122 m_servicesOverlay = overlay.get();
123 m_webPage->installPageOverlay(overlay.release(), PageOverlay::FadeMode::Fade);
124 m_servicesOverlay->setNeedsDisplay();
127 static const uint8_t AlignmentNone = 0;
128 static const uint8_t AlignmentLeft = 1 << 0;
129 static const uint8_t AlignmentRight = 1 << 1;
131 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
133 if (!gap.left().isEmpty()) {
134 LayoutUnit leftEdge = gap.left().x();
135 for (unsigned i = 0; i < 3; ++i) {
136 if (alignments[i] & AlignmentLeft)
137 rects[i].shiftXEdgeTo(leftEdge);
141 if (!gap.right().isEmpty()) {
142 LayoutUnit rightEdge = gap.right().maxX();
143 for (unsigned i = 0; i < 3; ++i) {
144 if (alignments[i] & AlignmentRight)
145 rects[i].shiftMaxXEdgeTo(rightEdge);
150 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
155 // All of the middle rects - everything but the first and last - can be unioned together.
156 if (rects.size() > 3) {
158 for (unsigned i = 1; i < rects.size() - 1; ++i)
159 united.unite(rects[i]);
162 rects[2] = rects.last();
166 // FIXME: The following alignments are correct for LTR text.
167 // We should also account for RTL.
168 uint8_t alignments[3];
169 if (rects.size() == 1) {
170 alignments[0] = AlignmentLeft | AlignmentRight;
171 alignments[1] = AlignmentNone;
172 alignments[2] = AlignmentNone;
173 } else if (rects.size() == 2) {
174 alignments[0] = AlignmentRight;
175 alignments[1] = AlignmentLeft;
176 alignments[2] = AlignmentNone;
178 alignments[0] = AlignmentRight;
179 alignments[1] = AlignmentLeft | AlignmentRight;
180 alignments[2] = AlignmentLeft;
183 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
184 for (auto& gap : gapRects)
185 expandForGap(rects, alignments, gap);
187 // If we have 3 rects we might need one final GapRects to align the edges.
188 if (rects.size() == 3) {
191 for (unsigned i = 0; i < 3; ++i) {
192 if (alignments[i] & AlignmentLeft) {
195 else if (rects[i].x() < left.x())
198 if (alignments[i] & AlignmentRight) {
201 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
206 if (!left.isEmpty() || !right.isEmpty()) {
209 gap.uniteRight(right);
210 expandForGap(rects, alignments, gap);
215 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
217 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
218 clearSelectionHighlight();
219 m_currentSelectionRects = rects;
220 m_isTextOnly = isTextOnly;
222 m_lastSelectionChangeTime = std::chrono::steady_clock::now();
224 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
226 // DataDetectors needs these reversed in order to place the arrow in the right location.
227 m_currentSelectionRects.reverse();
229 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
231 createOverlayIfNeeded();
237 void ServicesOverlayController::selectedTelephoneNumberRangesChanged(const Vector<RefPtr<Range>>& ranges)
239 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
240 LOG(Services, "ServicesOverlayController - Telephone number ranges changed - Had %lu, now have %lu\n", m_currentTelephoneNumberRanges.size(), ranges.size());
241 m_currentTelephoneNumberRanges = ranges;
242 m_telephoneNumberHighlights.clear();
243 m_telephoneNumberHighlights.resize(ranges.size());
245 createOverlayIfNeeded();
247 UNUSED_PARAM(ranges);
251 void ServicesOverlayController::clearHighlightState()
253 clearSelectionHighlight();
254 clearHoveredTelephoneNumberHighlight();
256 m_telephoneNumberHighlights.clear();
259 void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
261 if (m_currentSelectionRects.isEmpty() && m_currentTelephoneNumberRanges.isEmpty()) {
262 clearHighlightState();
266 if (drawTelephoneNumberHighlightIfVisible(graphicsContext, dirtyRect))
269 drawSelectionHighlight(graphicsContext, dirtyRect);
272 void ServicesOverlayController::drawSelectionHighlight(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
274 // It's possible to end up drawing the selection highlight before we've actually received the selection rects.
275 // If that happens we'll end up here again once we have the rects.
276 if (m_currentSelectionRects.isEmpty() || (!WebProcess::shared().hasRichContentServices() && !m_isTextOnly))
279 // If there are no installed selection services and we have no phone numbers detected, then we have nothing to draw.
280 if (!WebProcess::shared().hasSelectionServices() && m_currentTelephoneNumberRanges.isEmpty())
283 if (!m_selectionHighlight)
284 maybeCreateSelectionHighlight();
286 if (m_selectionHighlight)
287 drawHighlight(m_selectionHighlight.get(), graphicsContext);
290 bool ServicesOverlayController::drawTelephoneNumberHighlightIfVisible(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
292 // Make sure the hovered telephone number highlight is still hovered.
293 if (m_hoveredTelephoneNumberData) {
295 if (!DDHighlightPointIsOnHighlight(m_hoveredTelephoneNumberData->highlight.get(), (CGPoint)m_mousePosition, &onButton))
296 clearHoveredTelephoneNumberHighlight();
298 bool foundMatchingRange = false;
300 // Make sure the hovered highlight still corresponds to a current telephone number range.
301 for (auto& range : m_currentTelephoneNumberRanges) {
302 if (areRangesEqual(range.get(), m_hoveredTelephoneNumberData->range.get())) {
303 foundMatchingRange = true;
308 if (!foundMatchingRange)
309 clearHoveredTelephoneNumberHighlight();
312 // Found out which - if any - telephone number is hovered.
313 if (!m_hoveredTelephoneNumberData) {
314 bool mouseIsOverButton;
315 establishHoveredTelephoneHighlight(mouseIsOverButton);
318 // If a telephone number is actually hovered, draw it.
319 if (m_hoveredTelephoneNumberData) {
320 drawHighlight(m_hoveredTelephoneNumberData->highlight.get(), graphicsContext);
327 bool ServicesOverlayController::mouseIsOverHighlight(DDHighlightRef highlight, bool& mouseIsOverButton) const
330 bool hovered = DDHighlightPointIsOnHighlight(highlight, (CGPoint)m_mousePosition, &onButton);
331 mouseIsOverButton = onButton;
335 std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown() const
337 // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
338 // by virtue of being expanded to include the entire telephone number.
339 if (m_hoveredTelephoneNumberData)
340 return std::chrono::milliseconds::zero();
342 std::chrono::steady_clock::duration minimumTimeUntilHighlightShouldBeShown = 200_ms;
344 auto now = std::chrono::steady_clock::now();
345 auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
346 auto timeSinceMouseOverSelection = now - m_lastHoveredHighlightChangeTime;
348 return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceMouseOverSelection));
351 void ServicesOverlayController::repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&)
353 if (m_servicesOverlay)
354 m_servicesOverlay->setNeedsDisplay();
357 void ServicesOverlayController::drawHighlight(DDHighlightRef highlight, WebCore::GraphicsContext& graphicsContext)
361 bool mouseIsOverButton;
362 if (!mouseIsOverHighlight(highlight, mouseIsOverButton)) {
363 LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
367 auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown();
368 if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
369 m_repaintHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
373 CGContextRef cgContext = graphicsContext.platformContext();
375 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight, cgContext);
376 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight);
378 GraphicsContextStateSaver stateSaver(graphicsContext);
380 graphicsContext.translate(toFloatSize(highlightBoundingRect.origin));
382 CGRect highlightDrawRect = highlightBoundingRect;
383 highlightDrawRect.origin.x = 0;
384 highlightDrawRect.origin.y = 0;
386 CGContextDrawLayerInRect(cgContext, highlightDrawRect, highlightLayer);
389 void ServicesOverlayController::clearSelectionHighlight()
391 if (!m_selectionHighlight)
394 if (m_currentHoveredHighlight == m_selectionHighlight)
395 m_currentHoveredHighlight = nullptr;
396 if (m_currentMouseDownOnButtonHighlight == m_selectionHighlight)
397 m_currentMouseDownOnButtonHighlight = nullptr;
398 m_selectionHighlight = nullptr;
401 void ServicesOverlayController::clearHoveredTelephoneNumberHighlight()
403 if (!m_hoveredTelephoneNumberData)
406 if (m_currentHoveredHighlight == m_hoveredTelephoneNumberData->highlight)
407 m_currentHoveredHighlight = nullptr;
408 if (m_currentMouseDownOnButtonHighlight == m_hoveredTelephoneNumberData->highlight)
409 m_currentMouseDownOnButtonHighlight = nullptr;
410 m_hoveredTelephoneNumberData = nullptr;
413 void ServicesOverlayController::establishHoveredTelephoneHighlight(bool& mouseIsOverButton)
415 ASSERT(m_currentTelephoneNumberRanges.size() == m_telephoneNumberHighlights.size());
417 for (unsigned i = 0; i < m_currentTelephoneNumberRanges.size(); ++i) {
418 if (!m_telephoneNumberHighlights[i]) {
419 // FIXME: This will choke if the range wraps around the edge of the view.
420 // What should we do in that case?
421 IntRect rect = textQuadsToBoundingRectForRange(*m_currentTelephoneNumberRanges[i]);
423 // Convert to the main document's coordinate space.
424 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
425 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
426 // not equivalent to what we need when you have a topContentInset or a header banner.
427 FrameView* viewForRange = m_currentTelephoneNumberRanges[i]->ownerDocument().view();
430 FrameView& mainFrameView = *m_webPage->corePage()->mainFrame().view();
431 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
433 CGRect cgRect = rect;
434 m_telephoneNumberHighlights[i] = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, viewForRange->boundsRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
437 if (!mouseIsOverHighlight(m_telephoneNumberHighlights[i].get(), mouseIsOverButton))
440 if (!m_hoveredTelephoneNumberData || m_hoveredTelephoneNumberData->highlight != m_telephoneNumberHighlights[i])
441 m_hoveredTelephoneNumberData = std::make_unique<TelephoneNumberData>(m_telephoneNumberHighlights[i], m_currentTelephoneNumberRanges[i]);
443 m_servicesOverlay->setNeedsDisplay();
447 clearHoveredTelephoneNumberHighlight();
448 mouseIsOverButton = false;
451 void ServicesOverlayController::maybeCreateSelectionHighlight()
453 ASSERT(!m_selectionHighlight);
454 ASSERT(m_servicesOverlay);
456 Vector<CGRect> cgRects;
457 cgRects.reserveCapacity(m_currentSelectionRects.size());
459 for (auto& rect : m_currentSelectionRects)
460 cgRects.append((CGRect)pixelSnappedIntRect(rect));
462 if (!cgRects.isEmpty()) {
463 CGRect bounds = m_webPage->corePage()->mainFrame().view()->boundsRect();
464 m_selectionHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), bounds, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
466 m_servicesOverlay->setNeedsDisplay();
470 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
472 m_mousePosition = m_webPage->corePage()->mainFrame().view()->rootViewToContents(event.position());
474 DDHighlightRef oldHoveredHighlight = m_currentHoveredHighlight.get();
476 bool mouseIsOverButton = false;
477 establishHoveredTelephoneHighlight(mouseIsOverButton);
478 if (m_hoveredTelephoneNumberData) {
479 ASSERT(m_hoveredTelephoneNumberData->highlight);
480 m_currentHoveredHighlight = m_hoveredTelephoneNumberData->highlight;
482 if (!m_selectionHighlight)
483 maybeCreateSelectionHighlight();
485 if (m_selectionHighlight && mouseIsOverHighlight(m_selectionHighlight.get(), mouseIsOverButton))
486 m_currentHoveredHighlight = m_selectionHighlight;
488 m_currentHoveredHighlight = nullptr;
491 if (oldHoveredHighlight != m_currentHoveredHighlight) {
492 m_lastHoveredHighlightChangeTime = std::chrono::steady_clock::now();
493 m_servicesOverlay->setNeedsDisplay();
496 // If this event has nothing to do with the left button, it clears the current mouse down tracking and we're done processing it.
497 if (event.button() != WebMouseEvent::LeftButton) {
498 m_currentMouseDownOnButtonHighlight = nullptr;
502 // Check and see if the mouse went up and we have a current mouse down highlight button.
503 if (event.type() == WebEvent::MouseUp) {
504 RetainPtr<DDHighlightRef> mouseDownHighlight = WTF::move(m_currentMouseDownOnButtonHighlight);
506 // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
507 if (mouseIsOverButton && mouseDownHighlight && remainingTimeUntilHighlightShouldBeShown() <= std::chrono::steady_clock::duration::zero()) {
508 handleClick(m_mousePosition, mouseDownHighlight.get());
515 // Check and see if the mouse moved within the confines of the DD highlight button.
516 if (event.type() == WebEvent::MouseMove) {
517 // Moving with the mouse button down is okay as long as the mouse never leaves the highlight button.
518 if (m_currentMouseDownOnButtonHighlight && mouseIsOverButton)
521 m_currentMouseDownOnButtonHighlight = nullptr;
525 // Check and see if the mouse went down over a DD highlight button.
526 if (event.type() == WebEvent::MouseDown) {
527 if (m_currentHoveredHighlight && mouseIsOverButton) {
528 m_currentMouseDownOnButtonHighlight = m_currentHoveredHighlight;
529 m_servicesOverlay->setNeedsDisplay();
539 void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, DDHighlightRef highlight)
543 FrameView* frameView = m_webPage->mainFrameView();
547 IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
549 if (highlight == m_selectionHighlight) {
550 Vector<String> selectedTelephoneNumbers;
551 selectedTelephoneNumbers.reserveCapacity(m_currentTelephoneNumberRanges.size());
552 for (auto& range : m_currentTelephoneNumberRanges)
553 selectedTelephoneNumbers.append(range->text());
555 m_webPage->handleSelectionServiceClick(m_webPage->corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
556 } else if (m_hoveredTelephoneNumberData && m_hoveredTelephoneNumberData->highlight == highlight)
557 m_webPage->handleTelephoneNumberClick(m_hoveredTelephoneNumberData->range->text(), windowPoint);
559 ASSERT_NOT_REACHED();
562 } // namespace WebKit
564 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)