blob: a23dc407a9ec0655fb67f7ac4e05797a817f9f25 [file] [log] [blame]
/*
* Copyright (C) 2014-2020 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 "DictionaryLookup.h"
#if PLATFORM(COCOA)
#if ENABLE(REVEAL)
#import "Document.h"
#import "Editing.h"
#import "FocusController.h"
#import "Frame.h"
#import "FrameSelection.h"
#import "GraphicsContextCG.h"
#import "HTMLConverter.h"
#import "HitTestResult.h"
#import "NotImplemented.h"
#import "Page.h"
#import "Range.h"
#import "RenderObject.h"
#import "TextIterator.h"
#import "VisiblePosition.h"
#import "VisibleSelection.h"
#import "VisibleUnits.h"
#import <pal/ios/UIKitSoftLink.h>
#import <pal/spi/cg/CoreGraphicsSPI.h>
#import <pal/spi/cocoa/RevealSPI.h>
#import <pal/spi/ios/UIKitSPI.h>
#import <wtf/BlockObjCExceptions.h>
#import <wtf/RefPtr.h>
#if PLATFORM(MAC)
#import <Quartz/Quartz.h>
#else
#import <PDFKit/PDFKit.h>
#endif
#if PLATFORM(MACCATALYST)
#import <UIKitMacHelper/UINSRevealController.h>
SOFT_LINK_PRIVATE_FRAMEWORK(UIKitMacHelper)
SOFT_LINK(UIKitMacHelper, UINSSharedRevealController, id<UINSRevealController>, (void), ())
#endif // PLATFORM(MACCATALYST)
#if ENABLE(REVEAL)
SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(Reveal)
SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(RevealCore)
SOFT_LINK_CLASS_OPTIONAL(Reveal, RVPresenter)
SOFT_LINK_CLASS_OPTIONAL(Reveal, RVPresentingContext)
SOFT_LINK_CLASS_OPTIONAL(RevealCore, RVItem)
SOFT_LINK_CLASS_OPTIONAL(RevealCore, RVSelection)
#endif
#if PLATFORM(MAC)
@interface WebRevealHighlight <RVPresenterHighlightDelegate> : NSObject {
@private
Function<void()> _clearTextIndicator;
}
@property (nonatomic, readonly) NSRect highlightRect;
@property (nonatomic, readonly) BOOL useDefaultHighlight;
@property (nonatomic, readonly) RetainPtr<NSAttributedString> attributedString;
- (instancetype)initWithHighlightRect:(NSRect)highlightRect useDefaultHighlight:(BOOL)useDefaultHighlight attributedString:(NSAttributedString *) attributedString;
- (void)setClearTextIndicator:(Function<void()>&&)clearTextIndicator;
@end
@implementation WebRevealHighlight
- (instancetype)initWithHighlightRect:(NSRect)highlightRect useDefaultHighlight:(BOOL)useDefaultHighlight attributedString:(NSAttributedString *) attributedString
{
if (!(self = [super init]))
return nil;
_highlightRect = highlightRect;
_useDefaultHighlight = useDefaultHighlight;
_attributedString = adoptNS([attributedString copy]);
return self;
}
- (void)setClearTextIndicator:(Function<void()>&&)clearTextIndicator
{
_clearTextIndicator = WTFMove(clearTextIndicator);
}
- (NSArray<NSValue *> *)revealContext:(RVPresentingContext *)context rectsForItem:(RVItem *)item
{
UNUSED_PARAM(context);
UNUSED_PARAM(item);
return @[[NSValue valueWithRect:self.highlightRect]];
}
- (void)revealContext:(RVPresentingContext *)context drawRectsForItem:(RVItem *)item
{
UNUSED_PARAM(item);
for (NSValue *rectVal in context.itemRectsInView) {
NSRect rect = rectVal.rectValue;
// Get current font attributes from the attributed string above, and add paragraph style attribute in order to center text.
RetainPtr<NSMutableDictionary> attributes = adoptNS([[NSMutableDictionary alloc] initWithDictionary:[self.attributedString fontAttributesInRange:NSMakeRange(0, [self.attributedString length])]]);
RetainPtr<NSMutableParagraphStyle> paragraph = adoptNS([[NSMutableParagraphStyle alloc] init]);
[paragraph setAlignment:NSTextAlignmentCenter];
[attributes setObject:paragraph.get() forKey:NSParagraphStyleAttributeName];
RetainPtr<NSAttributedString> string = adoptNS([[NSAttributedString alloc] initWithString:[self.attributedString string] attributes:attributes.get()]);
[string drawInRect:rect];
}
}
- (BOOL)revealContext:(RVPresentingContext *)context shouldUseDefaultHighlightForItem:(RVItem *)item
{
UNUSED_PARAM(context);
UNUSED_PARAM(item);
return self.useDefaultHighlight;
}
- (void)revealContext:(RVPresentingContext *)context stopHighlightingItem:(RVItem *)item
{
UNUSED_PARAM(context);
UNUSED_PARAM(item);
auto block = WTFMove(_clearTextIndicator);
if (block)
block();
}
@end
#elif PLATFORM(MACCATALYST) // PLATFORM(MAC)
@interface WebRevealHighlight <UIRVPresenterHighlightDelegate> : NSObject {
@private
RefPtr<WebCore::Image> _image;
CGRect _highlightRect;
BOOL _highlighting;
UIView *_view;
}
- (instancetype)initWithHighlightRect:(NSRect)highlightRect view:(UIView *)view image:(RefPtr<WebCore::Image>&&)image;
@end
@implementation WebRevealHighlight
- (instancetype)initWithHighlightRect:(NSRect)highlightRect view:(UIView *)view image:(RefPtr<WebCore::Image>&&)image
{
if (!(self = [super init]))
return nil;
_highlightRect = highlightRect;
_view = view;
_highlighting = NO;
_image = image;
return self;
}
- (void)setImage:(RefPtr<WebCore::Image>&&)image
{
_image = WTFMove(image);
}
- (NSArray<NSValue *> *)highlightRectsForItem:(RVItem *)item
{
UNUSED_PARAM(item);
return @[[NSValue valueWithCGRect:_highlightRect]];
}
- (void)startHighlightingItem:(RVItem *)item
{
UNUSED_PARAM(item);
_highlighting = YES;
}
- (void)highlightItem:(RVItem *)item withProgress:(CGFloat)progress
{
UNUSED_PARAM(item);
UNUSED_PARAM(progress);
}
- (void)completeHighlightingItem:(RVItem *)item
{
UNUSED_PARAM(item);
}
- (void)stopHighlightingItem:(RVItem *)item
{
UNUSED_PARAM(item);
_highlighting = NO;
}
- (void)highlightRangeChangedForItem:(RVItem *)item
{
UNUSED_PARAM(item);
}
- (BOOL)highlighting
{
return _highlighting;
}
- (void)drawHighlightContentForItem:(RVItem *)item context:(CGContextRef)context
{
NSArray <NSValue *> *rects = [self highlightRectsForItem:item];
if (!rects.count)
return;
CGRect highlightRect = rects.firstObject.CGRectValue;
for (NSValue *rect in rects)
highlightRect = CGRectUnion(highlightRect, rect.CGRectValue);
highlightRect = [_view convertRect:highlightRect fromView:nil];
WebCore::CGContextStateSaver saveState(context);
CGAffineTransform contextTransform = CGContextGetCTM(context);
CGFloat backingScale = contextTransform.a;
CGFloat macCatalystScaleFactor = [PAL::getUIApplicationClass() sharedApplication]._iOSMacScale;
CGAffineTransform transform = CGAffineTransformMakeScale(macCatalystScaleFactor * backingScale, macCatalystScaleFactor * backingScale);
CGContextSetCTM(context, transform);
for (NSValue *v in rects) {
CGRect imgSrcRect = [_view convertRect:v.CGRectValue fromView:nil];
RetainPtr<CGImageRef> imageRef = _image->nativeImage();
CGRect origin = CGRectMake(imgSrcRect.origin.x - highlightRect.origin.x, imgSrcRect.origin.y - highlightRect.origin.y, highlightRect.size.width, highlightRect.size.height);
CGContextDrawImage(context, origin, imageRef.get());
}
}
@end
#endif // PLATFORM(MACCATALYST)
#endif // ENABLE(REVEAL)
namespace WebCore {
#if ENABLE(REVEAL)
std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeForSelection(const VisibleSelection& selection)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS;
if (!RevealLibrary() || !RevealCoreLibrary() || !getRVItemClass())
return { nullptr, nil };
if (!selection.toNormalizedRange())
return { nullptr, nil };
// Since we already have the range we want, we just need to grab the returned options.
auto selectionStart = selection.visibleStart();
auto selectionEnd = selection.visibleEnd();
// As context, we are going to use the surrounding paragraphs of text.
auto paragraphStart = startOfParagraph(selectionStart);
auto paragraphEnd = endOfParagraph(selectionEnd);
if (paragraphStart.isNull() || paragraphEnd.isNull())
return { nullptr, nil };
auto lengthToSelectionStart = characterCount({ *makeBoundaryPoint(paragraphStart), *makeBoundaryPoint(selectionStart) });
auto selectionCharacterCount = characterCount({ *makeBoundaryPoint(selectionStart), *makeBoundaryPoint(selectionEnd) });
NSRange rangeToPass = NSMakeRange(lengthToSelectionStart, selectionCharacterCount);
RefPtr<Range> fullCharacterRange = makeRange(paragraphStart, paragraphEnd);
String itemString = plainText(*fullCharacterRange);
NSRange highlightRange = adoptNS([allocRVItemInstance() initWithText:itemString selectedRange:rangeToPass]).get().highlightRange;
return { createLiveRange(resolveCharacterRange(*fullCharacterRange, highlightRange)), nil };
END_BLOCK_OBJC_EXCEPTIONS;
return { nullptr, nil };
}
std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeAtHitTestResult(const HitTestResult& hitTestResult)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS;
if (!RevealLibrary() || !RevealCoreLibrary() || !getRVItemClass())
return { nullptr, nil };
auto* node = hitTestResult.innerNonSharedNode();
if (!node || !node->renderer())
return { nullptr, nil };
auto* frame = node->document().frame();
if (!frame)
return { nullptr, nil };
// Don't do anything if there is no character at the point.
auto framePoint = hitTestResult.roundedPointInInnerNodeFrame();
if (!frame->rangeForPoint(framePoint))
return { nullptr, nil };
auto position = frame->visiblePositionForPoint(framePoint);
if (position.isNull())
position = firstPositionInOrBeforeNode(node);
auto selection = frame->page()->focusController().focusedOrMainFrame().selection().selection();
NSRange selectionRange;
NSUInteger hitIndex;
RefPtr<Range> fullCharacterRange;
if (selection.selectionType() == VisibleSelection::RangeSelection) {
auto selectionStart = selection.visibleStart();
auto selectionEnd = selection.visibleEnd();
// As context, we are going to use the surrounding paragraphs of text.
auto paragraphStart = startOfParagraph(selectionStart);
auto paragraphEnd = endOfParagraph(selectionEnd);
fullCharacterRange = makeRange(paragraphStart, paragraphEnd);
if (!fullCharacterRange)
return { nullptr, nil };
selectionRange = NSMakeRange(characterCount({ *makeBoundaryPoint(paragraphStart), *makeBoundaryPoint(selectionStart) }),
characterCount({ *makeBoundaryPoint(selectionStart), *makeBoundaryPoint(selectionEnd) }));
hitIndex = characterCount({ *makeBoundaryPoint(paragraphStart), *makeBoundaryPoint(position) });
} else {
VisibleSelection selectionAccountingForLineRules { position };
selectionAccountingForLineRules.expandUsingGranularity(WordGranularity);
position = selectionAccountingForLineRules.start();
// As context, we are going to use 250 characters of text before and after the point.
fullCharacterRange = rangeExpandedAroundPositionByCharacters(position, 250);
if (!fullCharacterRange)
return { nullptr, nil };
selectionRange = NSMakeRange(NSNotFound, 0);
hitIndex = characterCount({ *makeBoundaryPoint(fullCharacterRange->startPosition()), *makeBoundaryPoint(position) });
}
NSRange selectedRange = [getRVSelectionClass() revealRangeAtIndex:hitIndex selectedRanges:@[[NSValue valueWithRange:selectionRange]] shouldUpdateSelection:nil];
String itemString = plainText(*fullCharacterRange);
auto highlightRange = adoptNS([allocRVItemInstance() initWithText:itemString selectedRange:selectedRange]).get().highlightRange;
if (highlightRange.location == NSNotFound || !highlightRange.length)
return { nullptr, nil };
return { createLiveRange(resolveCharacterRange(*fullCharacterRange, highlightRange)), nil };
END_BLOCK_OBJC_EXCEPTIONS;
return { nullptr, nil };
}
static void expandSelectionByCharacters(PDFSelection *selection, NSInteger numberOfCharactersToExpand, NSInteger& charactersAddedBeforeStart, NSInteger& charactersAddedAfterEnd)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS;
size_t originalLength = selection.string.length;
[selection extendSelectionAtStart:numberOfCharactersToExpand];
charactersAddedBeforeStart = selection.string.length - originalLength;
[selection extendSelectionAtEnd:numberOfCharactersToExpand];
charactersAddedAfterEnd = selection.string.length - originalLength - charactersAddedBeforeStart;
END_BLOCK_OBJC_EXCEPTIONS;
}
std::tuple<NSString *, NSDictionary *> DictionaryLookup::stringForPDFSelection(PDFSelection *selection)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS;
if (!RevealLibrary() || !RevealCoreLibrary() || !getRVItemClass())
return { nullptr, nil };
// Don't do anything if there is no character at the point.
if (!selection || !selection.string.length)
return { @"", nil };
RetainPtr<PDFSelection> selectionForLookup = adoptNS([selection copy]);
// As context, we are going to use 250 characters of text before and after the point.
auto originalLength = [selectionForLookup string].length;
NSInteger charactersAddedBeforeStart = 0;
NSInteger charactersAddedAfterEnd = 0;
expandSelectionByCharacters(selectionForLookup.get(), 250, charactersAddedBeforeStart, charactersAddedAfterEnd);
auto fullPlainTextString = [selectionForLookup string];
auto rangeToPass = NSMakeRange(charactersAddedBeforeStart, 0);
RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:fullPlainTextString selectedRange:rangeToPass]);
NSRange extractedRange = item.get().highlightRange;
if (extractedRange.location == NSNotFound)
return { selection.string, nil };
NSInteger lookupAddedBefore = rangeToPass.location - extractedRange.location;
NSInteger lookupAddedAfter = (extractedRange.location + extractedRange.length) - (rangeToPass.location + originalLength);
[selection extendSelectionAtStart:lookupAddedBefore];
[selection extendSelectionAtEnd:lookupAddedAfter];
ASSERT([selection.string isEqualToString:[fullPlainTextString substringWithRange:extractedRange]]);
return { selection.string, nil };
END_BLOCK_OBJC_EXCEPTIONS;
return { @"", nil };
}
static WKRevealController showPopupOrCreateAnimationController(bool createAnimationController, const DictionaryPopupInfo& dictionaryPopupInfo, RevealView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
{
BEGIN_BLOCK_OBJC_EXCEPTIONS;
#if PLATFORM(MAC)
if (!RevealLibrary() || !RevealCoreLibrary() || !getRVItemClass() || !getRVPresenterClass())
return nil;
RetainPtr<NSMutableDictionary> mutableOptions = adoptNS([[NSMutableDictionary alloc] init]);
if (NSDictionary *options = dictionaryPopupInfo.options.get())
[mutableOptions addEntriesFromDictionary:options];
auto textIndicator = TextIndicator::create(dictionaryPopupInfo.textIndicator);
RetainPtr<RVPresenter> presenter = adoptNS([allocRVPresenterInstance() init]);
NSRect highlightRect;
NSPoint pointerLocation;
if (textIndicator.get().contentImage()) {
textIndicatorInstallationCallback(textIndicator.get());
FloatRect firstTextRectInViewCoordinates = textIndicator.get().textRectsInBoundingRectCoordinates()[0];
FloatRect textBoundingRectInViewCoordinates = textIndicator.get().textBoundingRectInRootViewCoordinates();
FloatRect selectionBoundingRectInViewCoordinates = textIndicator.get().selectionRectInRootViewCoordinates();
if (rootViewToViewConversionCallback) {
textBoundingRectInViewCoordinates = rootViewToViewConversionCallback(textBoundingRectInViewCoordinates);
selectionBoundingRectInViewCoordinates = rootViewToViewConversionCallback(selectionBoundingRectInViewCoordinates);
}
firstTextRectInViewCoordinates.moveBy(textBoundingRectInViewCoordinates.location());
highlightRect = selectionBoundingRectInViewCoordinates;
pointerLocation = firstTextRectInViewCoordinates.location();
} else {
NSPoint textBaselineOrigin = dictionaryPopupInfo.origin;
highlightRect = textIndicator->selectionRectInRootViewCoordinates();
pointerLocation = [view convertPoint:textBaselineOrigin toView:nil];
}
RetainPtr<WebRevealHighlight> webHighlight = adoptNS([[WebRevealHighlight alloc] initWithHighlightRect: highlightRect useDefaultHighlight:!textIndicator.get().contentImage() attributedString:dictionaryPopupInfo.attributedString.get()]);
RetainPtr<RVPresentingContext> context = adoptNS([allocRVPresentingContextInstance() initWithPointerLocationInView:pointerLocation inView:view highlightDelegate:(id<RVPresenterHighlightDelegate>) webHighlight.get()]);
RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:dictionaryPopupInfo.attributedString.get().string selectedRange:NSMakeRange(0, dictionaryPopupInfo.attributedString.get().string.length)]);
[webHighlight setClearTextIndicator:[webHighlight = WTFMove(webHighlight), clearTextIndicator = WTFMove(clearTextIndicator)] {
if (clearTextIndicator)
clearTextIndicator();
}];
if (createAnimationController)
return [presenter animationControllerForItem:item.get() documentContext:nil presentingContext:context.get() options:nil];
[presenter revealItem:item.get() documentContext:nil presentingContext:context.get() options:@{ @"forceLookup": @YES }];
return nil;
#elif PLATFORM(MACCATALYST)
UNUSED_PARAM(textIndicatorInstallationCallback);
UNUSED_PARAM(rootViewToViewConversionCallback);
UNUSED_PARAM(clearTextIndicator);
ASSERT_UNUSED(createAnimationController, !createAnimationController);
auto textIndicator = TextIndicator::create(dictionaryPopupInfo.textIndicator);
RetainPtr<WebRevealHighlight> webHighlight = adoptNS([[WebRevealHighlight alloc] initWithHighlightRect:[view convertRect:textIndicator->selectionRectInRootViewCoordinates() toView:nil] view:view image:textIndicator->contentImage()]);
RetainPtr<RVItem> item = adoptNS([allocRVItemInstance() initWithText:dictionaryPopupInfo.attributedString.get().string selectedRange:NSMakeRange(0, dictionaryPopupInfo.attributedString.get().string.length)]);
[UINSSharedRevealController() revealItem:item.get() locationInWindow:dictionaryPopupInfo.origin window:view.window highlighter:(id<UIRVPresenterHighlightDelegate>) webHighlight.get()];
return nil;
#else // PLATFORM(IOS_FAMILY)
UNUSED_PARAM(createAnimationController);
UNUSED_PARAM(dictionaryPopupInfo);
UNUSED_PARAM(view);
UNUSED_PARAM(textIndicatorInstallationCallback);
UNUSED_PARAM(rootViewToViewConversionCallback);
UNUSED_PARAM(clearTextIndicator);
return nil;
#endif // PLATFORM(IOS_FAMILY)
END_BLOCK_OBJC_EXCEPTIONS;
}
void DictionaryLookup::showPopup(const DictionaryPopupInfo& dictionaryPopupInfo, RevealView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
{
showPopupOrCreateAnimationController(false, dictionaryPopupInfo, view, textIndicatorInstallationCallback, rootViewToViewConversionCallback, WTFMove(clearTextIndicator));
}
void DictionaryLookup::hidePopup()
{
notImplemented();
}
#if PLATFORM(MAC)
WKRevealController DictionaryLookup::animationControllerForPopup(const DictionaryPopupInfo& dictionaryPopupInfo, NSView *view, const WTF::Function<void(TextIndicator&)>& textIndicatorInstallationCallback, const WTF::Function<FloatRect(FloatRect)>& rootViewToViewConversionCallback, WTF::Function<void()>&& clearTextIndicator)
{
return showPopupOrCreateAnimationController(true, dictionaryPopupInfo, view, textIndicatorInstallationCallback, rootViewToViewConversionCallback, WTFMove(clearTextIndicator));
}
#endif // PLATFORM(MAC)
#elif PLATFORM(IOS_FAMILY) // ENABLE(REVEAL)
std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeForSelection(const VisibleSelection&)
{
return { nullptr, nil };
}
std::tuple<RefPtr<Range>, NSDictionary *> DictionaryLookup::rangeAtHitTestResult(const HitTestResult&)
{
return { nullptr, nil };
}
#endif // ENABLE(REVEAL)
} // namespace WebCore
#endif // PLATFORM(COCOA)