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)
82 ServicesOverlayController::~ServicesOverlayController()
84 if (m_servicesOverlay) {
86 m_webPage->uninstallPageOverlay(m_servicesOverlay, PageOverlay::FadeMode::DoNotFade);
90 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
92 // Before the overlay is destroyed, it should have moved out of the WebPage,
93 // at which point we already cleared our back pointer.
94 ASSERT(!m_servicesOverlay);
97 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
102 ASSERT(m_servicesOverlay);
103 m_servicesOverlay = nullptr;
109 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
113 void ServicesOverlayController::createOverlayIfNeeded()
115 if (m_servicesOverlay) {
116 m_servicesOverlay->setNeedsDisplay();
120 if (m_currentTelephoneNumberRanges.isEmpty() && (!WebProcess::shared().hasSelectionServices() || m_currentSelectionRects.isEmpty()))
123 RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
124 m_servicesOverlay = overlay.get();
125 m_webPage->installPageOverlay(overlay.release(), PageOverlay::FadeMode::Fade);
126 m_servicesOverlay->setNeedsDisplay();
129 static const uint8_t AlignmentNone = 0;
130 static const uint8_t AlignmentLeft = 1 << 0;
131 static const uint8_t AlignmentRight = 1 << 1;
133 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
135 if (!gap.left().isEmpty()) {
136 LayoutUnit leftEdge = gap.left().x();
137 for (unsigned i = 0; i < 3; ++i) {
138 if (alignments[i] & AlignmentLeft)
139 rects[i].shiftXEdgeTo(leftEdge);
143 if (!gap.right().isEmpty()) {
144 LayoutUnit rightEdge = gap.right().maxX();
145 for (unsigned i = 0; i < 3; ++i) {
146 if (alignments[i] & AlignmentRight)
147 rects[i].shiftMaxXEdgeTo(rightEdge);
152 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
157 // All of the middle rects - everything but the first and last - can be unioned together.
158 if (rects.size() > 3) {
160 for (unsigned i = 1; i < rects.size() - 1; ++i)
161 united.unite(rects[i]);
164 rects[2] = rects.last();
168 // FIXME: The following alignments are correct for LTR text.
169 // We should also account for RTL.
170 uint8_t alignments[3];
171 if (rects.size() == 1) {
172 alignments[0] = AlignmentLeft | AlignmentRight;
173 alignments[1] = AlignmentNone;
174 alignments[2] = AlignmentNone;
175 } else if (rects.size() == 2) {
176 alignments[0] = AlignmentRight;
177 alignments[1] = AlignmentLeft;
178 alignments[2] = AlignmentNone;
180 alignments[0] = AlignmentRight;
181 alignments[1] = AlignmentLeft | AlignmentRight;
182 alignments[2] = AlignmentLeft;
185 // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
186 for (auto& gap : gapRects)
187 expandForGap(rects, alignments, gap);
189 // If we have 3 rects we might need one final GapRects to align the edges.
190 if (rects.size() == 3) {
193 for (unsigned i = 0; i < 3; ++i) {
194 if (alignments[i] & AlignmentLeft) {
197 else if (rects[i].x() < left.x())
200 if (alignments[i] & AlignmentRight) {
203 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
208 if (!left.isEmpty() || !right.isEmpty()) {
211 gap.uniteRight(right);
212 expandForGap(rects, alignments, gap);
217 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
219 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
220 clearSelectionHighlight();
221 m_currentSelectionRects = rects;
223 compactRectsWithGapRects(m_currentSelectionRects, gapRects);
225 // DataDetectors needs these reversed in order to place the arrow in the right location.
226 m_currentSelectionRects.reverse();
228 LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
230 createOverlayIfNeeded();
236 void ServicesOverlayController::selectedTelephoneNumberRangesChanged(const Vector<RefPtr<Range>>& ranges)
238 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
239 LOG(Services, "ServicesOverlayController - Telephone number ranges changed - Had %lu, now have %lu\n", m_currentTelephoneNumberRanges.size(), ranges.size());
240 m_currentTelephoneNumberRanges = ranges;
241 m_telephoneNumberHighlights.clear();
242 m_telephoneNumberHighlights.resize(ranges.size());
244 createOverlayIfNeeded();
246 UNUSED_PARAM(ranges);
250 void ServicesOverlayController::clearHighlightState()
252 clearSelectionHighlight();
253 clearHoveredTelephoneNumberHighlight();
255 m_telephoneNumberHighlights.clear();
258 void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
260 if (m_currentSelectionRects.isEmpty() && m_currentTelephoneNumberRanges.isEmpty()) {
261 clearHighlightState();
265 if (drawTelephoneNumberHighlightIfVisible(graphicsContext, dirtyRect))
268 drawSelectionHighlight(graphicsContext, dirtyRect);
271 void ServicesOverlayController::drawSelectionHighlight(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
273 // It's possible to end up drawing the selection highlight before we've actually received the selection rects.
274 // If that happens we'll end up here again once we have the rects.
275 if (m_currentSelectionRects.isEmpty())
278 // If there are no installed selection services and we have no phone numbers detected, then we have nothing to draw.
279 if (!WebProcess::shared().hasSelectionServices() && m_currentTelephoneNumberRanges.isEmpty())
282 if (!m_selectionHighlight)
283 maybeCreateSelectionHighlight();
285 if (m_selectionHighlight)
286 drawHighlight(m_selectionHighlight.get(), graphicsContext);
289 bool ServicesOverlayController::drawTelephoneNumberHighlightIfVisible(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
291 // Make sure the hovered telephone number highlight is still hovered.
292 if (m_hoveredTelephoneNumberData) {
294 if (!DDHighlightPointIsOnHighlight(m_hoveredTelephoneNumberData->highlight.get(), (CGPoint)m_mousePosition, &onButton))
295 clearHoveredTelephoneNumberHighlight();
297 bool foundMatchingRange = false;
299 // Make sure the hovered highlight still corresponds to a current telephone number range.
300 for (auto& range : m_currentTelephoneNumberRanges) {
301 if (areRangesEqual(range.get(), m_hoveredTelephoneNumberData->range.get())) {
302 foundMatchingRange = true;
307 if (!foundMatchingRange)
308 clearHoveredTelephoneNumberHighlight();
311 // Found out which - if any - telephone number is hovered.
312 if (!m_hoveredTelephoneNumberData) {
314 establishHoveredTelephoneHighlight(onButton);
317 // If a telephone number is actually hovered, draw it.
318 if (m_hoveredTelephoneNumberData) {
319 drawHighlight(m_hoveredTelephoneNumberData->highlight.get(), graphicsContext);
326 void ServicesOverlayController::drawHighlight(DDHighlightRef highlight, WebCore::GraphicsContext& graphicsContext)
331 bool mouseIsOverHighlight = DDHighlightPointIsOnHighlight(highlight, (CGPoint)m_mousePosition, &onButton);
333 if (!mouseIsOverHighlight) {
334 LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
338 CGContextRef cgContext = graphicsContext.platformContext();
340 CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight, cgContext);
341 CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight);
343 GraphicsContextStateSaver stateSaver(graphicsContext);
345 graphicsContext.translate(toFloatSize(highlightBoundingRect.origin));
347 CGRect highlightDrawRect = highlightBoundingRect;
348 highlightDrawRect.origin.x = 0;
349 highlightDrawRect.origin.y = 0;
351 CGContextDrawLayerInRect(cgContext, highlightDrawRect, highlightLayer);
354 void ServicesOverlayController::clearSelectionHighlight()
356 if (!m_selectionHighlight)
359 if (m_currentHoveredHighlight == m_selectionHighlight)
360 m_currentHoveredHighlight = nullptr;
361 if (m_currentMouseDownOnButtonHighlight == m_selectionHighlight)
362 m_currentMouseDownOnButtonHighlight = nullptr;
363 m_selectionHighlight = nullptr;
366 void ServicesOverlayController::clearHoveredTelephoneNumberHighlight()
368 if (!m_hoveredTelephoneNumberData)
371 if (m_currentHoveredHighlight == m_hoveredTelephoneNumberData->highlight)
372 m_currentHoveredHighlight = nullptr;
373 if (m_currentMouseDownOnButtonHighlight == m_hoveredTelephoneNumberData->highlight)
374 m_currentMouseDownOnButtonHighlight = nullptr;
375 m_hoveredTelephoneNumberData = nullptr;
378 void ServicesOverlayController::establishHoveredTelephoneHighlight(Boolean& onButton)
380 ASSERT(m_currentTelephoneNumberRanges.size() == m_telephoneNumberHighlights.size());
382 for (unsigned i = 0; i < m_currentTelephoneNumberRanges.size(); ++i) {
383 if (!m_telephoneNumberHighlights[i]) {
384 // FIXME: This will choke if the range wraps around the edge of the view.
385 // What should we do in that case?
386 IntRect rect = textQuadsToBoundingRectForRange(*m_currentTelephoneNumberRanges[i]);
388 // Convert to the main document's coordinate space.
389 // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
390 // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
391 // not equivalent to what we need when you have a topContentInset or a header banner.
392 FrameView* viewForRange = m_currentTelephoneNumberRanges[i]->ownerDocument().view();
395 FrameView& mainFrameView = *m_webPage->corePage()->mainFrame().view();
396 rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
398 CGRect cgRect = rect;
399 m_telephoneNumberHighlights[i] = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, viewForRange->boundsRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
402 if (!DDHighlightPointIsOnHighlight(m_telephoneNumberHighlights[i].get(), (CGPoint)m_mousePosition, &onButton))
405 if (!m_hoveredTelephoneNumberData || m_hoveredTelephoneNumberData->highlight != m_telephoneNumberHighlights[i])
406 m_hoveredTelephoneNumberData = std::make_unique<TelephoneNumberData>(m_telephoneNumberHighlights[i], m_currentTelephoneNumberRanges[i]);
408 m_servicesOverlay->setNeedsDisplay();
412 clearHoveredTelephoneNumberHighlight();
416 void ServicesOverlayController::maybeCreateSelectionHighlight()
418 ASSERT(!m_selectionHighlight);
419 ASSERT(m_servicesOverlay);
421 Vector<CGRect> cgRects;
422 cgRects.reserveCapacity(m_currentSelectionRects.size());
424 for (auto& rect : m_currentSelectionRects)
425 cgRects.append((CGRect)pixelSnappedIntRect(rect));
427 if (!cgRects.isEmpty()) {
428 CGRect bounds = m_webPage->corePage()->mainFrame().view()->boundsRect();
429 m_selectionHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), bounds, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
431 m_servicesOverlay->setNeedsDisplay();
435 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
437 m_mousePosition = m_webPage->corePage()->mainFrame().view()->rootViewToContents(event.position());
439 DDHighlightRef oldHoveredHighlight = m_currentHoveredHighlight.get();
441 Boolean onButton = false;
442 establishHoveredTelephoneHighlight(onButton);
443 if (m_hoveredTelephoneNumberData) {
444 ASSERT(m_hoveredTelephoneNumberData->highlight);
445 m_currentHoveredHighlight = m_hoveredTelephoneNumberData->highlight;
447 if (!m_selectionHighlight)
448 maybeCreateSelectionHighlight();
450 if (m_selectionHighlight && DDHighlightPointIsOnHighlight(m_selectionHighlight.get(), (CGPoint)m_mousePosition, &onButton))
451 m_currentHoveredHighlight = m_selectionHighlight;
453 m_currentHoveredHighlight = nullptr;
456 if (oldHoveredHighlight != m_currentHoveredHighlight)
457 m_servicesOverlay->setNeedsDisplay();
459 // If this event has nothing to do with the left button, it clears the current mouse down tracking and we're done processing it.
460 if (event.button() != WebMouseEvent::LeftButton) {
461 m_currentMouseDownOnButtonHighlight = nullptr;
465 // Check and see if the mouse went up and we have a current mouse down highlight button.
466 if (event.type() == WebEvent::MouseUp) {
467 RetainPtr<DDHighlightRef> mouseDownHighlight = std::move(m_currentMouseDownOnButtonHighlight);
469 // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
470 if (onButton && mouseDownHighlight) {
471 handleClick(m_mousePosition, mouseDownHighlight.get());
478 // Check and see if the mouse moved within the confines of the DD highlight button.
479 if (event.type() == WebEvent::MouseMove) {
480 // Moving with the mouse button down is okay as long as the mouse never leaves the highlight button.
481 if (m_currentMouseDownOnButtonHighlight && onButton)
484 m_currentMouseDownOnButtonHighlight = nullptr;
488 // Check and see if the mouse went down over a DD highlight button.
489 if (event.type() == WebEvent::MouseDown) {
490 if (m_currentHoveredHighlight && onButton) {
491 m_currentMouseDownOnButtonHighlight = m_currentHoveredHighlight;
492 m_servicesOverlay->setNeedsDisplay();
502 void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, DDHighlightRef highlight)
506 FrameView* frameView = m_webPage->mainFrameView();
510 IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
512 if (highlight == m_selectionHighlight) {
513 Vector<String> selectedTelephoneNumbers;
514 selectedTelephoneNumbers.reserveCapacity(m_currentTelephoneNumberRanges.size());
515 for (auto& range : m_currentTelephoneNumberRanges)
516 selectedTelephoneNumbers.append(range->text());
518 m_webPage->handleSelectionServiceClick(m_webPage->corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
519 } else if (m_hoveredTelephoneNumberData && m_hoveredTelephoneNumberData->highlight == highlight)
520 m_webPage->handleTelephoneNumberClick(m_hoveredTelephoneNumberData->range->text(), windowPoint);
522 ASSERT_NOT_REACHED();
525 } // namespace WebKit
527 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)