| /* |
| * Copyright (C) 2006-2016 Apple Inc. All rights reserved. |
| * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| * |
| * 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. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "WebEditorClient.h" |
| |
| #import "DOMCSSStyleDeclarationInternal.h" |
| #import "DOMDocumentFragmentInternal.h" |
| #import "DOMDocumentInternal.h" |
| #import "DOMHTMLElementInternal.h" |
| #import "DOMHTMLInputElementInternal.h" |
| #import "DOMHTMLTextAreaElementInternal.h" |
| #import "DOMNodeInternal.h" |
| #import "DOMRangeInternal.h" |
| #import "WebArchive.h" |
| #import "WebCreateFragmentInternal.h" |
| #import "WebDataSourceInternal.h" |
| #import "WebDelegateImplementationCaching.h" |
| #import "WebDocument.h" |
| #import "WebEditingDelegatePrivate.h" |
| #import "WebFormDelegate.h" |
| #import "WebFrameInternal.h" |
| #import "WebFrameView.h" |
| #import "WebHTMLViewInternal.h" |
| #import "WebKitLogging.h" |
| #import "WebKitVersionChecks.h" |
| #import "WebLocalizableStringsInternal.h" |
| #import "WebNSURLExtras.h" |
| #import "WebResourceInternal.h" |
| #import "WebViewInternal.h" |
| #import <JavaScriptCore/InitializeThreading.h> |
| #import <WebCore/ArchiveResource.h> |
| #import <WebCore/Document.h> |
| #import <WebCore/DocumentFragment.h> |
| #import <WebCore/Editor.h> |
| #import <WebCore/Event.h> |
| #import <WebCore/FloatQuad.h> |
| #import <WebCore/Frame.h> |
| #import <WebCore/FrameView.h> |
| #import <WebCore/HTMLInputElement.h> |
| #import <WebCore/HTMLNames.h> |
| #import <WebCore/HTMLTextAreaElement.h> |
| #import <WebCore/KeyboardEvent.h> |
| #import <WebCore/LegacyWebArchive.h> |
| #import <WebCore/Page.h> |
| #import <WebCore/PlatformKeyboardEvent.h> |
| #import <WebCore/RuntimeEnabledFeatures.h> |
| #import <WebCore/Settings.h> |
| #import <WebCore/SpellChecker.h> |
| #import <WebCore/StyleProperties.h> |
| #import <WebCore/TextIterator.h> |
| #import <WebCore/UndoStep.h> |
| #import <WebCore/UserTypingGestureIndicator.h> |
| #import <WebCore/VisibleUnits.h> |
| #import <WebCore/WebContentReader.h> |
| #import <WebCore/WebCoreObjCExtras.h> |
| #import <pal/spi/mac/NSSpellCheckerSPI.h> |
| #import <wtf/MainThread.h> |
| #import <wtf/RefPtr.h> |
| #import <wtf/RunLoop.h> |
| #import <wtf/text/WTFString.h> |
| |
| #if PLATFORM(IOS_FAMILY) |
| #import <WebCore/WebCoreThreadMessage.h> |
| #import "DOMElementInternal.h" |
| #import "WebFrameView.h" |
| #import "WebUIKitDelegate.h" |
| #endif |
| |
| using namespace WebCore; |
| |
| using namespace HTMLNames; |
| |
| #if !PLATFORM(IOS_FAMILY) |
| @interface NSSpellChecker (WebNSSpellCheckerDetails) |
| - (NSString *)languageForWordRange:(NSRange)range inString:(NSString *)string orthography:(NSOrthography *)orthography; |
| @end |
| #endif |
| |
| #if (PLATFORM(IOS_FAMILY) && __IPHONE_OS_VERSION_MIN_REQUIRED < 110000) |
| @interface NSAttributedString (WebNSAttributedStringDetails) |
| - (DOMDocumentFragment *)_documentFromRange:(NSRange)range document:(DOMDocument *)document documentAttributes:(NSDictionary *)attributes subresources:(NSArray **)subresources; |
| @end |
| #endif |
| |
| static WebViewInsertAction kit(EditorInsertAction action) |
| { |
| switch (action) { |
| case EditorInsertAction::Typed: |
| return WebViewInsertActionTyped; |
| case EditorInsertAction::Pasted: |
| return WebViewInsertActionPasted; |
| case EditorInsertAction::Dropped: |
| return WebViewInsertActionDropped; |
| } |
| } |
| |
| @interface WebUndoStep : NSObject |
| { |
| RefPtr<UndoStep> m_step; |
| } |
| |
| + (WebUndoStep *)stepWithUndoStep:(Ref<UndoStep>&&)step; |
| - (UndoStep&)step; |
| |
| @end |
| |
| @implementation WebUndoStep |
| |
| + (void)initialize |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| JSC::initializeThreading(); |
| WTF::initializeMainThreadToProcessMainThread(); |
| RunLoop::initializeMainRunLoop(); |
| #endif |
| } |
| |
| - (id)initWithUndoStep:(Ref<UndoStep>&&)step |
| { |
| self = [super init]; |
| if (!self) |
| return nil; |
| m_step = WTFMove(step); |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| if (WebCoreObjCScheduleDeallocateOnMainThread([WebUndoStep class], self)) |
| return; |
| |
| [super dealloc]; |
| } |
| |
| + (WebUndoStep *)stepWithUndoStep:(Ref<UndoStep>&&)step |
| { |
| return [[[WebUndoStep alloc] initWithUndoStep:WTFMove(step)] autorelease]; |
| } |
| |
| - (UndoStep&)step |
| { |
| return *m_step; |
| } |
| |
| @end |
| |
| @interface WebEditorUndoTarget : NSObject |
| |
| - (void)undoEditing:(id)arg; |
| - (void)redoEditing:(id)arg; |
| |
| @end |
| |
| @implementation WebEditorUndoTarget |
| |
| - (void)undoEditing:(id)arg |
| { |
| ASSERT([arg isKindOfClass:[WebUndoStep class]]); |
| [arg step].unapply(); |
| } |
| |
| - (void)redoEditing:(id)arg |
| { |
| ASSERT([arg isKindOfClass:[WebUndoStep class]]); |
| [arg step].reapply(); |
| } |
| |
| @end |
| |
| WebEditorClient::WebEditorClient(WebView *webView) |
| : m_webView(webView) |
| , m_undoTarget(adoptNS([[WebEditorUndoTarget alloc] init])) |
| { |
| } |
| |
| WebEditorClient::~WebEditorClient() |
| { |
| } |
| |
| bool WebEditorClient::isContinuousSpellCheckingEnabled() |
| { |
| return [m_webView isContinuousSpellCheckingEnabled]; |
| } |
| |
| void WebEditorClient::toggleContinuousSpellChecking() |
| { |
| [m_webView toggleContinuousSpellChecking:nil]; |
| } |
| |
| #if !PLATFORM(IOS_FAMILY) |
| |
| bool WebEditorClient::isGrammarCheckingEnabled() |
| { |
| return [m_webView isGrammarCheckingEnabled]; |
| } |
| |
| void WebEditorClient::toggleGrammarChecking() |
| { |
| [m_webView toggleGrammarChecking:nil]; |
| } |
| |
| int WebEditorClient::spellCheckerDocumentTag() |
| { |
| return [m_webView spellCheckerDocumentTag]; |
| } |
| |
| #endif |
| |
| bool WebEditorClient::shouldDeleteRange(Range* range) |
| { |
| return [[m_webView _editingDelegateForwarder] webView:m_webView |
| shouldDeleteDOMRange:kit(range)]; |
| } |
| |
| bool WebEditorClient::smartInsertDeleteEnabled() |
| { |
| Page* page = [m_webView page]; |
| if (!page) |
| return false; |
| return page->settings().smartInsertDeleteEnabled(); |
| } |
| |
| bool WebEditorClient::isSelectTrailingWhitespaceEnabled() const |
| { |
| Page* page = [m_webView page]; |
| if (!page) |
| return false; |
| return page->settings().selectTrailingWhitespaceEnabled(); |
| } |
| |
| bool WebEditorClient::shouldApplyStyle(StyleProperties* style, Range* range) |
| { |
| Ref<MutableStyleProperties> mutableStyle(style->isMutable() ? Ref<MutableStyleProperties>(static_cast<MutableStyleProperties&>(*style)) : style->mutableCopy()); |
| return [[m_webView _editingDelegateForwarder] webView:m_webView |
| shouldApplyStyle:kit(&mutableStyle->ensureCSSStyleDeclaration()) toElementsInDOMRange:kit(range)]; |
| } |
| |
| static void updateFontPanel(WebView *webView) |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| NSView <WebDocumentView> *view = [[[webView selectedFrame] frameView] documentView]; |
| if ([view isKindOfClass:[WebHTMLView class]]) |
| [(WebHTMLView *)view _updateFontPanel]; |
| #else |
| UNUSED_PARAM(webView); |
| #endif |
| } |
| |
| void WebEditorClient::didApplyStyle() |
| { |
| updateFontPanel(m_webView); |
| } |
| |
| bool WebEditorClient::shouldMoveRangeAfterDelete(Range* range, Range* rangeToBeReplaced) |
| { |
| return [[m_webView _editingDelegateForwarder] webView:m_webView |
| shouldMoveRangeAfterDelete:kit(range) replacingRange:kit(rangeToBeReplaced)]; |
| } |
| |
| bool WebEditorClient::shouldBeginEditing(Range* range) |
| { |
| return [[m_webView _editingDelegateForwarder] webView:m_webView |
| shouldBeginEditingInDOMRange:kit(range)]; |
| |
| return false; |
| } |
| |
| bool WebEditorClient::shouldEndEditing(Range* range) |
| { |
| return [[m_webView _editingDelegateForwarder] webView:m_webView |
| shouldEndEditingInDOMRange:kit(range)]; |
| } |
| |
| bool WebEditorClient::shouldInsertText(const String& text, Range* range, EditorInsertAction action) |
| { |
| WebView* webView = m_webView; |
| return [[webView _editingDelegateForwarder] webView:webView shouldInsertText:text replacingDOMRange:kit(range) givenAction:kit(action)]; |
| } |
| |
| bool WebEditorClient::shouldChangeSelectedRange(Range* fromRange, Range* toRange, EAffinity selectionAffinity, bool stillSelecting) |
| { |
| return [m_webView _shouldChangeSelectedDOMRange:kit(fromRange) toDOMRange:kit(toRange) affinity:kit(selectionAffinity) stillSelecting:stillSelecting]; |
| } |
| |
| void WebEditorClient::didBeginEditing() |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebViewDidBeginEditingNotification object:m_webView]; |
| #else |
| WebThreadPostNotification(WebViewDidBeginEditingNotification, m_webView, nil); |
| #endif |
| } |
| |
| #if PLATFORM(IOS_FAMILY) |
| |
| void WebEditorClient::startDelayingAndCoalescingContentChangeNotifications() |
| { |
| m_delayingContentChangeNotifications = true; |
| } |
| |
| void WebEditorClient::stopDelayingAndCoalescingContentChangeNotifications() |
| { |
| m_delayingContentChangeNotifications = false; |
| |
| if (m_hasDelayedContentChangeNotification) |
| this->respondToChangedContents(); |
| |
| m_hasDelayedContentChangeNotification = false; |
| } |
| |
| #endif |
| |
| void WebEditorClient::respondToChangedContents() |
| { |
| updateFontPanel(m_webView); |
| #if !PLATFORM(IOS_FAMILY) |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebViewDidChangeNotification object:m_webView]; |
| #else |
| if (m_delayingContentChangeNotifications) { |
| m_hasDelayedContentChangeNotification = true; |
| } else { |
| WebThreadPostNotification(WebViewDidChangeNotification, m_webView, nil); |
| } |
| #endif |
| } |
| |
| void WebEditorClient::respondToChangedSelection(Frame* frame) |
| { |
| if (frame->editor().isGettingDictionaryPopupInfo()) |
| return; |
| |
| NSView<WebDocumentView> *documentView = [[kit(frame) frameView] documentView]; |
| if ([documentView isKindOfClass:[WebHTMLView class]]) { |
| [(WebHTMLView *)documentView _selectionChanged]; |
| [m_webView updateTouchBar]; |
| m_lastEditorStateWasContentEditable = [(WebHTMLView *)documentView _isEditable] ? EditorStateIsContentEditable::Yes : EditorStateIsContentEditable::No; |
| } |
| |
| #if !PLATFORM(IOS_FAMILY) |
| // FIXME: This quirk is needed due to <rdar://problem/5009625> - We can phase it out once Aperture can adopt the new behavior on their end |
| if (!WebKitLinkedOnOrAfter(WEBKIT_FIRST_VERSION_WITHOUT_APERTURE_QUIRK) && [[[NSBundle mainBundle] bundleIdentifier] isEqualToString:@"com.apple.Aperture"]) |
| return; |
| |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebViewDidChangeSelectionNotification object:m_webView]; |
| #else |
| // Selection can be changed while deallocating down the WebView / Frame / Editor. Do not post in that case because it's already too late |
| // for the NSInvocation to retain the WebView. |
| if (![m_webView _isClosing]) |
| WebThreadPostNotification(WebViewDidChangeSelectionNotification, m_webView, nil); |
| #endif |
| |
| #if PLATFORM(MAC) |
| if (frame->editor().canEdit()) |
| requestCandidatesForSelection(frame->selection().selection()); |
| #endif |
| } |
| |
| void WebEditorClient::discardedComposition(Frame*) |
| { |
| // The effects of this function are currently achieved via -[WebHTMLView _updateSelectionForInputManager]. |
| } |
| |
| void WebEditorClient::canceledComposition() |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| [[NSTextInputContext currentInputContext] discardMarkedText]; |
| #endif |
| } |
| |
| void WebEditorClient::didEndEditing() |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebViewDidEndEditingNotification object:m_webView]; |
| #else |
| WebThreadPostNotification(WebViewDidEndEditingNotification, m_webView, nil); |
| #endif |
| } |
| |
| void WebEditorClient::didWriteSelectionToPasteboard() |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| [[m_webView _editingDelegateForwarder] webView:m_webView didWriteSelectionToPasteboard:[NSPasteboard generalPasteboard]]; |
| #endif |
| } |
| |
| void WebEditorClient::willWriteSelectionToPasteboard(WebCore::Range*) |
| { |
| // Not implemented WebKit, only WebKit2. |
| } |
| |
| void WebEditorClient::getClientPasteboardDataForRange(WebCore::Range*, Vector<String>& pasteboardTypes, Vector<RefPtr<WebCore::SharedBuffer>>& pasteboardData) |
| { |
| // Not implemented WebKit, only WebKit2. |
| } |
| |
| #if (PLATFORM(IOS_FAMILY) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000) || PLATFORM(MAC) |
| |
| // FIXME: Remove both this stub and the real version of this function below once we don't need the real version on any supported platform. |
| // This stub is not used outside WebKit, but it's here so we won't get a linker error. |
| __attribute__((__noreturn__)) |
| void _WebCreateFragment(Document&, NSAttributedString *, FragmentAndResources&) |
| { |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| #else |
| |
| static NSDictionary *attributesForAttributedStringConversion() |
| { |
| // This function needs to be kept in sync with identically named one in WebCore, which is used on newer OS versions. |
| NSMutableArray *excludedElements = [[NSMutableArray alloc] initWithObjects: |
| // Omit style since we want style to be inline so the fragment can be easily inserted. |
| @"style", |
| // Omit xml so the result is not XHTML. |
| @"xml", |
| // Omit tags that will get stripped when converted to a fragment anyway. |
| @"doctype", @"html", @"head", @"body", |
| // Omit deprecated tags. |
| @"applet", @"basefont", @"center", @"dir", @"font", @"menu", @"s", @"strike", @"u", |
| #if !ENABLE(ATTACHMENT_ELEMENT) |
| // Omit object so no file attachments are part of the fragment. |
| @"object", |
| #endif |
| nil]; |
| |
| #if ENABLE(ATTACHMENT_ELEMENT) |
| if (!RuntimeEnabledFeatures::sharedFeatures().attachmentElementEnabled()) |
| [excludedElements addObject:@"object"]; |
| #endif |
| |
| #if PLATFORM(IOS_FAMILY) |
| static NSString * const NSExcludedElementsDocumentAttribute = @"ExcludedElements"; |
| #endif |
| |
| NSDictionary *dictionary = [NSDictionary dictionaryWithObject:excludedElements forKey:NSExcludedElementsDocumentAttribute]; |
| |
| [excludedElements release]; |
| |
| return dictionary; |
| } |
| |
| void _WebCreateFragment(Document& document, NSAttributedString *string, FragmentAndResources& result) |
| { |
| static NSDictionary *documentAttributes = [attributesForAttributedStringConversion() retain]; |
| NSArray *subresources; |
| DOMDocumentFragment* fragment = [string _documentFromRange:NSMakeRange(0, [string length]) |
| document:kit(&document) documentAttributes:documentAttributes subresources:&subresources]; |
| result.fragment = core(fragment); |
| for (WebResource* resource in subresources) |
| result.resources.append([resource _coreResource]); |
| } |
| |
| #endif |
| |
| void WebEditorClient::setInsertionPasteboard(const String& pasteboardName) |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| NSPasteboard *pasteboard = pasteboardName.isEmpty() ? nil : [NSPasteboard pasteboardWithName:pasteboardName]; |
| [m_webView _setInsertionPasteboard:pasteboard]; |
| #endif |
| } |
| |
| #if USE(APPKIT) |
| |
| void WebEditorClient::uppercaseWord() |
| { |
| [m_webView uppercaseWord:nil]; |
| } |
| |
| void WebEditorClient::lowercaseWord() |
| { |
| [m_webView lowercaseWord:nil]; |
| } |
| |
| void WebEditorClient::capitalizeWord() |
| { |
| [m_webView capitalizeWord:nil]; |
| } |
| |
| #endif |
| |
| #if USE(AUTOMATIC_TEXT_REPLACEMENT) |
| |
| void WebEditorClient::showSubstitutionsPanel(bool show) |
| { |
| NSPanel *spellingPanel = [[NSSpellChecker sharedSpellChecker] substitutionsPanel]; |
| if (show) |
| [spellingPanel orderFront:nil]; |
| else |
| [spellingPanel orderOut:nil]; |
| } |
| |
| bool WebEditorClient::substitutionsPanelIsShowing() |
| { |
| return [[[NSSpellChecker sharedSpellChecker] substitutionsPanel] isVisible]; |
| } |
| |
| void WebEditorClient::toggleSmartInsertDelete() |
| { |
| [m_webView toggleSmartInsertDelete:nil]; |
| } |
| |
| bool WebEditorClient::isAutomaticQuoteSubstitutionEnabled() |
| { |
| return [m_webView isAutomaticQuoteSubstitutionEnabled]; |
| } |
| |
| void WebEditorClient::toggleAutomaticQuoteSubstitution() |
| { |
| [m_webView toggleAutomaticQuoteSubstitution:nil]; |
| } |
| |
| bool WebEditorClient::isAutomaticLinkDetectionEnabled() |
| { |
| return [m_webView isAutomaticLinkDetectionEnabled]; |
| } |
| |
| void WebEditorClient::toggleAutomaticLinkDetection() |
| { |
| [m_webView toggleAutomaticLinkDetection:nil]; |
| } |
| |
| bool WebEditorClient::isAutomaticDashSubstitutionEnabled() |
| { |
| return [m_webView isAutomaticDashSubstitutionEnabled]; |
| } |
| |
| void WebEditorClient::toggleAutomaticDashSubstitution() |
| { |
| [m_webView toggleAutomaticDashSubstitution:nil]; |
| } |
| |
| bool WebEditorClient::isAutomaticTextReplacementEnabled() |
| { |
| return [m_webView isAutomaticTextReplacementEnabled]; |
| } |
| |
| void WebEditorClient::toggleAutomaticTextReplacement() |
| { |
| [m_webView toggleAutomaticTextReplacement:nil]; |
| } |
| |
| bool WebEditorClient::isAutomaticSpellingCorrectionEnabled() |
| { |
| return [m_webView isAutomaticSpellingCorrectionEnabled]; |
| } |
| |
| void WebEditorClient::toggleAutomaticSpellingCorrection() |
| { |
| [m_webView toggleAutomaticSpellingCorrection:nil]; |
| } |
| |
| #endif // USE(AUTOMATIC_TEXT_REPLACEMENT) |
| |
| bool WebEditorClient::shouldInsertNode(Node *node, Range* replacingRange, EditorInsertAction givenAction) |
| { |
| return [[m_webView _editingDelegateForwarder] webView:m_webView shouldInsertNode:kit(node) replacingDOMRange:kit(replacingRange) givenAction:(WebViewInsertAction)givenAction]; |
| } |
| |
| void WebEditorClient::registerUndoOrRedoStep(UndoStep& step, bool isRedo) |
| { |
| NSUndoManager *undoManager = [m_webView undoManager]; |
| |
| #if PLATFORM(IOS_FAMILY) |
| // While we are undoing, we shouldn't be asked to register another Undo operation, we shouldn't even be touching the DOM. |
| // But just in case this happens, return to avoid putting the undo manager into an inconsistent state. |
| // Same for being asked to register a Redo operation in the midst of another Redo. |
| if (([undoManager isUndoing] && !isRedo) || ([undoManager isRedoing] && isRedo)) |
| return; |
| #endif |
| |
| NSString *actionName = step.label(); |
| [undoManager registerUndoWithTarget:m_undoTarget.get() selector:(isRedo ? @selector(redoEditing:) : @selector(undoEditing:)) object:[WebUndoStep stepWithUndoStep:step]]; |
| if (actionName) |
| [undoManager setActionName:actionName]; |
| m_haveUndoRedoOperations = YES; |
| } |
| |
| void WebEditorClient::updateEditorStateAfterLayoutIfEditabilityChanged() |
| { |
| // FIXME: We should update EditorStateIsContentEditable to track whether the state is richly |
| // editable or plainttext-only. |
| if (m_lastEditorStateWasContentEditable == EditorStateIsContentEditable::Unset) |
| return; |
| |
| Frame* frame = core([m_webView _selectedOrMainFrame]); |
| if (!frame) |
| return; |
| |
| NSView<WebDocumentView> *documentView = [[kit(frame) frameView] documentView]; |
| if (![documentView isKindOfClass:[WebHTMLView class]]) |
| return; |
| |
| EditorStateIsContentEditable editorStateIsContentEditable = [(WebHTMLView *)documentView _isEditable] ? EditorStateIsContentEditable::Yes : EditorStateIsContentEditable::No; |
| if (m_lastEditorStateWasContentEditable != editorStateIsContentEditable) |
| [m_webView updateTouchBar]; |
| } |
| |
| void WebEditorClient::registerUndoStep(UndoStep& command) |
| { |
| registerUndoOrRedoStep(command, false); |
| } |
| |
| void WebEditorClient::registerRedoStep(UndoStep& command) |
| { |
| registerUndoOrRedoStep(command, true); |
| } |
| |
| void WebEditorClient::clearUndoRedoOperations() |
| { |
| if (m_haveUndoRedoOperations) { |
| // workaround for <rdar://problem/4645507> NSUndoManager dies |
| // with uncaught exception when undo items cleared while |
| // groups are open |
| NSUndoManager *undoManager = [m_webView undoManager]; |
| int groupingLevel = [undoManager groupingLevel]; |
| for (int i = 0; i < groupingLevel; ++i) |
| [undoManager endUndoGrouping]; |
| |
| [undoManager removeAllActionsWithTarget:m_undoTarget.get()]; |
| |
| for (int i = 0; i < groupingLevel; ++i) |
| [undoManager beginUndoGrouping]; |
| |
| m_haveUndoRedoOperations = NO; |
| } |
| } |
| |
| bool WebEditorClient::canCopyCut(Frame*, bool defaultValue) const |
| { |
| return defaultValue; |
| } |
| |
| bool WebEditorClient::canPaste(Frame*, bool defaultValue) const |
| { |
| return defaultValue; |
| } |
| |
| bool WebEditorClient::canUndo() const |
| { |
| return [[m_webView undoManager] canUndo]; |
| } |
| |
| bool WebEditorClient::canRedo() const |
| { |
| return [[m_webView undoManager] canRedo]; |
| } |
| |
| void WebEditorClient::undo() |
| { |
| if (canUndo()) |
| [[m_webView undoManager] undo]; |
| } |
| |
| void WebEditorClient::redo() |
| { |
| if (canRedo()) |
| [[m_webView undoManager] redo]; |
| } |
| |
| void WebEditorClient::handleKeyboardEvent(KeyboardEvent& event) |
| { |
| auto* frame = downcast<Node>(event.target())->document().frame(); |
| #if !PLATFORM(IOS_FAMILY) |
| WebHTMLView *webHTMLView = (WebHTMLView *)[[kit(frame) frameView] documentView]; |
| if ([webHTMLView _interpretKeyEvent:&event savingCommands:NO]) |
| event.setDefaultHandled(); |
| #else |
| WebHTMLView *webHTMLView = (WebHTMLView *)[[kit(frame) frameView] documentView]; |
| if ([webHTMLView _handleEditingKeyEvent:&event]) |
| event.setDefaultHandled(); |
| #endif |
| } |
| |
| void WebEditorClient::handleInputMethodKeydown(KeyboardEvent& event) |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| // FIXME: Switch to WebKit2 model, interpreting the event before it's sent down to WebCore. |
| auto* frame = downcast<Node>(event.target())->document().frame(); |
| WebHTMLView *webHTMLView = (WebHTMLView *)[[kit(frame) frameView] documentView]; |
| if ([webHTMLView _interpretKeyEvent:&event savingCommands:YES]) |
| event.setDefaultHandled(); |
| #else |
| // iOS does not use input manager this way |
| #endif |
| } |
| |
| #define FormDelegateLog(ctrl) LOG(FormDelegate, "control=%@", ctrl) |
| |
| void WebEditorClient::textFieldDidBeginEditing(Element* element) |
| { |
| if (!is<HTMLInputElement>(*element)) |
| return; |
| |
| DOMHTMLInputElement* inputElement = kit(downcast<HTMLInputElement>(element)); |
| FormDelegateLog(inputElement); |
| CallFormDelegate(m_webView, @selector(textFieldDidBeginEditing:inFrame:), inputElement, kit(element->document().frame())); |
| } |
| |
| void WebEditorClient::textFieldDidEndEditing(Element* element) |
| { |
| if (!is<HTMLInputElement>(*element)) |
| return; |
| |
| DOMHTMLInputElement* inputElement = kit(downcast<HTMLInputElement>(element)); |
| FormDelegateLog(inputElement); |
| CallFormDelegate(m_webView, @selector(textFieldDidEndEditing:inFrame:), inputElement, kit(element->document().frame())); |
| } |
| |
| void WebEditorClient::textDidChangeInTextField(Element* element) |
| { |
| if (!is<HTMLInputElement>(*element)) |
| return; |
| |
| #if !PLATFORM(IOS_FAMILY) |
| if (!UserTypingGestureIndicator::processingUserTypingGesture() || UserTypingGestureIndicator::focusedElementAtGestureStart() != element) |
| return; |
| #endif |
| |
| DOMHTMLInputElement* inputElement = kit(downcast<HTMLInputElement>(element)); |
| FormDelegateLog(inputElement); |
| CallFormDelegate(m_webView, @selector(textDidChangeInTextField:inFrame:), inputElement, kit(element->document().frame())); |
| } |
| |
| static SEL selectorForKeyEvent(KeyboardEvent* event) |
| { |
| // FIXME: This helper function is for the auto-fill code so we can pass a selector to the form delegate. |
| // Eventually, we should move all of the auto-fill code down to WebKit and remove the need for this function by |
| // not relying on the selector in the new implementation. |
| // The key identifiers are from <http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set> |
| const String& key = event->keyIdentifier(); |
| if (key == "Up") |
| return @selector(moveUp:); |
| if (key == "Down") |
| return @selector(moveDown:); |
| IGNORE_WARNINGS_BEGIN("undeclared-selector") |
| if (key == "U+001B") |
| return @selector(cancel:); |
| if (key == "U+0009") { |
| if (event->shiftKey()) |
| return @selector(insertBacktab:); |
| return @selector(insertTab:); |
| } |
| if (key == "Enter") |
| return @selector(insertNewline:); |
| IGNORE_WARNINGS_END |
| return 0; |
| } |
| |
| bool WebEditorClient::doTextFieldCommandFromEvent(Element* element, KeyboardEvent* event) |
| { |
| if (!is<HTMLInputElement>(*element)) |
| return NO; |
| |
| DOMHTMLInputElement* inputElement = kit(downcast<HTMLInputElement>(element)); |
| FormDelegateLog(inputElement); |
| if (SEL commandSelector = selectorForKeyEvent(event)) |
| return CallFormDelegateReturningBoolean(NO, m_webView, @selector(textField:doCommandBySelector:inFrame:), inputElement, commandSelector, kit(element->document().frame())); |
| return NO; |
| } |
| |
| void WebEditorClient::textWillBeDeletedInTextField(Element* element) |
| { |
| if (!is<HTMLInputElement>(*element)) |
| return; |
| |
| DOMHTMLInputElement* inputElement = kit(downcast<HTMLInputElement>(element)); |
| FormDelegateLog(inputElement); |
| // We're using the deleteBackward selector for all deletion operations since the autofill code treats all deletions the same way. |
| CallFormDelegateReturningBoolean(NO, m_webView, @selector(textField:doCommandBySelector:inFrame:), inputElement, @selector(deleteBackward:), kit(element->document().frame())); |
| } |
| |
| void WebEditorClient::textDidChangeInTextArea(Element* element) |
| { |
| if (!is<HTMLTextAreaElement>(*element)) |
| return; |
| |
| DOMHTMLTextAreaElement* textAreaElement = kit(downcast<HTMLTextAreaElement>(element)); |
| FormDelegateLog(textAreaElement); |
| CallFormDelegate(m_webView, @selector(textDidChangeInTextArea:inFrame:), textAreaElement, kit(element->document().frame())); |
| } |
| |
| #if PLATFORM(IOS_FAMILY) |
| |
| bool WebEditorClient::hasRichlyEditableSelection() |
| { |
| if ([[m_webView _UIKitDelegateForwarder] respondsToSelector:@selector(hasRichlyEditableSelection)]) |
| return [[m_webView _UIKitDelegateForwarder] hasRichlyEditableSelection]; |
| |
| return false; |
| } |
| |
| int WebEditorClient::getPasteboardItemsCount() |
| { |
| if ([[m_webView _UIKitDelegateForwarder] respondsToSelector:@selector(getPasteboardItemsCount)]) |
| return [[m_webView _UIKitDelegateForwarder] getPasteboardItemsCount]; |
| |
| return 0; |
| } |
| |
| RefPtr<WebCore::DocumentFragment> WebEditorClient::documentFragmentFromDelegate(int index) |
| { |
| if ([[m_webView _editingDelegateForwarder] respondsToSelector:@selector(documentFragmentForPasteboardItemAtIndex:)]) { |
| DOMDocumentFragment *fragmentFromDelegate = [[m_webView _editingDelegateForwarder] documentFragmentForPasteboardItemAtIndex:index]; |
| if (fragmentFromDelegate) |
| return core(fragmentFromDelegate); |
| } |
| |
| return nullptr; |
| } |
| |
| bool WebEditorClient::performsTwoStepPaste(WebCore::DocumentFragment* fragment) |
| { |
| if ([[m_webView _UIKitDelegateForwarder] respondsToSelector:@selector(performsTwoStepPaste:)]) |
| return [[m_webView _UIKitDelegateForwarder] performsTwoStepPaste:kit(fragment)]; |
| |
| return false; |
| } |
| |
| bool WebEditorClient::performTwoStepDrop(DocumentFragment& fragment, Range& destination, bool isMove) |
| { |
| if ([[m_webView _UIKitDelegateForwarder] respondsToSelector:@selector(performTwoStepDrop:atDestination:isMove:)]) |
| return [[m_webView _UIKitDelegateForwarder] performTwoStepDrop:kit(&fragment) atDestination:kit(&destination) isMove:isMove]; |
| |
| return false; |
| } |
| |
| Vector<TextCheckingResult> WebEditorClient::checkTextOfParagraph(StringView string, OptionSet<TextCheckingType> checkingTypes, const VisibleSelection&) |
| { |
| ASSERT(checkingTypes.contains(TextCheckingType::Spelling)); |
| |
| Vector<TextCheckingResult> results; |
| |
| NSArray *incomingResults = [[m_webView _UIKitDelegateForwarder] checkSpellingOfString:string.createNSStringWithoutCopying().get()]; |
| for (NSValue *incomingResult in incomingResults) { |
| NSRange resultRange = [incomingResult rangeValue]; |
| ASSERT(resultRange.location != NSNotFound && resultRange.length > 0); |
| TextCheckingResult result; |
| result.type = TextCheckingType::Spelling; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| results.append(result); |
| } |
| |
| return results; |
| } |
| |
| #endif // PLATFORM(IOS_FAMILY) |
| |
| #if !PLATFORM(IOS_FAMILY) |
| |
| bool WebEditorClient::performTwoStepDrop(DocumentFragment&, Range&, bool) |
| { |
| return false; |
| } |
| |
| bool WebEditorClient::shouldEraseMarkersAfterChangeSelection(TextCheckingType type) const |
| { |
| // This prevents erasing spelling markers on OS X Lion or later to match AppKit on these Mac OS X versions. |
| return type != TextCheckingType::Spelling; |
| } |
| |
| void WebEditorClient::ignoreWordInSpellDocument(const String& text) |
| { |
| [[NSSpellChecker sharedSpellChecker] ignoreWord:text inSpellDocumentWithTag:spellCheckerDocumentTag()]; |
| } |
| |
| void WebEditorClient::learnWord(const String& text) |
| { |
| [[NSSpellChecker sharedSpellChecker] learnWord:text]; |
| } |
| |
| void WebEditorClient::checkSpellingOfString(StringView text, int* misspellingLocation, int* misspellingLength) |
| { |
| NSRange range = [[NSSpellChecker sharedSpellChecker] checkSpellingOfString:text.createNSStringWithoutCopying().get() startingAt:0 language:nil wrap:NO inSpellDocumentWithTag:spellCheckerDocumentTag() wordCount:NULL]; |
| |
| if (misspellingLocation) { |
| // WebCore expects -1 to represent "not found" |
| if (range.location == NSNotFound) |
| *misspellingLocation = -1; |
| else |
| *misspellingLocation = range.location; |
| } |
| |
| if (misspellingLength) |
| *misspellingLength = range.length; |
| } |
| |
| String WebEditorClient::getAutoCorrectSuggestionForMisspelledWord(const String& inputWord) |
| { |
| // This method can be implemented using customized algorithms for the particular browser. |
| // Currently, it computes an empty string. |
| return String(); |
| } |
| |
| void WebEditorClient::checkGrammarOfString(StringView text, Vector<GrammarDetail>& details, int* badGrammarLocation, int* badGrammarLength) |
| { |
| NSArray *grammarDetails; |
| NSRange range = [[NSSpellChecker sharedSpellChecker] checkGrammarOfString:text.createNSStringWithoutCopying().get() startingAt:0 language:nil wrap:NO inSpellDocumentWithTag:spellCheckerDocumentTag() details:&grammarDetails]; |
| if (badGrammarLocation) |
| // WebCore expects -1 to represent "not found" |
| *badGrammarLocation = (range.location == NSNotFound) ? -1 : static_cast<int>(range.location); |
| if (badGrammarLength) |
| *badGrammarLength = range.length; |
| for (NSDictionary *detail in grammarDetails) { |
| ASSERT(detail); |
| GrammarDetail grammarDetail; |
| NSValue *detailRangeAsNSValue = [detail objectForKey:NSGrammarRange]; |
| ASSERT(detailRangeAsNSValue); |
| NSRange detailNSRange = [detailRangeAsNSValue rangeValue]; |
| ASSERT(detailNSRange.location != NSNotFound); |
| ASSERT(detailNSRange.length > 0); |
| grammarDetail.location = detailNSRange.location; |
| grammarDetail.length = detailNSRange.length; |
| grammarDetail.userDescription = [detail objectForKey:NSGrammarUserDescription]; |
| NSArray *guesses = [detail objectForKey:NSGrammarCorrections]; |
| for (NSString *guess in guesses) |
| grammarDetail.guesses.append(String(guess)); |
| details.append(grammarDetail); |
| } |
| } |
| |
| static Vector<TextCheckingResult> core(NSArray *incomingResults, OptionSet<TextCheckingType> checkingTypes) |
| { |
| Vector<TextCheckingResult> results; |
| |
| for (NSTextCheckingResult *incomingResult in incomingResults) { |
| NSRange resultRange = [incomingResult range]; |
| NSTextCheckingType resultType = [incomingResult resultType]; |
| ASSERT(resultRange.location != NSNotFound); |
| ASSERT(resultRange.length > 0); |
| if (resultType == NSTextCheckingTypeSpelling && checkingTypes.contains(TextCheckingType::Spelling)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Spelling; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeGrammar && checkingTypes.contains(TextCheckingType::Grammar)) { |
| TextCheckingResult result; |
| NSArray *details = [incomingResult grammarDetails]; |
| result.type = TextCheckingType::Grammar; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| for (NSDictionary *incomingDetail in details) { |
| ASSERT(incomingDetail); |
| GrammarDetail detail; |
| NSValue *detailRangeAsNSValue = [incomingDetail objectForKey:NSGrammarRange]; |
| ASSERT(detailRangeAsNSValue); |
| NSRange detailNSRange = [detailRangeAsNSValue rangeValue]; |
| ASSERT(detailNSRange.location != NSNotFound); |
| ASSERT(detailNSRange.length > 0); |
| detail.location = detailNSRange.location; |
| detail.length = detailNSRange.length; |
| detail.userDescription = [incomingDetail objectForKey:NSGrammarUserDescription]; |
| NSArray *guesses = [incomingDetail objectForKey:NSGrammarCorrections]; |
| for (NSString *guess in guesses) |
| detail.guesses.append(String(guess)); |
| result.details.append(detail); |
| } |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeLink && checkingTypes.contains(TextCheckingType::Link)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Link; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| result.replacement = [[incomingResult URL] absoluteString]; |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeQuote && checkingTypes.contains(TextCheckingType::Quote)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Quote; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| result.replacement = [incomingResult replacementString]; |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeDash && checkingTypes.contains(TextCheckingType::Dash)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Dash; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| result.replacement = [incomingResult replacementString]; |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeReplacement && checkingTypes.contains(TextCheckingType::Replacement)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Replacement; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| result.replacement = [incomingResult replacementString]; |
| results.append(result); |
| } else if (resultType == NSTextCheckingTypeCorrection && checkingTypes.contains(TextCheckingType::Correction)) { |
| TextCheckingResult result; |
| result.type = TextCheckingType::Correction; |
| result.location = resultRange.location; |
| result.length = resultRange.length; |
| result.replacement = [incomingResult replacementString]; |
| results.append(result); |
| } |
| } |
| |
| return results; |
| } |
| |
| static int insertionPointFromCurrentSelection(const VisibleSelection& currentSelection) |
| { |
| VisiblePosition selectionStart = currentSelection.visibleStart(); |
| VisiblePosition paragraphStart = startOfParagraph(selectionStart); |
| return TextIterator::rangeLength(makeRange(paragraphStart, selectionStart).get()); |
| } |
| |
| Vector<TextCheckingResult> WebEditorClient::checkTextOfParagraph(StringView string, OptionSet<TextCheckingType> coreCheckingTypes, const VisibleSelection& currentSelection) |
| { |
| NSDictionary *options = @{ NSTextCheckingInsertionPointKey : [NSNumber numberWithUnsignedInteger:insertionPointFromCurrentSelection(currentSelection)] }; |
| return core([[NSSpellChecker sharedSpellChecker] checkString:string.createNSStringWithoutCopying().get() range:NSMakeRange(0, string.length()) types:(nsTextCheckingTypes(coreCheckingTypes) | NSTextCheckingTypeOrthography) options:options inSpellDocumentWithTag:spellCheckerDocumentTag() orthography:NULL wordCount:NULL], coreCheckingTypes); |
| } |
| |
| void WebEditorClient::updateSpellingUIWithGrammarString(const String& badGrammarPhrase, const GrammarDetail& grammarDetail) |
| { |
| NSMutableArray* corrections = [NSMutableArray array]; |
| for (unsigned i = 0; i < grammarDetail.guesses.size(); i++) { |
| NSString* guess = grammarDetail.guesses[i]; |
| [corrections addObject:guess]; |
| } |
| NSRange grammarRange = NSMakeRange(grammarDetail.location, grammarDetail.length); |
| NSString* grammarUserDescription = grammarDetail.userDescription; |
| NSDictionary* grammarDetailDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSValue valueWithRange:grammarRange], NSGrammarRange, grammarUserDescription, NSGrammarUserDescription, corrections, NSGrammarCorrections, nil]; |
| |
| [[NSSpellChecker sharedSpellChecker] updateSpellingPanelWithGrammarString:badGrammarPhrase detail:grammarDetailDict]; |
| } |
| |
| void WebEditorClient::updateSpellingUIWithMisspelledWord(const String& misspelledWord) |
| { |
| [[NSSpellChecker sharedSpellChecker] updateSpellingPanelWithMisspelledWord:misspelledWord]; |
| } |
| |
| void WebEditorClient::showSpellingUI(bool show) |
| { |
| NSPanel *spellingPanel = [[NSSpellChecker sharedSpellChecker] spellingPanel]; |
| if (show) |
| [spellingPanel orderFront:nil]; |
| else |
| [spellingPanel orderOut:nil]; |
| } |
| |
| bool WebEditorClient::spellingUIIsShowing() |
| { |
| return [[[NSSpellChecker sharedSpellChecker] spellingPanel] isVisible]; |
| } |
| |
| void WebEditorClient::getGuessesForWord(const String& word, const String& context, const WebCore::VisibleSelection& currentSelection, Vector<String>& guesses) |
| { |
| guesses.clear(); |
| NSString* language = nil; |
| NSOrthography* orthography = nil; |
| NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker]; |
| NSDictionary *options = @{ NSTextCheckingInsertionPointKey : [NSNumber numberWithUnsignedInteger:insertionPointFromCurrentSelection(currentSelection)] }; |
| if (context.length()) { |
| [checker checkString:context range:NSMakeRange(0, context.length()) types:NSTextCheckingTypeOrthography options:options inSpellDocumentWithTag:spellCheckerDocumentTag() orthography:&orthography wordCount:0]; |
| language = [checker languageForWordRange:NSMakeRange(0, context.length()) inString:context orthography:orthography]; |
| } |
| NSArray* stringsArray = [checker guessesForWordRange:NSMakeRange(0, word.length()) inString:word language:language inSpellDocumentWithTag:spellCheckerDocumentTag()]; |
| unsigned count = [stringsArray count]; |
| |
| if (count > 0) { |
| NSEnumerator* enumerator = [stringsArray objectEnumerator]; |
| NSString* string; |
| while ((string = [enumerator nextObject]) != nil) |
| guesses.append(string); |
| } |
| } |
| |
| #endif // !PLATFORM(IOS_FAMILY) |
| |
| void WebEditorClient::willSetInputMethodState() |
| { |
| } |
| |
| void WebEditorClient::setInputMethodState(bool) |
| { |
| } |
| |
| #if PLATFORM(MAC) |
| |
| void WebEditorClient::requestCandidatesForSelection(const VisibleSelection& selection) |
| { |
| if (![m_webView shouldRequestCandidates]) |
| return; |
| |
| RefPtr<Range> selectedRange = selection.toNormalizedRange(); |
| if (!selectedRange) |
| return; |
| |
| Frame* frame = core([m_webView _selectedOrMainFrame]); |
| if (!frame) |
| return; |
| |
| m_lastSelectionForRequestedCandidates = selection; |
| |
| VisiblePosition selectionStart = selection.visibleStart(); |
| VisiblePosition selectionEnd = selection.visibleEnd(); |
| VisiblePosition paragraphStart = startOfParagraph(selectionStart); |
| VisiblePosition paragraphEnd = endOfParagraph(selectionEnd); |
| |
| int lengthToSelectionStart = TextIterator::rangeLength(makeRange(paragraphStart, selectionStart).get()); |
| int lengthToSelectionEnd = TextIterator::rangeLength(makeRange(paragraphStart, selectionEnd).get()); |
| m_rangeForCandidates = NSMakeRange(lengthToSelectionStart, lengthToSelectionEnd - lengthToSelectionStart); |
| m_paragraphContextForCandidateRequest = plainText(frame->editor().contextRangeForCandidateRequest().get()); |
| |
| NSTextCheckingTypes checkingTypes = NSTextCheckingTypeSpelling | NSTextCheckingTypeReplacement | NSTextCheckingTypeCorrection; |
| auto weakEditor = makeWeakPtr(*this); |
| m_lastCandidateRequestSequenceNumber = [[NSSpellChecker sharedSpellChecker] requestCandidatesForSelectedRange:m_rangeForCandidates inString:m_paragraphContextForCandidateRequest.get() types:checkingTypes options:nil inSpellDocumentWithTag:spellCheckerDocumentTag() completionHandler:[weakEditor](NSInteger sequenceNumber, NSArray<NSTextCheckingResult *> *candidates) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| if (!weakEditor) |
| return; |
| weakEditor->handleRequestedCandidates(sequenceNumber, candidates); |
| }); |
| }]; |
| } |
| |
| void WebEditorClient::handleRequestedCandidates(NSInteger sequenceNumber, NSArray<NSTextCheckingResult *> *candidates) |
| { |
| if (![m_webView shouldRequestCandidates]) |
| return; |
| |
| if (m_lastCandidateRequestSequenceNumber != sequenceNumber) |
| return; |
| |
| Frame* frame = core([m_webView _selectedOrMainFrame]); |
| if (!frame) |
| return; |
| |
| const VisibleSelection& selection = frame->selection().selection(); |
| if (selection != m_lastSelectionForRequestedCandidates) |
| return; |
| |
| RefPtr<Range> selectedRange = selection.toNormalizedRange(); |
| if (!selectedRange) |
| return; |
| |
| IntRect rectForSelectionCandidates; |
| Vector<FloatQuad> quads; |
| selectedRange->absoluteTextQuads(quads); |
| if (!quads.isEmpty()) |
| rectForSelectionCandidates = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox()); |
| else { |
| // Range::absoluteTextQuads() will be empty at the start of a paragraph. |
| if (selection.isCaret()) |
| rectForSelectionCandidates = frame->view()->contentsToWindow(frame->selection().absoluteCaretBounds()); |
| } |
| |
| [m_webView showCandidates:candidates forString:m_paragraphContextForCandidateRequest.get() inRect:rectForSelectionCandidates forSelectedRange:m_rangeForCandidates view:m_webView completionHandler:nil]; |
| } |
| |
| void WebEditorClient::handleAcceptedCandidateWithSoftSpaces(TextCheckingResult acceptedCandidate) |
| { |
| Frame* frame = core([m_webView _selectedOrMainFrame]); |
| if (!frame) |
| return; |
| |
| const VisibleSelection& selection = frame->selection().selection(); |
| if (selection != m_lastSelectionForRequestedCandidates) |
| return; |
| |
| NSView <WebDocumentView> *view = [[[m_webView selectedFrame] frameView] documentView]; |
| if ([view isKindOfClass:[WebHTMLView class]]) { |
| unsigned replacementLength = acceptedCandidate.replacement.length(); |
| if (replacementLength > 0) { |
| NSRange replacedRange = NSMakeRange(acceptedCandidate.location, replacementLength); |
| NSRange softSpaceRange = NSMakeRange(NSMaxRange(replacedRange) - 1, 1); |
| if (acceptedCandidate.replacement.endsWith(" ")) |
| [(WebHTMLView *)view _setSoftSpaceRange:softSpaceRange]; |
| } |
| } |
| |
| frame->editor().handleAcceptedCandidate(acceptedCandidate); |
| } |
| |
| #endif // PLATFORM(MAC) |
| |
| #if !PLATFORM(IOS_FAMILY) |
| |
| @interface WebEditorSpellCheckResponder : NSObject |
| { |
| WebEditorClient* _client; |
| int _sequence; |
| RetainPtr<NSArray> _results; |
| } |
| - (id)initWithClient:(WebEditorClient*)client sequence:(int)sequence results:(NSArray*)results; |
| - (void)perform; |
| @end |
| |
| @implementation WebEditorSpellCheckResponder |
| - (id)initWithClient:(WebEditorClient*)client sequence:(int)sequence results:(NSArray*)results |
| { |
| self = [super init]; |
| if (!self) |
| return nil; |
| _client = client; |
| _sequence = sequence; |
| _results = results; |
| return self; |
| } |
| |
| - (void)perform |
| { |
| _client->didCheckSucceed(_sequence, _results.get()); |
| } |
| |
| @end |
| |
| void WebEditorClient::didCheckSucceed(int sequence, NSArray* results) |
| { |
| ASSERT_UNUSED(sequence, sequence == m_textCheckingRequest->data().sequence()); |
| m_textCheckingRequest->didSucceed(core(results, m_textCheckingRequest->data().checkingTypes())); |
| m_textCheckingRequest = nullptr; |
| } |
| |
| #endif |
| |
| void WebEditorClient::requestCheckingOfString(WebCore::TextCheckingRequest& request, const VisibleSelection& currentSelection) |
| { |
| #if !PLATFORM(IOS_FAMILY) |
| ASSERT(!m_textCheckingRequest); |
| m_textCheckingRequest = &request; |
| |
| int sequence = m_textCheckingRequest->data().sequence(); |
| NSRange range = NSMakeRange(0, m_textCheckingRequest->data().text().length()); |
| NSRunLoop* currentLoop = [NSRunLoop currentRunLoop]; |
| NSDictionary *options = @{ NSTextCheckingInsertionPointKey : [NSNumber numberWithUnsignedInteger:insertionPointFromCurrentSelection(currentSelection)] }; |
| [[NSSpellChecker sharedSpellChecker] requestCheckingOfString:m_textCheckingRequest->data().text() range:range types:NSTextCheckingAllSystemTypes options:options inSpellDocumentWithTag:0 completionHandler:^(NSInteger, NSArray* results, NSOrthography*, NSInteger) { |
| [currentLoop performSelector:@selector(perform) |
| target:[[[WebEditorSpellCheckResponder alloc] initWithClient:this sequence:sequence results:results] autorelease] |
| argument:nil order:0 modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]; |
| }]; |
| #endif |
| } |
| |
| #if PLATFORM(IOS_FAMILY) |
| bool WebEditorClient::shouldAllowSingleClickToChangeSelection(WebCore::Node& targetNode, const WebCore::VisibleSelection& newSelection) const |
| { |
| // The text selection assistant will handle selection in the case where we are already editing the node |
| auto* editableRoot = newSelection.rootEditableElement(); |
| return !editableRoot || editableRoot != targetNode.rootEditableElement(); |
| } |
| #endif |