1/*
2 * Copyright (C) 2014-2018 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 "DictionaryLookup.h"
28
29#if PLATFORM(COCOA)
30
31#if ENABLE(REVEAL)
32
33#import "Document.h"
34#import "Editing.h"
35#import "FocusController.h"
36#import "Frame.h"
37#import "FrameSelection.h"
38#import "HTMLConverter.h"
39#import "HitTestResult.h"
40#import "NotImplemented.h"
41#import "Page.h"
42#import "Range.h"
43#import "RenderObject.h"
44#import "TextIterator.h"
45#import "VisiblePosition.h"
46#import "VisibleSelection.h"
47#import "VisibleUnits.h"
48#import <pal/spi/cocoa/RevealSPI.h>
49#import <pal/spi/mac/LookupSPI.h>
50#import <wtf/BlockObjCExceptions.h>
51#import <wtf/RefPtr.h>
52
53#if !PLATFORM(WATCH)
54
55#import <PDFKit/PDFKit.h>
56
57#endif // !PLATFORM(WATCH)
58
59#if PLATFORM(MAC)
60
61@interface WebRevealHighlight <RVPresenterHighlightDelegate> : NSObject {
62@private
63 Function<void()> _clearTextIndicator;
64 NSRect _highlightRect;
65 BOOL _useDefaultHighlight;
66 NSAttributedString *_attributedString;
67}
68
69@property (nonatomic, readonly) NSRect highlightRect;
70@property (nonatomic, readonly) BOOL useDefaultHighlight;
71@property (nonatomic, readonly) NSAttributedString *attributedString;
72
73- (instancetype)initWithHighlightRect:(NSRect)highlightRect useDefaultHighlight:(BOOL)useDefaultHighlight attributedString:(NSAttributedString *) attributedString;
74- (void)setClearTextIndicator:(Function<void()>&&)clearTextIndicator;
75
76@end
77
78@implementation WebRevealHighlight
79
80@synthesize highlightRect=_highlightRect;
81@synthesize useDefaultHighlight=_useDefaultHighlight;
82@synthesize attributedString=_attributedString;
83
84- (instancetype)initWithHighlightRect:(NSRect)highlightRect useDefaultHighlight:(BOOL)useDefaultHighlight attributedString:(NSAttributedString *) attributedString
85{
86 if (!(self = [super init]))
87 return nil;
88
89 _highlightRect = highlightRect;
90 _useDefaultHighlight = useDefaultHighlight;
91 _attributedString = attributedString;
92
93 return self;
94}
95
96- (void)setClearTextIndicator:(Function<void()>&&)clearTextIndicator
97{
98 _clearTextIndicator = WTFMove(clearTextIndicator);
99}
100
101- (NSArray<NSValue *> *)revealContext:(RVPresentingContext *)context rectsForItem:(RVItem *)item
102{
103 UNUSED_PARAM(context);
104 UNUSED_PARAM(item);
105 return @[[NSValue valueWithRect:self.highlightRect]];
106}
107
108- (void)revealContext:(RVPresentingContext *)context drawRectsForItem:(RVItem *)item
109{
110 UNUSED_PARAM(item);
111
112 for (NSValue *rectVal in context.itemRectsInView) {
113 NSRect rect = rectVal.rectValue;
114
115 // Get current font attributes from the attributed string above, and add paragraph style attribute in order to center text.
116 RetainPtr<NSMutableDictionary> attributes = adoptNS([[NSMutableDictionary alloc] initWithDictionary:[self.attributedString fontAttributesInRange:NSMakeRange(0, [self.attributedString length])]]);
117 RetainPtr<NSMutableParagraphStyle> paragraph = adoptNS([[NSMutableParagraphStyle alloc] init]);
118 [paragraph setAlignment:NSTextAlignmentCenter];
119 [attributes setObject:paragraph.get() forKey:NSParagraphStyleAttributeName];
120
121 RetainPtr<NSAttributedString> string = adoptNS([[NSAttributedString alloc] initWithString:[self.attributedString string] attributes:attributes.get()]);
122 [string drawInRect:rect];
123 }
124}
125
126- (BOOL)revealContext:(RVPresentingContext *)context shouldUseDefaultHighlightForItem:(RVItem *)item
127{
128 UNUSED_PARAM(context);
129 UNUSED_PARAM(item);
130 return self.useDefaultHighlight;
131}
132
133- (void)revealContext:(RVPresentingContext *)context stopHighlightingItem:(RVItem *)item
134{
135 UNUSED_PARAM(context);
136 UNUSED_PARAM(item);
137 auto block = WTFMove(_clearTextIndicator);
138 if (block)
139 block();
140}
141
142@end
143
144#endif // PLATFORM(MAC)
145
146#endif // ENABLE(REVEAL)
147
148namespace WebCore {
149
150#if ENABLE(REVEAL)
151
152std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeForSelection(const VisibleSelection& selection)
153{
154 BEGIN_BLOCK_OBJC_EXCEPTIONS;
155
156 if (!getRVItemClass())
157 return { nullptr, nil };
158
159 auto selectedRange = selection.toNormalizedRange();
160 if (!selectedRange)
161 return { nullptr, nil };
162
163 // Since we already have the range we want, we just need to grab the returned options.
164 auto selectionStart = selection.visibleStart();
165 auto selectionEnd = selection.visibleEnd();
166
167 // As context, we are going to use the surrounding paragraphs of text.
168 auto paragraphStart = startOfParagraph(selectionStart);
169 auto paragraphEnd = endOfParagraph(selectionEnd);
170
171 int lengthToSelectionStart = TextIterator::rangeLength(makeRange(paragraphStart, selectionStart).get());
172 int lengthToSelectionEnd = TextIterator::rangeLength(makeRange(paragraphStart, selectionEnd).get());
173 NSRange rangeToPass = NSMakeRange(lengthToSelectionStart, lengthToSelectionEnd - lengthToSelectionStart);
174
175 RefPtr<Range> fullCharacterRange = makeRange(paragraphStart, paragraphEnd);
176 String itemString = plainText(fullCharacterRange.get());
177 RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:itemString selectedRange:rangeToPass]);
178 NSRange highlightRange = item.get().highlightRange;
179
180 return { TextIterator::subrange(*fullCharacterRange, highlightRange.location, highlightRange.length), nil };
181
182 END_BLOCK_OBJC_EXCEPTIONS;
183
184 return { nullptr, nil };
185}
186
187std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeAtHitTestResult(const HitTestResult& hitTestResult)
188{
189 BEGIN_BLOCK_OBJC_EXCEPTIONS;
190
191 if (!getRVItemClass())
192 return { nullptr, nil };
193
194 auto* node = hitTestResult.innerNonSharedNode();
195 if (!node || !node->renderer())
196 return { nullptr, nil };
197
198 auto* frame = node->document().frame();
199 if (!frame)
200 return { nullptr, nil };
201
202 // Don't do anything if there is no character at the point.
203 auto framePoint = hitTestResult.roundedPointInInnerNodeFrame();
204 if (!frame->rangeForPoint(framePoint))
205 return { nullptr, nil };
206
207 auto position = frame->visiblePositionForPoint(framePoint);
208 if (position.isNull())
209 position = firstPositionInOrBeforeNode(node);
210
211 auto selection = frame->page()->focusController().focusedOrMainFrame().selection().selection();
212 NSRange selectionRange;
213 int hitIndex;
214 RefPtr<Range> fullCharacterRange;
215
216 if (selection.selectionType() == VisibleSelection::RangeSelection) {
217 auto selectionStart = selection.visibleStart();
218 auto selectionEnd = selection.visibleEnd();
219
220 // As context, we are going to use the surrounding paragraphs of text.
221 auto paragraphStart = startOfParagraph(selectionStart);
222 auto paragraphEnd = endOfParagraph(selectionEnd);
223
224 auto rangeToSelectionStart = makeRange(paragraphStart, selectionStart);
225 auto rangeToSelectionEnd = makeRange(paragraphStart, selectionEnd);
226
227 fullCharacterRange = makeRange(paragraphStart, paragraphEnd);
228
229 selectionRange = NSMakeRange(TextIterator::rangeLength(rangeToSelectionStart.get()), TextIterator::rangeLength(makeRange(selectionStart, selectionEnd).get()));
230
231 hitIndex = TextIterator::rangeLength(makeRange(paragraphStart, position).get());
232 } else {
233 VisibleSelection selectionAccountingForLineRules { position };
234 selectionAccountingForLineRules.expandUsingGranularity(WordGranularity);
235 position = selectionAccountingForLineRules.start();
236 // As context, we are going to use 250 characters of text before and after the point.
237 fullCharacterRange = rangeExpandedAroundPositionByCharacters(position, 250);
238
239 if (!fullCharacterRange)
240 return { nullptr, nil };
241
242 selectionRange = NSMakeRange(NSNotFound, 0);
243 hitIndex = TextIterator::rangeLength(makeRange(fullCharacterRange->startPosition(), position).get());
244 }
245
246 NSRange selectedRange = [getRVSelectionClass() revealRangeAtIndex:hitIndex selectedRanges:@[[NSValue valueWithRange:selectionRange]] shouldUpdateSelection:nil];
247
248 String itemString = plainText(fullCharacterRange.get());
249 RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:itemString selectedRange:selectedRange]);
250 NSRange highlightRange = item.get().highlightRange;
251
252 if (highlightRange.location == NSNotFound || !highlightRange.length)
253 return { nullptr, nil };
254
255 return { TextIterator::subrange(*fullCharacterRange, highlightRange.location, highlightRange.length), nil };
256
257 END_BLOCK_OBJC_EXCEPTIONS;
258
259 return { nullptr, nil };
260
261}
262
263#if !PLATFORM(WATCH)
264
265static void expandSelectionByCharacters(PDFSelection *selection, NSInteger numberOfCharactersToExpand, NSInteger& charactersAddedBeforeStart, NSInteger& charactersAddedAfterEnd)
266{
267 BEGIN_BLOCK_OBJC_EXCEPTIONS;
268
269 size_t originalLength = selection.string.length;
270 [selection extendSelectionAtStart:numberOfCharactersToExpand];
271
272 charactersAddedBeforeStart = selection.string.length - originalLength;
273
274 [selection extendSelectionAtEnd:numberOfCharactersToExpand];
275 charactersAddedAfterEnd = selection.string.length - originalLength - charactersAddedBeforeStart;
276
277 END_BLOCK_OBJC_EXCEPTIONS;
278}
279
280std::tuple<NSString *, NSDictionary *> DictionaryLookup::stringForPDFSelection(PDFSelection *selection)
281{
282 BEGIN_BLOCK_OBJC_EXCEPTIONS;
283
284 if (!getRVItemClass())
285 return { nullptr, nil };
286
287 // Don't do anything if there is no character at the point.
288 if (!selection || !selection.string.length)
289 return { @"", nil };
290
291 RetainPtr<PDFSelection> selectionForLookup = adoptNS([selection copy]);
292
293 // As context, we are going to use 250 characters of text before and after the point.
294 auto originalLength = [selectionForLookup string].length;
295 NSInteger charactersAddedBeforeStart = 0;
296 NSInteger charactersAddedAfterEnd = 0;
297 expandSelectionByCharacters(selectionForLookup.get(), 250, charactersAddedBeforeStart, charactersAddedAfterEnd);
298
299 auto fullPlainTextString = [selectionForLookup string];
300 auto rangeToPass = NSMakeRange(charactersAddedBeforeStart, 0);
301
302 RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:fullPlainTextString selectedRange:rangeToPass]);
303 NSRange extractedRange = item.get().highlightRange;
304
305 if (extractedRange.location == NSNotFound)
306 return { selection.string, nil };
307
308 NSInteger lookupAddedBefore = rangeToPass.location - extractedRange.location;
309 NSInteger lookupAddedAfter = (extractedRange.location + extractedRange.length) - (rangeToPass.location + originalLength);
310
311 [selection extendSelectionAtStart:lookupAddedBefore];
312 [selection extendSelectionAtEnd:lookupAddedAfter];
313
314 ASSERT([selection.string isEqualToString:[fullPlainTextString substringWithRange:extractedRange]]);
315 return { selection.string, nil };
316
317 END_BLOCK_OBJC_EXCEPTIONS;
318
319 return { @"", nil };
320}
321
322#endif // !PLATFORM(WATCH)
323
324static WKRevealController showPopupOrCreateAnimationController(bool createAnimationController, const DictionaryPopupInfo& dictionaryPopupInfo, NSView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
325{
326 BEGIN_BLOCK_OBJC_EXCEPTIONS;
327
328#if PLATFORM(MAC)
329
330 if (!getRVItemClass() || !getRVPresenterClass())
331 return nil;
332
333 RetainPtr<NSMutableDictionary> mutableOptions = adoptNS([[NSMutableDictionary alloc] init]);
334 if (NSDictionary *options = dictionaryPopupInfo.options.get())
335 [mutableOptions addEntriesFromDictionary:options];
336
337 auto textIndicator = TextIndicator::create(dictionaryPopupInfo.textIndicator);
338
339 RetainPtr<RVPresenter> presenter = adoptNS([allocRVPresenterInstance() init]);
340
341 NSRect highlightRect;
342 NSPoint pointerLocation;
343
344 if (textIndicator.get().contentImage()) {
345 textIndicatorInstallationCallback(textIndicator.get());
346
347 FloatRect firstTextRectInViewCoordinates = textIndicator.get().textRectsInBoundingRectCoordinates()[0];
348 FloatRect textBoundingRectInViewCoordinates = textIndicator.get().textBoundingRectInRootViewCoordinates();
349 FloatRect selectionBoundingRectInViewCoordinates = textIndicator.get().selectionRectInRootViewCoordinates();
350
351 if (rootViewToViewConversionCallback) {
352 textBoundingRectInViewCoordinates = rootViewToViewConversionCallback(textBoundingRectInViewCoordinates);
353 selectionBoundingRectInViewCoordinates = rootViewToViewConversionCallback(selectionBoundingRectInViewCoordinates);
354 }
355
356 firstTextRectInViewCoordinates.moveBy(textBoundingRectInViewCoordinates.location());
357 highlightRect = selectionBoundingRectInViewCoordinates;
358 pointerLocation = firstTextRectInViewCoordinates.location();
359
360 } else {
361 NSPoint textBaselineOrigin = dictionaryPopupInfo.origin;
362
363 highlightRect = textIndicator->selectionRectInRootViewCoordinates();
364 pointerLocation = [view convertPoint:textBaselineOrigin toView:nil];
365 }
366
367 RetainPtr<WebRevealHighlight> webHighlight = adoptNS([[WebRevealHighlight alloc] initWithHighlightRect: highlightRect useDefaultHighlight:!textIndicator.get().contentImage() attributedString:dictionaryPopupInfo.attributedString.get()]);
368 RetainPtr<RVPresentingContext> context = adoptNS([allocRVPresentingContextInstance() initWithPointerLocationInView:pointerLocation inView:view highlightDelegate:(id<RVPresenterHighlightDelegate>) webHighlight.get()]);
369
370 RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:dictionaryPopupInfo.attributedString.get().string selectedRange:NSMakeRange(0, 0)]);
371
372 [webHighlight setClearTextIndicator:[webHighlight = WTFMove(webHighlight), clearTextIndicator = WTFMove(clearTextIndicator)] {
373 if (clearTextIndicator)
374 clearTextIndicator();
375 }];
376
377 if (createAnimationController)
378 return [presenter animationControllerForItem:item.get() documentContext:nil presentingContext:context.get() options:nil];
379 [presenter revealItem:item.get() documentContext:nil presentingContext:context.get() options:nil];
380 return nil;
381
382#else // PLATFORM(MAC)
383
384 UNUSED_PARAM(createAnimationController);
385 UNUSED_PARAM(dictionaryPopupInfo);
386 UNUSED_PARAM(view);
387 UNUSED_PARAM(textIndicatorInstallationCallback);
388 UNUSED_PARAM(rootViewToViewConversionCallback);
389 UNUSED_PARAM(clearTextIndicator);
390
391 return nil;
392#endif // PLATFORM(MAC)
393
394 END_BLOCK_OBJC_EXCEPTIONS;
395
396}
397
398void DictionaryLookup::showPopup(const DictionaryPopupInfo& dictionaryPopupInfo, NSView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
399{
400 showPopupOrCreateAnimationController(false, dictionaryPopupInfo, view, textIndicatorInstallationCallback, rootViewToViewConversionCallback, WTFMove(clearTextIndicator));
401}
402
403void DictionaryLookup::hidePopup()
404{
405 notImplemented();
406}
407
408#if PLATFORM(MAC)
409
410WKRevealController DictionaryLookup::animationControllerForPopup(const DictionaryPopupInfo& dictionaryPopupInfo, NSView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
411{
412 return showPopupOrCreateAnimationController(true, dictionaryPopupInfo, view, textIndicatorInstallationCallback, rootViewToViewConversionCallback, WTFMove(clearTextIndicator));
413}
414
415#endif // PLATFORM(MAC)
416
417#elif PLATFORM(IOS_FAMILY) // ENABLE(REVEAL)
418
419std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeForSelection(const VisibleSelection&)
420{
421 return { nullptr, nil };
422}
423
424std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeAtHitTestResult(const HitTestResult&)
425{
426 return { nullptr, nil };
427}
428
429#endif // ENABLE(REVEAL)
430
431} // namespace WebCore
432
433#endif // PLATFORM(COCOA)