diff options
author | Ben Murdoch <benm@google.com> | 2011-06-02 12:07:03 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2011-06-10 10:47:21 +0100 |
commit | 2daae5fd11344eaa88a0d92b0f6d65f8d2255c00 (patch) | |
tree | e4964fbd1cb70599f7718ff03e50ea1dab33890b /Source/WebCore/editing | |
parent | 87bdf0060a247bfbe668342b87e0874182e0ffa9 (diff) | |
download | external_webkit-2daae5fd11344eaa88a0d92b0f6d65f8d2255c00.zip external_webkit-2daae5fd11344eaa88a0d92b0f6d65f8d2255c00.tar.gz external_webkit-2daae5fd11344eaa88a0d92b0f6d65f8d2255c00.tar.bz2 |
Merge WebKit at r84325: Initial merge by git.
Change-Id: Ic1a909300ecc0a13ddc6b4e784371d2ac6e3d59b
Diffstat (limited to 'Source/WebCore/editing')
36 files changed, 1794 insertions, 1056 deletions
diff --git a/Source/WebCore/editing/ApplyStyleCommand.cpp b/Source/WebCore/editing/ApplyStyleCommand.cpp index 59540ec..c9649d0 100644 --- a/Source/WebCore/editing/ApplyStyleCommand.cpp +++ b/Source/WebCore/editing/ApplyStyleCommand.cpp @@ -54,242 +54,6 @@ namespace WebCore { using namespace HTMLNames; -static RGBA32 getRGBAFontColor(CSSStyleDeclaration* style) -{ - RefPtr<CSSValue> colorValue = style->getPropertyCSSValue(CSSPropertyColor); - if (!colorValue || !colorValue->isPrimitiveValue()) - return Color::transparent; - - CSSPrimitiveValue* primitiveColor = static_cast<CSSPrimitiveValue*>(colorValue.get()); - RGBA32 rgba = 0; - if (primitiveColor->primitiveType() != CSSPrimitiveValue::CSS_RGBCOLOR) { - CSSParser::parseColor(rgba, colorValue->cssText()); - // Need to take care of named color such as green and black - // This code should be removed after https://bugs.webkit.org/show_bug.cgi?id=28282 is fixed. - } else - rgba = primitiveColor->getRGBA32Value(); - - return rgba; -} - -class StyleChange { -public: - StyleChange(EditingStyle*, const Position&); - - String cssStyle() const { return m_cssStyle; } - bool applyBold() const { return m_applyBold; } - bool applyItalic() const { return m_applyItalic; } - bool applyUnderline() const { return m_applyUnderline; } - bool applyLineThrough() const { return m_applyLineThrough; } - bool applySubscript() const { return m_applySubscript; } - bool applySuperscript() const { return m_applySuperscript; } - bool applyFontColor() const { return m_applyFontColor.length() > 0; } - bool applyFontFace() const { return m_applyFontFace.length() > 0; } - bool applyFontSize() const { return m_applyFontSize.length() > 0; } - - String fontColor() { return m_applyFontColor; } - String fontFace() { return m_applyFontFace; } - String fontSize() { return m_applyFontSize; } - - bool operator==(const StyleChange& other) - { - return m_cssStyle == other.m_cssStyle - && m_applyBold == other.m_applyBold - && m_applyItalic == other.m_applyItalic - && m_applyUnderline == other.m_applyUnderline - && m_applyLineThrough == other.m_applyLineThrough - && m_applySubscript == other.m_applySubscript - && m_applySuperscript == other.m_applySuperscript - && m_applyFontColor == other.m_applyFontColor - && m_applyFontFace == other.m_applyFontFace - && m_applyFontSize == other.m_applyFontSize; - } - bool operator!=(const StyleChange& other) - { - return !(*this == other); - } -private: - void init(EditingStyle*, const Position&); - void reconcileTextDecorationProperties(CSSMutableStyleDeclaration*); - void extractTextStyles(Document*, CSSMutableStyleDeclaration*, bool shouldUseFixedFontDefaultSize); - - String m_cssStyle; - bool m_applyBold; - bool m_applyItalic; - bool m_applyUnderline; - bool m_applyLineThrough; - bool m_applySubscript; - bool m_applySuperscript; - String m_applyFontColor; - String m_applyFontFace; - String m_applyFontSize; -}; - - -StyleChange::StyleChange(EditingStyle* style, const Position& position) - : m_applyBold(false) - , m_applyItalic(false) - , m_applyUnderline(false) - , m_applyLineThrough(false) - , m_applySubscript(false) - , m_applySuperscript(false) -{ - init(style, position); -} - -void StyleChange::init(EditingStyle* style, const Position& position) -{ - Document* document = position.anchorNode() ? position.anchorNode()->document() : 0; - if (!style || !style->style() || !document || !document->frame()) - return; - - RefPtr<CSSComputedStyleDeclaration> computedStyle = position.computedStyle(); - RefPtr<CSSMutableStyleDeclaration> mutableStyle = getPropertiesNotIn(style->style(), computedStyle.get()); - - reconcileTextDecorationProperties(mutableStyle.get()); - if (!document->frame()->editor()->shouldStyleWithCSS()) - extractTextStyles(document, mutableStyle.get(), computedStyle->useFixedFontDefaultSize()); - - // Changing the whitespace style in a tab span would collapse the tab into a space. - if (isTabSpanTextNode(position.deprecatedNode()) || isTabSpanNode((position.deprecatedNode()))) - mutableStyle->removeProperty(CSSPropertyWhiteSpace); - - // If unicode-bidi is present in mutableStyle and direction is not, then add direction to mutableStyle. - // FIXME: Shouldn't this be done in getPropertiesNotIn? - if (mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi) && !style->style()->getPropertyCSSValue(CSSPropertyDirection)) - mutableStyle->setProperty(CSSPropertyDirection, style->style()->getPropertyValue(CSSPropertyDirection)); - - // Save the result for later - m_cssStyle = mutableStyle->cssText().stripWhiteSpace(); -} - -void StyleChange::reconcileTextDecorationProperties(CSSMutableStyleDeclaration* style) -{ - RefPtr<CSSValue> textDecorationsInEffect = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); - RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); - // We shouldn't have both text-decoration and -webkit-text-decorations-in-effect because that wouldn't make sense. - ASSERT(!textDecorationsInEffect || !textDecoration); - if (textDecorationsInEffect) { - style->setProperty(CSSPropertyTextDecoration, textDecorationsInEffect->cssText()); - style->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); - textDecoration = textDecorationsInEffect; - } - - // If text-decoration is set to "none", remove the property because we don't want to add redundant "text-decoration: none". - if (textDecoration && !textDecoration->isValueList()) - style->removeProperty(CSSPropertyTextDecoration); -} - -static int getIdentifierValue(CSSStyleDeclaration* style, int propertyID) -{ - if (!style) - return 0; - - RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); - if (!value || !value->isPrimitiveValue()) - return 0; - - return static_cast<CSSPrimitiveValue*>(value.get())->getIdent(); -} - -static void setTextDecorationProperty(CSSMutableStyleDeclaration* style, const CSSValueList* newTextDecoration, int propertyID) -{ - if (newTextDecoration->length()) - style->setProperty(propertyID, newTextDecoration->cssText(), style->getPropertyPriority(propertyID)); - else { - // text-decoration: none is redundant since it does not remove any text decorations. - ASSERT(!style->getPropertyPriority(propertyID)); - style->removeProperty(propertyID); - } -} - -static bool isCSSValueLength(CSSPrimitiveValue* value) -{ - return value->primitiveType() >= CSSPrimitiveValue::CSS_PX && value->primitiveType() <= CSSPrimitiveValue::CSS_PC; -} - -int legacyFontSizeFromCSSValue(Document* document, CSSPrimitiveValue* value, bool shouldUseFixedFontDefaultSize, LegacyFontSizeMode mode) -{ - if (isCSSValueLength(value)) { - int pixelFontSize = value->getIntValue(CSSPrimitiveValue::CSS_PX); - int legacyFontSize = CSSStyleSelector::legacyFontSize(document, pixelFontSize, shouldUseFixedFontDefaultSize); - // Use legacy font size only if pixel value matches exactly to that of legacy font size. - int cssPrimitiveEquivalent = legacyFontSize - 1 + CSSValueXSmall; - if (mode == AlwaysUseLegacyFontSize || CSSStyleSelector::fontSizeForKeyword(document, cssPrimitiveEquivalent, shouldUseFixedFontDefaultSize) == pixelFontSize) - return legacyFontSize; - - return 0; - } - - if (CSSValueXSmall <= value->getIdent() && value->getIdent() <= CSSValueWebkitXxxLarge) - return value->getIdent() - CSSValueXSmall + 1; - - return 0; -} - -void StyleChange::extractTextStyles(Document* document, CSSMutableStyleDeclaration* style, bool shouldUseFixedFontDefaultSize) -{ - ASSERT(style); - - if (getIdentifierValue(style, CSSPropertyFontWeight) == CSSValueBold) { - style->removeProperty(CSSPropertyFontWeight); - m_applyBold = true; - } - - int fontStyle = getIdentifierValue(style, CSSPropertyFontStyle); - if (fontStyle == CSSValueItalic || fontStyle == CSSValueOblique) { - style->removeProperty(CSSPropertyFontStyle); - m_applyItalic = true; - } - - // Assuming reconcileTextDecorationProperties has been called, there should not be -webkit-text-decorations-in-effect - // Furthermore, text-decoration: none has been trimmed so that text-decoration property is always a CSSValueList. - RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); - if (textDecoration && textDecoration->isValueList()) { - DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); - DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); - - RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); - if (newTextDecoration->removeAll(underline.get())) - m_applyUnderline = true; - if (newTextDecoration->removeAll(lineThrough.get())) - m_applyLineThrough = true; - - // If trimTextDecorations, delete underline and line-through - setTextDecorationProperty(style, newTextDecoration.get(), CSSPropertyTextDecoration); - } - - int verticalAlign = getIdentifierValue(style, CSSPropertyVerticalAlign); - switch (verticalAlign) { - case CSSValueSub: - style->removeProperty(CSSPropertyVerticalAlign); - m_applySubscript = true; - break; - case CSSValueSuper: - style->removeProperty(CSSPropertyVerticalAlign); - m_applySuperscript = true; - break; - } - - if (style->getPropertyCSSValue(CSSPropertyColor)) { - m_applyFontColor = Color(getRGBAFontColor(style)).serialized(); - style->removeProperty(CSSPropertyColor); - } - - m_applyFontFace = style->getPropertyValue(CSSPropertyFontFamily); - style->removeProperty(CSSPropertyFontFamily); - - if (RefPtr<CSSValue> fontSize = style->getPropertyCSSValue(CSSPropertyFontSize)) { - if (!fontSize->isPrimitiveValue()) - style->removeProperty(CSSPropertyFontSize); // Can't make sense of the number. Put no font size. - else if (int legacyFontSize = legacyFontSizeFromCSSValue(document, static_cast<CSSPrimitiveValue*>(fontSize.get()), - shouldUseFixedFontDefaultSize, UseLegacyFontSizeOnlyIfPixelValuesMatch)) { - m_applyFontSize = String::number(legacyFontSize); - style->removeProperty(CSSPropertyFontSize); - } - } -} - static String& styleSpanClassString() { DEFINE_STATIC_LOCAL(String, styleSpanClassString, ((AppleStyleSpanClass))); @@ -354,95 +118,6 @@ PassRefPtr<HTMLElement> createStyleSpanElement(Document* document) return styleElement.release(); } -static void diffTextDecorations(CSSMutableStyleDeclaration* style, int propertID, CSSValue* refTextDecoration) -{ - RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(propertID); - if (!textDecoration || !textDecoration->isValueList() || !refTextDecoration || !refTextDecoration->isValueList()) - return; - - RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); - CSSValueList* valuesInRefTextDecoration = static_cast<CSSValueList*>(refTextDecoration); - - for (size_t i = 0; i < valuesInRefTextDecoration->length(); i++) - newTextDecoration->removeAll(valuesInRefTextDecoration->item(i)); - - setTextDecorationProperty(style, newTextDecoration.get(), propertID); -} - -static bool fontWeightIsBold(CSSStyleDeclaration* style) -{ - ASSERT(style); - RefPtr<CSSValue> fontWeight = style->getPropertyCSSValue(CSSPropertyFontWeight); - - if (!fontWeight) - return false; - if (!fontWeight->isPrimitiveValue()) - return false; - - // Because b tag can only bold text, there are only two states in plain html: bold and not bold. - // Collapse all other values to either one of these two states for editing purposes. - switch (static_cast<CSSPrimitiveValue*>(fontWeight.get())->getIdent()) { - case CSSValue100: - case CSSValue200: - case CSSValue300: - case CSSValue400: - case CSSValue500: - case CSSValueNormal: - return false; - case CSSValueBold: - case CSSValue600: - case CSSValue700: - case CSSValue800: - case CSSValue900: - return true; - } - - ASSERT_NOT_REACHED(); // For CSSValueBolder and CSSValueLighter - return false; // Make compiler happy -} - -static int getTextAlignment(CSSStyleDeclaration* style) -{ - int textAlign = getIdentifierValue(style, CSSPropertyTextAlign); - switch (textAlign) { - case CSSValueCenter: - case CSSValueWebkitCenter: - return CSSValueCenter; - case CSSValueJustify: - return CSSValueJustify; - case CSSValueLeft: - case CSSValueWebkitLeft: - return CSSValueLeft; - case CSSValueRight: - case CSSValueWebkitRight: - return CSSValueRight; - } - return CSSValueInvalid; -} - -RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle) -{ - ASSERT(styleWithRedundantProperties); - ASSERT(baseStyle); - RefPtr<CSSMutableStyleDeclaration> result = styleWithRedundantProperties->copy(); - baseStyle->diff(result.get()); - - RefPtr<CSSValue> baseTextDecorationsInEffect = baseStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); - diffTextDecorations(result.get(), CSSPropertyTextDecoration, baseTextDecorationsInEffect.get()); - diffTextDecorations(result.get(), CSSPropertyWebkitTextDecorationsInEffect, baseTextDecorationsInEffect.get()); - - if (fontWeightIsBold(result.get()) == fontWeightIsBold(baseStyle)) - result->removeProperty(CSSPropertyFontWeight); - - if (getRGBAFontColor(result.get()) == getRGBAFontColor(baseStyle)) - result->removeProperty(CSSPropertyColor); - - if (getTextAlignment(result.get()) == getTextAlignment(baseStyle)) - result->removeProperty(CSSPropertyTextAlign); - - return result; -} - ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, EditAction editingAction, EPropertyLevel propertyLevel) : CompositeEditCommand(document) , m_style(style->copy()) diff --git a/Source/WebCore/editing/ApplyStyleCommand.h b/Source/WebCore/editing/ApplyStyleCommand.h index dc2217e..6953456 100644 --- a/Source/WebCore/editing/ApplyStyleCommand.h +++ b/Source/WebCore/editing/ApplyStyleCommand.h @@ -34,7 +34,6 @@ namespace WebCore { class CSSPrimitiveValue; class EditingStyle; -class HTMLElement; class StyleChange; enum ShouldIncludeTypingStyle { @@ -130,11 +129,8 @@ private: IsInlineElementToRemoveFunction m_isInlineElementToRemoveFunction; }; -enum LegacyFontSizeMode { AlwaysUseLegacyFontSize, UseLegacyFontSizeOnlyIfPixelValuesMatch }; -int legacyFontSizeFromCSSValue(Document*, CSSPrimitiveValue*, bool shouldUseFixedFontDefaultSize, LegacyFontSizeMode); bool isStyleSpan(const Node*); PassRefPtr<HTMLElement> createStyleSpanElement(Document*); -RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle); } // namespace WebCore diff --git a/Source/WebCore/editing/CompositeEditCommand.cpp b/Source/WebCore/editing/CompositeEditCommand.cpp index 2a69fb7..cf2959a 100644 --- a/Source/WebCore/editing/CompositeEditCommand.cpp +++ b/Source/WebCore/editing/CompositeEditCommand.cpp @@ -1217,23 +1217,27 @@ Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Posi // Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions // to determine if the split is necessary. Returns the last split node. -PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor) +PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool shouldSplitAncestor) { + ASSERT(start); + ASSERT(end); ASSERT(start != end); RefPtr<Node> node; - for (node = start; node && node->parentNode() != end; node = node->parentNode()) { + if (shouldSplitAncestor && end->parentNode()) + end = end->parentNode(); + + RefPtr<Node> endNode = end; + for (node = start; node && node->parentNode() != endNode; node = node->parentNode()) { if (!node->parentNode()->isElementNode()) break; - VisiblePosition positionInParent(firstPositionInNode(node->parentNode()), DOWNSTREAM); - VisiblePosition positionInNode(firstPositionInOrBeforeNode(node.get()), DOWNSTREAM); + // Do not split a node when doing so introduces an empty node. + VisiblePosition positionInParent = firstPositionInNode(node->parentNode()); + VisiblePosition positionInNode = firstPositionInOrBeforeNode(node.get()); if (positionInParent != positionInNode) - applyCommandToComposite(SplitElementCommand::create(static_cast<Element*>(node->parentNode()), node)); - } - if (splitAncestor) { - splitElement(static_cast<Element*>(end), node); - return node->parentNode(); + splitElement(static_cast<Element*>(node->parentNode()), node); } + return node.release(); } diff --git a/Source/WebCore/editing/CorrectionPanelInfo.h b/Source/WebCore/editing/CorrectionPanelInfo.h deleted file mode 100644 index 76099e1..0000000 --- a/Source/WebCore/editing/CorrectionPanelInfo.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2010 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 COMPUTER, INC. ``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 COMPUTER, INC. OR - * 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. - */ - -#ifndef CorrectionPanelInfo_h -#define CorrectionPanelInfo_h - -#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) -// Some platforms provide UI for suggesting autocorrection. -#define SUPPORT_AUTOCORRECTION_PANEL 1 -// Some platforms use spelling and autocorrection markers to provide visual cue. -// On such platform, if word with marker is edited, we need to remove the marker. -#define REMOVE_MARKERS_UPON_EDITING 1 -#else -#define SUPPORT_AUTOCORRECTION_PANEL 0 -#define REMOVE_MARKERS_UPON_EDITING 0 -#endif // #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) - -#include "Range.h" - -namespace WebCore { - -struct CorrectionPanelInfo { - enum PanelType { - PanelTypeCorrection = 0, - PanelTypeReversion, - PanelTypeSpellingSuggestions - }; - - RefPtr<Range> rangeToBeReplaced; - String replacedString; - String replacementString; - PanelType panelType; - bool isActive; -}; - -enum ReasonForDismissingCorrectionPanel { - ReasonForDismissingCorrectionPanelCancelled = 0, - ReasonForDismissingCorrectionPanelIgnored, - ReasonForDismissingCorrectionPanelAccepted -}; -} // namespace WebCore - -#endif // CorrectionPanelInfo_h diff --git a/Source/WebCore/editing/DeleteButtonController.cpp b/Source/WebCore/editing/DeleteButtonController.cpp index 332e68f..5e76c08 100644 --- a/Source/WebCore/editing/DeleteButtonController.cpp +++ b/Source/WebCore/editing/DeleteButtonController.cpp @@ -112,8 +112,12 @@ static bool isDeletableElement(const Node* node) return false; // Allow blocks that have background images - if (style->hasBackgroundImage() && style->backgroundImage()->canRender(1.0f)) - return true; + if (style->hasBackgroundImage()) { + for (const FillLayer* background = style->backgroundLayers(); background; background = background->next()) { + if (background->image() && background->image()->canRender(1)) + return true; + } + } // Allow blocks with a minimum number of non-transparent borders unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible(); @@ -188,7 +192,7 @@ void DeleteButtonController::createDeletionUI() CSSMutableStyleDeclaration* style = container->getInlineStyleDecl(); style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone); style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone); - style->setProperty(CSSPropertyWebkitUserModify, CSSValueNone); + style->setProperty(CSSPropertyWebkitUserModify, CSSValueReadOnly); style->setProperty(CSSPropertyVisibility, CSSValueHidden); style->setProperty(CSSPropertyPosition, CSSValueAbsolute); style->setProperty(CSSPropertyCursor, CSSValueDefault); diff --git a/Source/WebCore/editing/DeleteSelectionCommand.cpp b/Source/WebCore/editing/DeleteSelectionCommand.cpp index cbebe54..529c71d 100644 --- a/Source/WebCore/editing/DeleteSelectionCommand.cpp +++ b/Source/WebCore/editing/DeleteSelectionCommand.cpp @@ -281,7 +281,7 @@ void DeleteSelectionCommand::saveTypingStyleState() return; // Figure out the typing style in effect before the delete is done. - m_typingStyle = EditingStyle::create(positionBeforeTabSpan(m_selectionToDelete.start())); + m_typingStyle = EditingStyle::create(m_selectionToDelete.start()); m_typingStyle->removeStyleAddedByNode(enclosingAnchorElement(m_selectionToDelete.start())); // If we're deleting into a Mail blockquote, save the style at end() instead of start() @@ -335,6 +335,15 @@ static void updatePositionForNodeRemoval(Node* node, Position& position) } } +static Position firstEditablePositionInNode(Node* node) +{ + ASSERT(node); + Node* next = node; + while (next && !next->rendererIsEditable()) + next = next->traverseNextNode(node); + return next ? firstPositionInOrBeforeNode(next) : Position(); +} + void DeleteSelectionCommand::removeNode(PassRefPtr<Node> node) { if (!node) @@ -372,11 +381,14 @@ void DeleteSelectionCommand::removeNode(PassRefPtr<Node> node) removeNode(remove); } - // make sure empty cell has some height + // Make sure empty cell has some height, if a placeholder can be inserted. updateLayout(); RenderObject *r = node->renderer(); - if (r && r->isTableCell() && toRenderTableCell(r)->contentHeight() <= 0) - insertBlockPlaceholder(firstPositionInNode(node.get())); + if (r && r->isTableCell() && toRenderTableCell(r)->contentHeight() <= 0) { + Position firstEditablePosition = firstEditablePositionInNode(node.get()); + if (firstEditablePosition.isNotNull()) + insertBlockPlaceholder(firstEditablePosition); + } return; } diff --git a/Source/WebCore/editing/EditingAllInOne.cpp b/Source/WebCore/editing/EditingAllInOne.cpp index e4e0bbb..f38cbe6 100644 --- a/Source/WebCore/editing/EditingAllInOne.cpp +++ b/Source/WebCore/editing/EditingAllInOne.cpp @@ -63,6 +63,7 @@ #include <SetNodeAttributeCommand.cpp> #include <SmartReplace.cpp> #include <SmartReplaceCF.cpp> +#include <SpellingCorrectionController.cpp> #include <SpellChecker.cpp> #include <SplitElementCommand.cpp> #include <SplitTextNodeCommand.cpp> diff --git a/Source/WebCore/editing/EditingBehavior.h b/Source/WebCore/editing/EditingBehavior.h index a367c52..4ee85f3 100644 --- a/Source/WebCore/editing/EditingBehavior.h +++ b/Source/WebCore/editing/EditingBehavior.h @@ -59,6 +59,10 @@ public: // On Mac, when processing a contextual click, the object being clicked upon should be selected. bool shouldSelectOnContextualMenuClick() const { return m_type == EditingMacBehavior; } + + // On Mac, when the web view loses focus, any active selection clears. On Windows, the selection + // should remain highlighted, just in an inactive state. + bool shouldClearSelectionWhenLosingWebPageFocus() const { return m_type == EditingMacBehavior; } private: EditingBehaviorType m_type; diff --git a/Source/WebCore/editing/EditingStyle.cpp b/Source/WebCore/editing/EditingStyle.cpp index 668c943..9509ead 100644 --- a/Source/WebCore/editing/EditingStyle.cpp +++ b/Source/WebCore/editing/EditingStyle.cpp @@ -30,6 +30,8 @@ #include "ApplyStyleCommand.h" #include "CSSComputedStyleDeclaration.h" #include "CSSMutableStyleDeclaration.h" +#include "CSSParser.h" +#include "CSSStyleSelector.h" #include "CSSValueKeywords.h" #include "CSSValueList.h" #include "Frame.h" @@ -87,6 +89,8 @@ static PassRefPtr<CSSMutableStyleDeclaration> editingStyleFromComputedStyle(Pass return copyEditingProperties(style.get()); } +static RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle); + class HTMLElementEquivalent { public: static PassOwnPtr<HTMLElementEquivalent> create(CSSPropertyID propertyID, int primitiveValue, const QualifiedName& tagName) @@ -300,6 +304,11 @@ EditingStyle::~EditingStyle() void EditingStyle::init(Node* node, PropertiesToInclude propertiesToInclude) { + if (isTabSpanTextNode(node)) + node = tabSpanNode(node)->parentNode(); + else if (isTabSpanNode(node)) + node = node->parentNode(); + RefPtr<CSSComputedStyleDeclaration> computedStyleAtPosition = computedStyle(node); m_mutableStyle = propertiesToInclude == AllProperties && computedStyleAtPosition ? computedStyleAtPosition->copy() : editingStyleFromComputedStyle(computedStyleAtPosition); @@ -768,4 +777,270 @@ void EditingStyle::mergeStyle(CSSMutableStyleDeclaration* style) } } +static void reconcileTextDecorationProperties(CSSMutableStyleDeclaration* style) +{ + RefPtr<CSSValue> textDecorationsInEffect = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + // We shouldn't have both text-decoration and -webkit-text-decorations-in-effect because that wouldn't make sense. + ASSERT(!textDecorationsInEffect || !textDecoration); + if (textDecorationsInEffect) { + style->setProperty(CSSPropertyTextDecoration, textDecorationsInEffect->cssText()); + style->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); + textDecoration = textDecorationsInEffect; + } + + // If text-decoration is set to "none", remove the property because we don't want to add redundant "text-decoration: none". + if (textDecoration && !textDecoration->isValueList()) + style->removeProperty(CSSPropertyTextDecoration); +} + +StyleChange::StyleChange(EditingStyle* style, const Position& position) + : m_applyBold(false) + , m_applyItalic(false) + , m_applyUnderline(false) + , m_applyLineThrough(false) + , m_applySubscript(false) + , m_applySuperscript(false) +{ + Document* document = position.anchorNode() ? position.anchorNode()->document() : 0; + if (!style || !style->style() || !document || !document->frame()) + return; + + RefPtr<CSSComputedStyleDeclaration> computedStyle = position.computedStyle(); + RefPtr<CSSMutableStyleDeclaration> mutableStyle = getPropertiesNotIn(style->style(), computedStyle.get()); + + reconcileTextDecorationProperties(mutableStyle.get()); + if (!document->frame()->editor()->shouldStyleWithCSS()) + extractTextStyles(document, mutableStyle.get(), computedStyle->useFixedFontDefaultSize()); + + // Changing the whitespace style in a tab span would collapse the tab into a space. + if (isTabSpanTextNode(position.deprecatedNode()) || isTabSpanNode((position.deprecatedNode()))) + mutableStyle->removeProperty(CSSPropertyWhiteSpace); + + // If unicode-bidi is present in mutableStyle and direction is not, then add direction to mutableStyle. + // FIXME: Shouldn't this be done in getPropertiesNotIn? + if (mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi) && !style->style()->getPropertyCSSValue(CSSPropertyDirection)) + mutableStyle->setProperty(CSSPropertyDirection, style->style()->getPropertyValue(CSSPropertyDirection)); + + // Save the result for later + m_cssStyle = mutableStyle->cssText().stripWhiteSpace(); +} + +static void setTextDecorationProperty(CSSMutableStyleDeclaration* style, const CSSValueList* newTextDecoration, int propertyID) +{ + if (newTextDecoration->length()) + style->setProperty(propertyID, newTextDecoration->cssText(), style->getPropertyPriority(propertyID)); + else { + // text-decoration: none is redundant since it does not remove any text decorations. + ASSERT(!style->getPropertyPriority(propertyID)); + style->removeProperty(propertyID); + } +} + +static RGBA32 getRGBAFontColor(CSSStyleDeclaration* style) +{ + RefPtr<CSSValue> colorValue = style->getPropertyCSSValue(CSSPropertyColor); + if (!colorValue || !colorValue->isPrimitiveValue()) + return Color::transparent; + + CSSPrimitiveValue* primitiveColor = static_cast<CSSPrimitiveValue*>(colorValue.get()); + if (primitiveColor->primitiveType() == CSSPrimitiveValue::CSS_RGBCOLOR) + return primitiveColor->getRGBA32Value(); + + // Need to take care of named color such as green and black + // This code should be removed after https://bugs.webkit.org/show_bug.cgi?id=28282 is fixed. + RGBA32 rgba = 0; + CSSParser::parseColor(rgba, colorValue->cssText()); + return rgba; +} + +void StyleChange::extractTextStyles(Document* document, CSSMutableStyleDeclaration* style, bool shouldUseFixedFontDefaultSize) +{ + ASSERT(style); + + if (getIdentifierValue(style, CSSPropertyFontWeight) == CSSValueBold) { + style->removeProperty(CSSPropertyFontWeight); + m_applyBold = true; + } + + int fontStyle = getIdentifierValue(style, CSSPropertyFontStyle); + if (fontStyle == CSSValueItalic || fontStyle == CSSValueOblique) { + style->removeProperty(CSSPropertyFontStyle); + m_applyItalic = true; + } + + // Assuming reconcileTextDecorationProperties has been called, there should not be -webkit-text-decorations-in-effect + // Furthermore, text-decoration: none has been trimmed so that text-decoration property is always a CSSValueList. + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + if (textDecoration && textDecoration->isValueList()) { + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + if (newTextDecoration->removeAll(underline.get())) + m_applyUnderline = true; + if (newTextDecoration->removeAll(lineThrough.get())) + m_applyLineThrough = true; + + // If trimTextDecorations, delete underline and line-through + setTextDecorationProperty(style, newTextDecoration.get(), CSSPropertyTextDecoration); + } + + int verticalAlign = getIdentifierValue(style, CSSPropertyVerticalAlign); + switch (verticalAlign) { + case CSSValueSub: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySubscript = true; + break; + case CSSValueSuper: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySuperscript = true; + break; + } + + if (style->getPropertyCSSValue(CSSPropertyColor)) { + m_applyFontColor = Color(getRGBAFontColor(style)).serialized(); + style->removeProperty(CSSPropertyColor); + } + + m_applyFontFace = style->getPropertyValue(CSSPropertyFontFamily); + style->removeProperty(CSSPropertyFontFamily); + + if (RefPtr<CSSValue> fontSize = style->getPropertyCSSValue(CSSPropertyFontSize)) { + if (!fontSize->isPrimitiveValue()) + style->removeProperty(CSSPropertyFontSize); // Can't make sense of the number. Put no font size. + else if (int legacyFontSize = legacyFontSizeFromCSSValue(document, static_cast<CSSPrimitiveValue*>(fontSize.get()), + shouldUseFixedFontDefaultSize, UseLegacyFontSizeOnlyIfPixelValuesMatch)) { + m_applyFontSize = String::number(legacyFontSize); + style->removeProperty(CSSPropertyFontSize); + } + } +} + +static void diffTextDecorations(CSSMutableStyleDeclaration* style, int propertID, CSSValue* refTextDecoration) +{ + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(propertID); + if (!textDecoration || !textDecoration->isValueList() || !refTextDecoration || !refTextDecoration->isValueList()) + return; + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + CSSValueList* valuesInRefTextDecoration = static_cast<CSSValueList*>(refTextDecoration); + + for (size_t i = 0; i < valuesInRefTextDecoration->length(); i++) + newTextDecoration->removeAll(valuesInRefTextDecoration->item(i)); + + setTextDecorationProperty(style, newTextDecoration.get(), propertID); +} + +static bool fontWeightIsBold(CSSStyleDeclaration* style) +{ + ASSERT(style); + RefPtr<CSSValue> fontWeight = style->getPropertyCSSValue(CSSPropertyFontWeight); + + if (!fontWeight) + return false; + if (!fontWeight->isPrimitiveValue()) + return false; + + // Because b tag can only bold text, there are only two states in plain html: bold and not bold. + // Collapse all other values to either one of these two states for editing purposes. + switch (static_cast<CSSPrimitiveValue*>(fontWeight.get())->getIdent()) { + case CSSValue100: + case CSSValue200: + case CSSValue300: + case CSSValue400: + case CSSValue500: + case CSSValueNormal: + return false; + case CSSValueBold: + case CSSValue600: + case CSSValue700: + case CSSValue800: + case CSSValue900: + return true; + } + + ASSERT_NOT_REACHED(); // For CSSValueBolder and CSSValueLighter + return false; // Make compiler happy +} + +static int getTextAlignment(CSSStyleDeclaration* style) +{ + int textAlign = getIdentifierValue(style, CSSPropertyTextAlign); + switch (textAlign) { + case CSSValueCenter: + case CSSValueWebkitCenter: + return CSSValueCenter; + case CSSValueJustify: + return CSSValueJustify; + case CSSValueLeft: + case CSSValueWebkitLeft: + return CSSValueLeft; + case CSSValueRight: + case CSSValueWebkitRight: + return CSSValueRight; + } + return CSSValueInvalid; +} + +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle) +{ + ASSERT(styleWithRedundantProperties); + ASSERT(baseStyle); + RefPtr<CSSMutableStyleDeclaration> result = styleWithRedundantProperties->copy(); + baseStyle->diff(result.get()); + + RefPtr<CSSValue> baseTextDecorationsInEffect = baseStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + diffTextDecorations(result.get(), CSSPropertyTextDecoration, baseTextDecorationsInEffect.get()); + diffTextDecorations(result.get(), CSSPropertyWebkitTextDecorationsInEffect, baseTextDecorationsInEffect.get()); + + if (fontWeightIsBold(result.get()) == fontWeightIsBold(baseStyle)) + result->removeProperty(CSSPropertyFontWeight); + + if (getRGBAFontColor(result.get()) == getRGBAFontColor(baseStyle)) + result->removeProperty(CSSPropertyColor); + + if (getTextAlignment(result.get()) == getTextAlignment(baseStyle)) + result->removeProperty(CSSPropertyTextAlign); + + return result; +} + + +int getIdentifierValue(CSSStyleDeclaration* style, int propertyID) +{ + if (!style) + return 0; + + RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); + if (!value || !value->isPrimitiveValue()) + return 0; + + return static_cast<CSSPrimitiveValue*>(value.get())->getIdent(); +} + +static bool isCSSValueLength(CSSPrimitiveValue* value) +{ + return value->primitiveType() >= CSSPrimitiveValue::CSS_PX && value->primitiveType() <= CSSPrimitiveValue::CSS_PC; +} + +int legacyFontSizeFromCSSValue(Document* document, CSSPrimitiveValue* value, bool shouldUseFixedFontDefaultSize, LegacyFontSizeMode mode) +{ + if (isCSSValueLength(value)) { + int pixelFontSize = value->getIntValue(CSSPrimitiveValue::CSS_PX); + int legacyFontSize = CSSStyleSelector::legacyFontSize(document, pixelFontSize, shouldUseFixedFontDefaultSize); + // Use legacy font size only if pixel value matches exactly to that of legacy font size. + int cssPrimitiveEquivalent = legacyFontSize - 1 + CSSValueXSmall; + if (mode == AlwaysUseLegacyFontSize || CSSStyleSelector::fontSizeForKeyword(document, cssPrimitiveEquivalent, shouldUseFixedFontDefaultSize) == pixelFontSize) + return legacyFontSize; + + return 0; + } + + if (CSSValueXSmall <= value->getIdent() && value->getIdent() <= CSSValueWebkitXxxLarge) + return value->getIdent() - CSSValueXSmall + 1; + + return 0; +} + } diff --git a/Source/WebCore/editing/EditingStyle.h b/Source/WebCore/editing/EditingStyle.h index 37bfea7..257a2f9 100644 --- a/Source/WebCore/editing/EditingStyle.h +++ b/Source/WebCore/editing/EditingStyle.h @@ -32,6 +32,7 @@ #define EditingStyle_h #include "CSSPropertyNames.h" +#include "PlatformString.h" #include "WritingDirection.h" #include <wtf/Forward.h> #include <wtf/RefCounted.h> @@ -43,6 +44,7 @@ namespace WebCore { class CSSStyleDeclaration; class CSSComputedStyleDeclaration; class CSSMutableStyleDeclaration; +class CSSPrimitiveValue; class Document; class HTMLElement; class Node; @@ -144,6 +146,62 @@ private: friend class HTMLAttributeEquivalent; }; +class StyleChange { +public: + StyleChange(EditingStyle*, const Position&); + + String cssStyle() const { return m_cssStyle; } + bool applyBold() const { return m_applyBold; } + bool applyItalic() const { return m_applyItalic; } + bool applyUnderline() const { return m_applyUnderline; } + bool applyLineThrough() const { return m_applyLineThrough; } + bool applySubscript() const { return m_applySubscript; } + bool applySuperscript() const { return m_applySuperscript; } + bool applyFontColor() const { return m_applyFontColor.length() > 0; } + bool applyFontFace() const { return m_applyFontFace.length() > 0; } + bool applyFontSize() const { return m_applyFontSize.length() > 0; } + + String fontColor() { return m_applyFontColor; } + String fontFace() { return m_applyFontFace; } + String fontSize() { return m_applyFontSize; } + + bool operator==(const StyleChange& other) + { + return m_cssStyle == other.m_cssStyle + && m_applyBold == other.m_applyBold + && m_applyItalic == other.m_applyItalic + && m_applyUnderline == other.m_applyUnderline + && m_applyLineThrough == other.m_applyLineThrough + && m_applySubscript == other.m_applySubscript + && m_applySuperscript == other.m_applySuperscript + && m_applyFontColor == other.m_applyFontColor + && m_applyFontFace == other.m_applyFontFace + && m_applyFontSize == other.m_applyFontSize; + } + bool operator!=(const StyleChange& other) + { + return !(*this == other); + } +private: + void extractTextStyles(Document*, CSSMutableStyleDeclaration*, bool shouldUseFixedFontDefaultSize); + + String m_cssStyle; + bool m_applyBold; + bool m_applyItalic; + bool m_applyUnderline; + bool m_applyLineThrough; + bool m_applySubscript; + bool m_applySuperscript; + String m_applyFontColor; + String m_applyFontFace; + String m_applyFontSize; +}; + +// FIXME: Remove these functions or make them non-global to discourage using CSSStyleDeclaration directly. +int getIdentifierValue(CSSStyleDeclaration*, int propertyID); +enum LegacyFontSizeMode { AlwaysUseLegacyFontSize, UseLegacyFontSizeOnlyIfPixelValuesMatch }; +int legacyFontSizeFromCSSValue(Document*, CSSPrimitiveValue*, bool shouldUseFixedFontDefaultSize, LegacyFontSizeMode); + } // namespace WebCore #endif // EditingStyle_h diff --git a/Source/WebCore/editing/Editor.cpp b/Source/WebCore/editing/Editor.cpp index a793e0b..e7c5c1d 100644 --- a/Source/WebCore/editing/Editor.cpp +++ b/Source/WebCore/editing/Editor.cpp @@ -38,6 +38,7 @@ #include "CachedResourceLoader.h" #include "ClipboardEvent.h" #include "CompositionEvent.h" +#include "SpellingCorrectionController.h" #include "CreateLinkCommand.h" #include "DeleteButtonController.h" #include "DeleteSelectionCommand.h" @@ -64,7 +65,6 @@ #include "NodeList.h" #include "Page.h" #include "Pasteboard.h" -#include "TextCheckerClient.h" #include "TextCheckingHelper.h" #include "RemoveFormatCommand.h" #include "RenderBlock.h" @@ -94,35 +94,6 @@ using namespace HTMLNames; using namespace WTF; using namespace Unicode; -static inline bool isAmbiguousBoundaryCharacter(UChar character) -{ - // These are characters that can behave as word boundaries, but can appear within words. - // If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed. - // FIXME: this is required until 6853027 is fixed and text checking can do this for us. - return character == '\'' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim; -} - -static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection() -{ - DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForAutoCorrection, ()); - if (markerTypesForAutoCorrection.isEmpty()) { - markerTypesForAutoCorrection.append(DocumentMarker::Replacement); - markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator); - markerTypesForAutoCorrection.append(DocumentMarker::SpellCheckingExemption); - } - return markerTypesForAutoCorrection; -} - -static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement() -{ - DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ()); - if (markerTypesForReplacement.isEmpty()) { - markerTypesForReplacement.append(DocumentMarker::Replacement); - markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption); - } - return markerTypesForReplacement; -} - // When an event handler has moved the selection outside of a text control // we should use the target control's selection for this editing operation. VisibleSelection Editor::selectionForCommand(Event* event) @@ -210,7 +181,7 @@ bool Editor::handleTextEvent(TextEvent* event) bool Editor::canEdit() const { - return m_frame->selection()->isContentEditable(); + return m_frame->selection()->rootEditableElement(); } bool Editor::canEditRichly() const @@ -278,7 +249,7 @@ bool Editor::canPaste() const bool Editor::canDelete() const { SelectionController* selection = m_frame->selection(); - return selection->isRange() && selection->isContentEditable(); + return selection->isRange() && selection->rootEditableElement(); } bool Editor::canDeleteRange(Range* range) const @@ -324,7 +295,7 @@ bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity g if (m_frame->selection()->isRange()) { if (isTypingAction) { - TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity); + TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete() ? TypingCommand::SmartDelete : 0, granularity); revealSelectionAfterEditingOperation(); } else { if (killRing) @@ -333,14 +304,19 @@ bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity g // Implicitly calls revealSelectionAfterEditingOperation(). } } else { + TypingCommand::Options options = 0; + if (canSmartCopyOrDelete()) + options |= TypingCommand::SmartDelete; + if (killRing) + options |= TypingCommand::KillRing; switch (direction) { case DirectionForward: case DirectionRight: - TypingCommand::forwardDeleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + TypingCommand::forwardDeleteKeyPressed(m_frame->document(), options, granularity); break; case DirectionBackward: case DirectionLeft: - TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + TypingCommand::deleteKeyPressed(m_frame->document(), options, granularity); break; } revealSelectionAfterEditingOperation(); @@ -440,7 +416,7 @@ void Editor::replaceSelectionWithFragment(PassRefPtr<DocumentFragment> fragment, Node* nodeToCheck = m_frame->selection()->rootEditableElement(); if (m_spellChecker->canCheckAsynchronously(nodeToCheck)) - m_spellChecker->requestCheckingFor(nodeToCheck); + m_spellChecker->requestCheckingFor(textCheckingTypeMaskFor(MarkSpelling | MarkGrammar), nodeToCheck); } void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace) @@ -518,48 +494,7 @@ void Editor::respondToChangedSelection(const VisibleSelection& oldSelection) if (client()) client()->respondToChangedSelection(); m_deleteButtonController->respondToChangedSelection(oldSelection); - -#if SUPPORT_AUTOCORRECTION_PANEL - VisibleSelection currentSelection(frame()->selection()->selection()); - // When user moves caret to the end of autocorrected word and pauses, we show the panel - // containing the original pre-correction word so that user can quickly revert the - // undesired autocorrection. Here, we start correction panel timer once we confirm that - // the new caret position is at the end of a word. - if (!currentSelection.isCaret() || currentSelection == oldSelection) - return; - - VisiblePosition selectionPosition = currentSelection.start(); - VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); - if (selectionPosition != endPositionOfWord) - return; - - Position position = endPositionOfWord.deepEquivalent(); - if (position.anchorType() != Position::PositionIsOffsetInAnchor) - return; - - Node* node = position.containerNode(); - int endOffset = position.offsetInContainerNode(); - Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); - size_t markerCount = markers.size(); - for (size_t i = 0; i < markerCount; ++i) { - const DocumentMarker& marker = markers[i]; - if (((marker.type == DocumentMarker::Replacement && !marker.description.isNull()) || marker.type == DocumentMarker::Spelling) && static_cast<int>(marker.endOffset) == endOffset) { - RefPtr<Range> wordRange = Range::create(frame()->document(), node, marker.startOffset, node, marker.endOffset); - String currentWord = plainText(wordRange.get()); - if (currentWord.length()) { - m_correctionPanelInfo.rangeToBeReplaced = wordRange; - m_correctionPanelInfo.replacedString = currentWord; - if (marker.type == DocumentMarker::Spelling) - startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions); - else { - m_correctionPanelInfo.replacementString = marker.description; - startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion); - } - } - break; - } - } -#endif // SUPPORT_AUTOCORRECTION_PANEL + m_spellingCorrector->respondToChangedSelection(oldSelection); } void Editor::respondToChangedContents(const VisibleSelection& endingSelection) @@ -570,9 +505,7 @@ void Editor::respondToChangedContents(const VisibleSelection& endingSelection) m_frame->document()->axObjectCache()->postNotification(node->renderer(), AXObjectCache::AXValueChanged, false); } -#if REMOVE_MARKERS_UPON_EDITING - removeSpellAndCorrectionMarkersFromWordsToBeEdited(true); -#endif + updateMarkersForWordsAffectedByEditing(true); if (client()) client()->respondToChangedContents(); @@ -983,7 +916,7 @@ TriState Editor::selectionHasStyle(int propertyID, const String& value) const return state; } -static bool hasTransparentBackgroundColor(CSSStyleDeclaration* style) +bool Editor::hasTransparentBackgroundColor(CSSStyleDeclaration* style) { RefPtr<CSSValue> cssValue = style->getPropertyCSSValue(CSSPropertyBackgroundColor); if (!cssValue) @@ -1059,10 +992,7 @@ void Editor::appliedEditing(PassRefPtr<EditCommand> cmd) dispatchEditableContentChangedEvents(*cmd); VisibleSelection newSelection(cmd->endingSelection()); -#if SUPPORT_AUTOCORRECTION_PANEL - if (cmd->isTopLevelCommand() && !cmd->shouldRetainAutocorrectionIndicator()) - m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); -#endif + m_spellingCorrector->respondToAppliedEditing(cmd.get()); // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. changeSelectionAfterCommand(newSelection, false, false); @@ -1121,17 +1051,14 @@ Editor::Editor(Frame* frame) // This is off by default, since most editors want this behavior (this matches IE but not FF). , m_shouldStyleWithCSS(false) , m_killRing(adoptPtr(new KillRing)) - , m_spellChecker(new SpellChecker(frame, frame->page() ? frame->page()->editorClient()->textChecker() : 0)) - , m_correctionPanelTimer(this, &Editor::correctionPanelTimerFired) + , m_spellChecker(adoptPtr(new SpellChecker(frame, frame->page() ? frame->page()->editorClient()->textChecker() : 0))) + , m_spellingCorrector(adoptPtr(new SpellingCorrectionController(frame))) , m_areMarkedTextMatchesHighlighted(false) { } Editor::~Editor() { -#if SUPPORT_AUTOCORRECTION_PANEL - dismissCorrectionPanel(ReasonForDismissingCorrectionPanelIgnored); -#endif } void Editor::clear() @@ -1164,10 +1091,8 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn if (!shouldInsertText(text, range.get(), EditorInsertActionTyped)) return true; -#if REMOVE_MARKERS_UPON_EDITING if (!text.isEmpty()) - removeSpellAndCorrectionMarkersFromWordsToBeEdited(isSpaceOrNewline(text[0])); -#endif + updateMarkersForWordsAffectedByEditing(text[0]); bool shouldConsiderApplyingAutocorrection = false; if (text == " " || text == "\t") @@ -1176,7 +1101,7 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn if (text.length() == 1 && isPunct(text[0]) && !isAmbiguousBoundaryCharacter(text[0])) shouldConsiderApplyingAutocorrection = true; - bool autocorrectionWasApplied = shouldConsiderApplyingAutocorrection && applyAutocorrectionBeforeTypingIfAppropriate(); + bool autocorrectionWasApplied = shouldConsiderApplyingAutocorrection && m_spellingCorrector->applyAutocorrectionBeforeTypingIfAppropriate(); // Get the selection to use for the event that triggered this insertText. // If the event handler changed the selection, we may want to use a different selection @@ -1187,7 +1112,7 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn RefPtr<Document> document = selectionStart->document(); // Insert the text - TypingCommand::TypingCommandOptions options = 0; + TypingCommand::Options options = 0; if (selectInsertedText) options |= TypingCommand::SelectInsertedText; if (autocorrectionWasApplied) @@ -1212,7 +1137,7 @@ bool Editor::insertLineBreak() if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) return true; - bool autocorrectionIsApplied = applyAutocorrectionBeforeTypingIfAppropriate(); + bool autocorrectionIsApplied = m_spellingCorrector->applyAutocorrectionBeforeTypingIfAppropriate(); TypingCommand::insertLineBreak(m_frame->document(), autocorrectionIsApplied ? TypingCommand::RetainAutocorrectionIndicator : 0); revealSelectionAfterEditingOperation(); @@ -1230,7 +1155,7 @@ bool Editor::insertParagraphSeparator() if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) return true; - bool autocorrectionIsApplied = applyAutocorrectionBeforeTypingIfAppropriate(); + bool autocorrectionIsApplied = m_spellingCorrector->applyAutocorrectionBeforeTypingIfAppropriate(); TypingCommand::insertParagraphSeparator(m_frame->document(), autocorrectionIsApplied ? TypingCommand::RetainAutocorrectionIndicator : 0); revealSelectionAfterEditingOperation(); @@ -1247,9 +1172,7 @@ void Editor::cut() } RefPtr<Range> selection = selectedRange(); if (shouldDeleteRange(selection.get())) { -#if REMOVE_MARKERS_UPON_EDITING - removeSpellAndCorrectionMarkersFromWordsToBeEdited(true); -#endif + updateMarkersForWordsAffectedByEditing(true); if (isNodeInTextFormControl(m_frame->selection()->start().deprecatedNode())) Pasteboard::generalPasteboard()->writePlainText(selectedText()); else @@ -1288,9 +1211,7 @@ void Editor::paste() return; // DHTML did the whole operation if (!canPaste()) return; -#if REMOVE_MARKERS_UPON_EDITING - removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); -#endif + updateMarkersForWordsAffectedByEditing(false); CachedResourceLoader* loader = m_frame->document()->cachedResourceLoader(); loader->setAllowStaleResources(true); if (m_frame->selection()->isContentRichlyEditable()) @@ -1306,9 +1227,7 @@ void Editor::pasteAsPlainText() return; if (!canPaste()) return; -#if REMOVE_MARKERS_UPON_EDITING - removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); -#endif + updateMarkersForWordsAffectedByEditing(false); pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); } @@ -1461,7 +1380,7 @@ void Editor::toggleAutomaticTextReplacement() bool Editor::isAutomaticSpellingCorrectionEnabled() { - return client() && client()->isAutomaticSpellingCorrectionEnabled(); + return m_spellingCorrector->isAutomaticSpellingCorrectionEnabled(); } void Editor::toggleAutomaticSpellingCorrection() @@ -1614,7 +1533,7 @@ void Editor::confirmComposition(const String& text, bool preserveSelection) // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input // will delete the old composition with an optimized replace operation. if (text.isEmpty()) - TypingCommand::deleteSelection(m_frame->document(), false); + TypingCommand::deleteSelection(m_frame->document(), 0); m_compositionNode = 0; m_customCompositionUnderlines.clear(); @@ -1682,13 +1601,13 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input // will delete the old composition with an optimized replace operation. if (text.isEmpty()) - TypingCommand::deleteSelection(m_frame->document(), false); + TypingCommand::deleteSelection(m_frame->document(), TypingCommand::PreventSpellChecking); m_compositionNode = 0; m_customCompositionUnderlines.clear(); if (!text.isEmpty()) { - TypingCommand::insertText(m_frame->document(), text, true, TypingCommand::TextCompositionUpdate); + TypingCommand::insertText(m_frame->document(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionUpdate); // Find out what node has the composition now. Position base = m_frame->selection()->base().downstream(); @@ -2050,39 +1969,24 @@ void Editor::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelecti markBadGrammar(movingSelection); } -void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping) +void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping, bool doReplacement) { #if USE(UNIFIED_TEXT_CHECKING) -#if SUPPORT_AUTOCORRECTION_PANEL - // Apply pending autocorrection before next round of spell checking. - bool doApplyCorrection = true; - VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); - VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); - if (currentWord.visibleEnd() == startOfSelection) { - String wordText = plainText(currentWord.toNormalizedRange().get()); - if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) - doApplyCorrection = false; - } - if (doApplyCorrection) - handleCorrectionPanelResult(dismissCorrectionPanelSoon(ReasonForDismissingCorrectionPanelAccepted)); - else - m_correctionPanelInfo.rangeToBeReplaced.clear(); -#else - UNUSED_PARAM(selectionAfterTyping); -#endif + m_spellingCorrector->applyPendingCorrection(selectionAfterTyping); + TextCheckingOptions textCheckingOptions = 0; if (isContinuousSpellCheckingEnabled()) textCheckingOptions |= MarkSpelling; #if USE(AUTOMATIC_TEXT_REPLACEMENT) - if (isAutomaticQuoteSubstitutionEnabled() - || isAutomaticLinkDetectionEnabled() - || isAutomaticDashSubstitutionEnabled() - || isAutomaticTextReplacementEnabled() - || ((textCheckingOptions & MarkSpelling) && isAutomaticSpellingCorrectionEnabled())) + if (doReplacement + && (isAutomaticQuoteSubstitutionEnabled() + || isAutomaticLinkDetectionEnabled() + || isAutomaticDashSubstitutionEnabled() + || isAutomaticTextReplacementEnabled() + || ((textCheckingOptions & MarkSpelling) && isAutomaticSpellingCorrectionEnabled()))) textCheckingOptions |= PerformReplacement; #endif - if (!textCheckingOptions & (MarkSpelling | PerformReplacement)) return; @@ -2099,6 +2003,8 @@ void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, #else UNUSED_PARAM(selectionAfterTyping); + UNUSED_PARAM(doReplacement); + if (!isContinuousSpellCheckingEnabled()) return; @@ -2207,12 +2113,13 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh { #if USE(UNIFIED_TEXT_CHECKING) // There shouldn't be pending autocorrection at this moment. - ASSERT(!m_correctionPanelInfo.rangeToBeReplaced); + ASSERT(!m_spellingCorrector->hasPendingCorrection()); bool shouldMarkSpelling = textCheckingOptions & MarkSpelling; bool shouldMarkGrammar = textCheckingOptions & MarkGrammar; bool shouldPerformReplacement = textCheckingOptions & PerformReplacement; bool shouldShowCorrectionPanel = textCheckingOptions & ShowCorrectionPanel; + bool shouldCheckForCorrection = shouldShowCorrectionPanel || (textCheckingOptions & CheckForCorrection); // This function is called with selections already expanded to word boundaries. ExceptionCode ec = 0; @@ -2240,7 +2147,7 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh if (shouldMarkGrammar ? (spellingParagraph.isRangeEmpty() && grammarParagraph.isEmpty()) : spellingParagraph.isEmpty()) return; - if (shouldPerformReplacement || shouldMarkSpelling) { + if (shouldPerformReplacement || shouldMarkSpelling || shouldCheckForCorrection) { if (m_frame->selection()->selectionType() == VisibleSelection::CaretSelection) { // Attempt to save the caret position so we can restore it later if needed Position caretPosition = m_frame->selection()->end(); @@ -2257,38 +2164,17 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh } Vector<TextCheckingResult> results; - uint64_t checkingTypes = 0; - if (shouldMarkSpelling) - checkingTypes |= TextCheckingTypeSpelling; - if (shouldMarkGrammar) - checkingTypes |= TextCheckingTypeGrammar; - if (shouldShowCorrectionPanel) - checkingTypes |= TextCheckingTypeCorrection; - if (shouldPerformReplacement) { -#if USE(AUTOMATIC_TEXT_REPLACEMENT) - if (isAutomaticLinkDetectionEnabled()) - checkingTypes |= TextCheckingTypeLink; - if (isAutomaticQuoteSubstitutionEnabled()) - checkingTypes |= TextCheckingTypeQuote; - if (isAutomaticDashSubstitutionEnabled()) - checkingTypes |= TextCheckingTypeDash; - if (isAutomaticTextReplacementEnabled()) - checkingTypes |= TextCheckingTypeReplacement; - if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) - checkingTypes |= TextCheckingTypeCorrection; -#endif - } if (shouldMarkGrammar) - textChecker()->checkTextOfParagraph(grammarParagraph.textCharacters(), grammarParagraph.textLength(), checkingTypes, results); + textChecker()->checkTextOfParagraph(grammarParagraph.textCharacters(), grammarParagraph.textLength(), + textCheckingTypeMaskFor(textCheckingOptions), results); else - textChecker()->checkTextOfParagraph(spellingParagraph.textCharacters(), spellingParagraph.textLength(), checkingTypes, results); + textChecker()->checkTextOfParagraph(spellingParagraph.textCharacters(), spellingParagraph.textLength(), + textCheckingTypeMaskFor(textCheckingOptions), results); -#if SUPPORT_AUTOCORRECTION_PANEL // If this checking is only for showing correction panel, we shouldn't bother to mark misspellings. if (shouldShowCorrectionPanel) shouldMarkSpelling = false; -#endif int offsetDueToReplacement = 0; @@ -2307,10 +2193,8 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh if (shouldMarkSpelling && result->type == TextCheckingTypeSpelling && resultLocation >= spellingParagraph.checkingStart() && resultLocation + resultLength <= spellingRangeEndOffset && !resultEndsAtAmbiguousBoundary) { ASSERT(resultLength > 0 && resultLocation >= 0); RefPtr<Range> misspellingRange = spellingParagraph.subrange(resultLocation, resultLength); -#if SUPPORT_AUTOCORRECTION_PANEL - if (m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption)) + if (!m_spellingCorrector->isSpellingMarkerAllowed(misspellingRange)) continue; -#endif // SUPPORT_AUTOCORRECTION_PANEL misspellingRange->startContainer(ec)->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); } else if (shouldMarkGrammar && result->type == TextCheckingTypeGrammar && grammarParagraph.checkingRangeCovers(resultLocation, resultLength)) { ASSERT(resultLength > 0 && resultLocation >= 0); @@ -2322,7 +2206,7 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh grammarRange->startContainer(ec)->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); } } - } else if ((shouldPerformReplacement || shouldShowCorrectionPanel) && resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingParagraph.checkingStart() + } else if (resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingParagraph.checkingStart() && (result->type == TextCheckingTypeLink || result->type == TextCheckingTypeQuote || result->type == TextCheckingTypeDash @@ -2348,45 +2232,33 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh if (result->type == TextCheckingTypeLink && selectionOffset > resultLocation + resultLength + 1) continue; + String replacedString; + // Don't correct spelling in an already-corrected word. if (result->type == TextCheckingTypeCorrection) { - Node* node = rangeToReplace->startContainer(); - int startOffset = rangeToReplace->startOffset(); - int endOffset = startOffset + replacementLength; - Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); - size_t markerCount = markers.size(); - for (size_t i = 0; i < markerCount; ++i) { - const DocumentMarker& marker = markers[i]; - if ((marker.type == DocumentMarker::Replacement || marker.type == DocumentMarker::RejectedCorrection) && static_cast<int>(marker.startOffset) < endOffset && static_cast<int>(marker.endOffset) > startOffset) { - doReplacement = false; - break; - } - if (static_cast<int>(marker.startOffset) >= endOffset) - break; - } + replacedString = plainText(rangeToReplace.get()); + DocumentMarkerController* markers = m_frame->document()->markers(); + if (markers->hasMarkers(rangeToReplace.get(), DocumentMarker::Replacement)) { + doReplacement = false; + m_spellingCorrector->recordSpellcheckerResponseForModifiedCorrection(rangeToReplace.get(), replacedString, result->replacement); + } else if (markers->hasMarkers(rangeToReplace.get(), DocumentMarker::RejectedCorrection)) + doReplacement = false; } - if (!doReplacement) + if (!(shouldPerformReplacement || shouldShowCorrectionPanel) || !doReplacement) continue; -#if SUPPORT_AUTOCORRECTION_PANEL if (shouldShowCorrectionPanel) { + ASSERT(SUPPORT_AUTOCORRECTION_PANEL); + // shouldShowCorrectionPanel can be true only when the panel is available. if (resultLocation + resultLength == spellingRangeEndOffset) { // We only show the correction panel on the last word. - FloatRect boundingBox = windowRectForRange(rangeToReplace.get()); - if (boundingBox.isEmpty()) - break; - m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace; - m_correctionPanelInfo.replacedString = plainText(rangeToReplace.get()); - m_correctionPanelInfo.replacementString = result->replacement; - m_correctionPanelInfo.isActive = true; - client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, result->replacement, Vector<String>()); + m_spellingCorrector->show(rangeToReplace, result->replacement); break; } // If this function is called for showing correction panel, we ignore other correction or replacement. continue; } -#endif if (selectionToReplace != m_frame->selection()->selection()) { if (!m_frame->selection()->shouldChangeSelection(selectionToReplace)) @@ -2400,22 +2272,18 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh if (canEditRichly()) applyCommand(CreateLinkCommand::create(m_frame->document(), result->replacement)); } else if (canEdit() && shouldInsertText(result->replacement, rangeToReplace.get(), EditorInsertActionTyped)) { - String replacedString; if (result->type == TextCheckingTypeCorrection) - replacedString = plainText(rangeToReplace.get()); - - bool useSpellingCorrectionCommand = false; -#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) - if (result->type == TextCheckingTypeCorrection) - useSpellingCorrectionCommand = true; -#endif - if (useSpellingCorrectionCommand) applyCommand(SpellingCorrectionCommand::create(rangeToReplace, result->replacement)); else { m_frame->selection()->setSelection(selectionToReplace); replaceSelectionWithText(result->replacement, false, false); } + if (AXObjectCache::accessibilityEnabled()) { + if (Element* root = m_frame->selection()->selection().rootEditableElement()) + m_frame->document()->axObjectCache()->postNotification(root->renderer(), AXObjectCache::AXAutocorrectionOccured, true); + } + selectionChanged = true; offsetDueToReplacement += replacementLength - resultLength; if (resultLocation < selectionOffset) { @@ -2424,13 +2292,9 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCh ambiguousBoundaryOffset = selectionOffset - 1; } - if (result->type == TextCheckingTypeCorrection) { - // Add a marker so that corrections can easily be undone and won't be re-corrected. - RefPtr<Range> replacedRange = spellingParagraph.subrange(resultLocation, replacementLength); - replacedRange->startContainer()->document()->markers()->addMarker(replacedRange.get(), DocumentMarker::Replacement, replacedString); - replacedRange->startContainer()->document()->markers()->addMarker(replacedRange.get(), DocumentMarker::CorrectionIndicator); - replacedRange->startContainer()->document()->markers()->addMarker(replacedRange.get(), DocumentMarker::SpellCheckingExemption); - } + // Add a marker so that corrections can easily be undone and won't be re-corrected. + if (result->type == TextCheckingTypeCorrection) + m_spellingCorrector->markCorrection(spellingParagraph.subrange(resultLocation, replacementLength), replacedString); } } } @@ -2467,17 +2331,12 @@ void Editor::changeBackToReplacedString(const String& replacedString) if (!shouldInsertText(replacedString, selection.get(), EditorInsertActionPasted)) return; -#if SUPPORT_AUTOCORRECTION_PANEL - String replacement = plainText(selection.get()); - client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacement); -#endif + m_spellingCorrector->recordAutocorrectionResponseReversed(replacedString, selection); TextCheckingParagraph paragraph(selection); replaceSelectionWithText(replacedString, false, false); RefPtr<Range> changedRange = paragraph.subrange(paragraph.checkingStart(), replacedString.length()); changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::Replacement, String()); -#if SUPPORT_AUTOCORRECTION_PANEL - changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption); -#endif + m_spellingCorrector->markReversed(changedRange.get()); #else ASSERT_NOT_REACHED(); UNUSED_PARAM(replacedString); @@ -2490,7 +2349,7 @@ void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelec #if USE(UNIFIED_TEXT_CHECKING) if (!isContinuousSpellCheckingEnabled()) return; - TextCheckingOptions textCheckingOptions = MarkSpelling; + TextCheckingOptions textCheckingOptions = MarkSpelling | CheckForCorrection; if (markGrammar && isGrammarCheckingEnabled()) textCheckingOptions |= MarkGrammar; markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellingSelection.toNormalizedRange().get(), grammarSelection.toNormalizedRange().get()); @@ -2502,140 +2361,16 @@ void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelec #endif } -void Editor::correctionPanelTimerFired(Timer<Editor>*) -{ -#if SUPPORT_AUTOCORRECTION_PANEL - m_correctionPanelIsDismissedByEditor = false; - switch (m_correctionPanelInfo.panelType) { - case CorrectionPanelInfo::PanelTypeCorrection: { - VisibleSelection selection(frame()->selection()->selection()); - VisiblePosition start(selection.start(), selection.affinity()); - VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); - VisibleSelection adjacentWords = VisibleSelection(p, start); - markAllMisspellingsAndBadGrammarInRanges(MarkSpelling | ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); - } - break; - case CorrectionPanelInfo::PanelTypeReversion: { - m_correctionPanelInfo.isActive = true; - m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); - FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); - if (!boundingBox.isEmpty()) - client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector<String>()); - } - break; - case CorrectionPanelInfo::PanelTypeSpellingSuggestions: { - if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString) - break; - String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get()); - Vector<String> suggestions; - textChecker()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions); - if (suggestions.isEmpty()) { - m_correctionPanelInfo.rangeToBeReplaced.clear(); - break; - } - String topSuggestion = suggestions.first(); - suggestions.remove(0); - m_correctionPanelInfo.isActive = true; - FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); - if (!boundingBox.isEmpty()) - client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, topSuggestion, suggestions); - } - break; - } -#endif -} - -void Editor::handleCorrectionPanelResult(const String& correction) -{ - Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get(); - if (!replacedRange || m_frame->document() != replacedRange->ownerDocument()) - return; - - String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); - // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. - if (currentWord != m_correctionPanelInfo.replacedString) - return; - - m_correctionPanelInfo.isActive = false; - - switch (m_correctionPanelInfo.panelType) { - case CorrectionPanelInfo::PanelTypeCorrection: - if (correction.length()) { - m_correctionPanelInfo.replacementString = correction; - applyCorrectionPanelInfo(markerTypesForAutocorrection()); - } else { - if (!m_correctionPanelIsDismissedByEditor) - replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString); - } - break; - case CorrectionPanelInfo::PanelTypeReversion: - case CorrectionPanelInfo::PanelTypeSpellingSuggestions: - if (correction.length()) { - m_correctionPanelInfo.replacementString = correction; - applyCorrectionPanelInfo(markerTypesForReplacement()); - } - break; - } - - m_correctionPanelInfo.rangeToBeReplaced.clear(); -} - -void Editor::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type) -{ -#if SUPPORT_AUTOCORRECTION_PANEL - const double correctionPanelTimerInterval = 0.3; - if (isAutomaticSpellingCorrectionEnabled()) { - if (type == CorrectionPanelInfo::PanelTypeCorrection) - // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. - m_correctionPanelInfo.rangeToBeReplaced.clear(); - m_correctionPanelInfo.panelType = type; - m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval); - } -#else - UNUSED_PARAM(type); -#endif -} - -void Editor::stopCorrectionPanelTimer() +void Editor::unappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) { -#if SUPPORT_AUTOCORRECTION_PANEL - m_correctionPanelTimer.stop(); - m_correctionPanelInfo.rangeToBeReplaced.clear(); -#endif + m_spellingCorrector->respondToUnappliedSpellCorrection(selectionOfCorrected, corrected, correction); } -void Editor::dismissCorrectionPanel(ReasonForDismissingCorrectionPanel reasonForDismissing) +void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionAtWordBoundary) { -#if SUPPORT_AUTOCORRECTION_PANEL - if (!m_correctionPanelInfo.isActive) + if (!m_spellingCorrector->shouldRemoveMarkersUponEditing()) return; - m_correctionPanelInfo.isActive = false; - m_correctionPanelIsDismissedByEditor = true; - if (client()) - client()->dismissCorrectionPanel(reasonForDismissing); -#else - UNUSED_PARAM(reasonForDismissing); -#endif -} -String Editor::dismissCorrectionPanelSoon(ReasonForDismissingCorrectionPanel reasonForDismissing) -{ -#if SUPPORT_AUTOCORRECTION_PANEL - if (!m_correctionPanelInfo.isActive) - return String(); - m_correctionPanelInfo.isActive = false; - m_correctionPanelIsDismissedByEditor = true; - if (!client()) - return String(); - return client()->dismissCorrectionPanelSoon(reasonForDismissing); -#else - UNUSED_PARAM(reasonForDismissing); - return String(); -#endif -} - -void Editor::removeSpellAndCorrectionMarkersFromWordsToBeEdited(bool doNotRemoveIfSelectionAtWordBoundary) -{ // We want to remove the markers from a word if an editing command will change the word. This can happen in one of // several scenarios: // 1. Insert in the middle of a word. @@ -2701,102 +2436,6 @@ void Editor::removeSpellAndCorrectionMarkersFromWordsToBeEdited(bool doNotRemove document->markers()->clearDescriptionOnMarkersIntersectingRange(wordRange.get(), DocumentMarker::Replacement); } -void Editor::applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd) -{ -#if SUPPORT_AUTOCORRECTION_PANEL - if (!m_correctionPanelInfo.rangeToBeReplaced) - return; - - ExceptionCode ec = 0; - RefPtr<Range> paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); - if (ec) - return; - - setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition())); - setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition())); - - // After we replace the word at range rangeToBeReplaced, we need to add markers to that range. - // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore. - // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced - // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph - // to store this value. In order to obtain this offset, we need to first create a range - // which spans from the start of paragraph to the start position of rangeToBeReplaced. - RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); - if (ec) - return; - - Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition(); - correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec); - if (ec) - return; - - // Take note of the location of autocorrection so that we can add marker after the replacement took place. - int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); - - // Clone the range, since the caller of this method may want to keep the original range around. - RefPtr<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); - applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString)); - setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start()); - RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, m_correctionPanelInfo.replacementString.length()); - String newText = plainText(replacementRange.get()); - - // Check to see if replacement succeeded. - if (newText != m_correctionPanelInfo.replacementString) - return; - - DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers(); - size_t size = markerTypesToAdd.size(); - for (size_t i = 0; i < size; ++i) { - if (m_correctionPanelInfo.panelType == CorrectionPanelInfo::PanelTypeReversion) - markers->addMarker(replacementRange.get(), markerTypesToAdd[i]); - else - markers->addMarker(replacementRange.get(), markerTypesToAdd[i], m_correctionPanelInfo.replacedString); - } -#else // SUPPORT_AUTOCORRECTION_PANEL - UNUSED_PARAM(markerTypesToAdd); -#endif // SUPPORT_AUTOCORRECTION_PANEL -} - -bool Editor::applyAutocorrectionBeforeTypingIfAppropriate() -{ - if (!m_correctionPanelInfo.rangeToBeReplaced || !m_correctionPanelInfo.isActive) - return false; - - if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeCorrection) - return false; - - Position caretPosition = m_frame->selection()->selection().start(); - - if (m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition) { - handleCorrectionPanelResult(dismissCorrectionPanelSoon(ReasonForDismissingCorrectionPanelAccepted)); - return true; - } - - // Pending correction should always be where caret is. But in case this is not always true, we still want to dismiss the panel without accepting the correction. - ASSERT(m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition); - dismissCorrectionPanel(ReasonForDismissingCorrectionPanelIgnored); - return false; -} - -void Editor::unappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) -{ -#if SUPPORT_AUTOCORRECTION_PANEL - client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); - m_frame->document()->updateLayout(); - m_frame->selection()->setSelection(selectionOfCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle | SelectionController::SpellCorrectionTriggered); - RefPtr<Range> range = Range::create(m_frame->document(), m_frame->selection()->selection().start(), m_frame->selection()->selection().end()); - - DocumentMarkerController* markers = m_frame->document()->markers(); - markers->removeMarkers(range.get(), DocumentMarker::Spelling); - markers->addMarker(range.get(), DocumentMarker::Replacement); - markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption); -#else // SUPPORT_AUTOCORRECTION_PANEL - UNUSED_PARAM(selectionOfCorrected); - UNUSED_PARAM(corrected); - UNUSED_PARAM(correction); -#endif // SUPPORT_AUTOCORRECTION_PANEL -} - PassRefPtr<Range> Editor::rangeForPoint(const IntPoint& windowPoint) { Document* document = m_frame->documentAtPoint(windowPoint); @@ -2919,6 +2558,22 @@ void Editor::addToKillRing(Range* range, bool prepend) m_shouldStartNewKillRingSequence = false; } +void Editor::startCorrectionPanelTimer() +{ + m_spellingCorrector->startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeCorrection); +} + +void Editor::handleCorrectionPanelResult(const String& correction) +{ + m_spellingCorrector->handleCorrectionPanelResult(correction); +} + + +void Editor::dismissCorrectionPanelAsIgnored() +{ + m_spellingCorrector->dismiss(ReasonForDismissingCorrectionPanelIgnored); +} + bool Editor::insideVisibleArea(const IntPoint& point) const { if (m_frame->excludeFromTextSearch()) @@ -3491,14 +3146,7 @@ void Editor::setMarkedTextMatchesAreHighlighted(bool flag) void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, SelectionController::SetSelectionOptions options) { -#if SUPPORT_AUTOCORRECTION_PANEL - // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. - VisibleSelection currentSelection(frame()->selection()->selection()); - if (currentSelection != oldSelection) { - stopCorrectionPanelTimer(); - dismissCorrectionPanel(ReasonForDismissingCorrectionPanelIgnored); - } -#endif // SUPPORT_AUTOCORRECTION_PANEL + m_spellingCorrector->stopPendingCorrection(oldSelection); bool closeTyping = options & SelectionController::CloseTyping; bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); @@ -3514,11 +3162,8 @@ void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, Sel newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); } - bool shouldCheckSpellingAndGrammar = true; -#if SUPPORT_AUTOCORRECTION_PANEL // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. - shouldCheckSpellingAndGrammar = !(options & SelectionController::SpellCorrectionTriggered); -#endif + bool shouldCheckSpellingAndGrammar = !(options & SelectionController::SpellCorrectionTriggered); // When typing we check spelling elsewhere, so don't redo it here. // If this is a change in selection resulting from a delete operation, @@ -3593,7 +3238,49 @@ bool Editor::selectionStartHasMarkerFor(DocumentMarker::MarkerType markerType, i FloatRect Editor::windowRectForRange(const Range* range) const { FrameView* view = frame()->view(); - return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect(); + if (!view) + return FloatRect(); + Vector<FloatQuad> textQuads; + range->textQuads(textQuads); + FloatRect boundingRect; + size_t size = textQuads.size(); + for (size_t i = 0; i < size; ++i) + boundingRect.unite(textQuads[i].boundingBox()); + return view->contentsToWindow(IntRect(boundingRect)); } +TextCheckingTypeMask Editor::textCheckingTypeMaskFor(TextCheckingOptions textCheckingOptions) +{ + bool shouldMarkSpelling = textCheckingOptions & MarkSpelling; + bool shouldMarkGrammar = textCheckingOptions & MarkGrammar; + bool shouldShowCorrectionPanel = textCheckingOptions & ShowCorrectionPanel; + bool shouldCheckForCorrection = shouldShowCorrectionPanel || (textCheckingOptions & CheckForCorrection); + + TextCheckingTypeMask checkingTypes = 0; + if (shouldMarkSpelling) + checkingTypes |= TextCheckingTypeSpelling; + if (shouldMarkGrammar) + checkingTypes |= TextCheckingTypeGrammar; + if (shouldCheckForCorrection) + checkingTypes |= TextCheckingTypeCorrection; + +#if USE(AUTOMATIC_TEXT_REPLACEMENT) + bool shouldPerformReplacement = textCheckingOptions & PerformReplacement; + if (shouldPerformReplacement) { + if (isAutomaticLinkDetectionEnabled()) + checkingTypes |= TextCheckingTypeLink; + if (isAutomaticQuoteSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeQuote; + if (isAutomaticDashSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeDash; + if (isAutomaticTextReplacementEnabled()) + checkingTypes |= TextCheckingTypeReplacement; + if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) + checkingTypes |= TextCheckingTypeCorrection; + } +#endif + + return checkingTypes; +} + } // namespace WebCore diff --git a/Source/WebCore/editing/Editor.h b/Source/WebCore/editing/Editor.h index c723ddf..24d5c87 100644 --- a/Source/WebCore/editing/Editor.h +++ b/Source/WebCore/editing/Editor.h @@ -28,7 +28,6 @@ #include "ClipboardAccessPolicy.h" #include "Color.h" -#include "CorrectionPanelInfo.h" #include "DocumentMarker.h" #include "EditAction.h" #include "EditingBehavior.h" @@ -51,6 +50,7 @@ namespace WebCore { class CSSMutableStyleDeclaration; class CSSStyleDeclaration; class Clipboard; +class SpellingCorrectionController; class DeleteButtonController; class EditCommand; class EditorClient; @@ -198,6 +198,7 @@ public: Command command(const String& commandName); // Command source is CommandFromMenuOrKeyBinding. Command command(const String& commandName, EditorCommandSource); static bool commandIsSupportedFromMenuOrKeyBinding(const String& commandName); // Works without a frame. + static bool hasTransparentBackgroundColor(CSSStyleDeclaration*); bool insertText(const String&, Event* triggeringEvent); bool insertTextForConfirmedComposition(const String& text); @@ -219,10 +220,20 @@ public: Vector<String> guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical); bool isSpellCheckingEnabledInFocusedNode() const; bool isSpellCheckingEnabledFor(Node*) const; - void markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping); + void markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping, bool doReplacement); void markMisspellings(const VisibleSelection&, RefPtr<Range>& firstMisspellingRange); void markBadGrammar(const VisibleSelection&); void markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection); + + enum TextCheckingOptionFlags { + MarkSpelling = 1 << 0, + MarkGrammar = 1 << 1, + PerformReplacement = 1 << 2, + ShowCorrectionPanel = 1 << 3, + CheckForCorrection = 1 << 4, + }; + typedef unsigned TextCheckingOptions; + #if USE(AUTOMATIC_TEXT_REPLACEMENT) void uppercaseWord(); void lowercaseWord(); @@ -242,14 +253,6 @@ public: void toggleAutomaticSpellingCorrection(); #endif - enum TextCheckingOptionFlags { - MarkSpelling = 1 << 0, - MarkGrammar = 1 << 1, - PerformReplacement = 1 << 2, - ShowCorrectionPanel = 1 << 3, - }; - typedef unsigned TextCheckingOptions; - void markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions, Range* spellingRange, Range* grammarRange); void changeBackToReplacedString(const String& replacedString); @@ -323,9 +326,10 @@ public: void addToKillRing(Range*, bool prepend); - void startCorrectionPanelTimer(CorrectionPanelInfo::PanelType); + void startCorrectionPanelTimer(); // If user confirmed a correction in the correction panel, correction has non-zero length, otherwise it means that user has dismissed the panel. void handleCorrectionPanelResult(const String& correction); + void dismissCorrectionPanelAsIgnored(); void pasteAsFragment(PassRefPtr<DocumentFragment>, bool smartReplace, bool matchStyle); void pasteAsPlainText(const String&, bool smartReplace); @@ -381,7 +385,7 @@ public: #endif bool selectionStartHasMarkerFor(DocumentMarker::MarkerType, int from, int length) const; - void removeSpellAndCorrectionMarkersFromWordsToBeEdited(bool doNotRemoveIfSelectionAtWordBoundary); + void updateMarkersForWordsAffectedByEditing(bool onlyHandleWordsContainingSelection); private: Frame* m_frame; @@ -396,10 +400,8 @@ private: bool m_shouldStartNewKillRingSequence; bool m_shouldStyleWithCSS; OwnPtr<KillRing> m_killRing; - CorrectionPanelInfo m_correctionPanelInfo; OwnPtr<SpellChecker> m_spellChecker; - Timer<Editor> m_correctionPanelTimer; - bool m_correctionPanelIsDismissedByEditor; + OwnPtr<SpellingCorrectionController> m_spellingCorrector; VisibleSelection m_mark; bool m_areMarkedTextMatchesHighlighted; @@ -413,6 +415,7 @@ private: void writeSelectionToPasteboard(Pasteboard*); void revealSelectionAfterEditingOperation(); void markMisspellingsOrBadGrammar(const VisibleSelection&, bool checkSpelling, RefPtr<Range>& firstMisspellingRange); + TextCheckingTypeMask textCheckingTypeMaskFor(TextCheckingOptions); void selectComposition(); void confirmComposition(const String&, bool preserveSelection); @@ -423,11 +426,10 @@ private: PassRefPtr<Range> nextVisibleRange(Range*, const String&, FindOptions); void changeSelectionAfterCommand(const VisibleSelection& newSelection, bool closeTyping, bool clearTypingStyle); - void correctionPanelTimerFired(Timer<Editor>*); + Node* findEventTargetFromSelection() const; void stopCorrectionPanelTimer(); - void dismissCorrectionPanel(ReasonForDismissingCorrectionPanel); - String dismissCorrectionPanelSoon(ReasonForDismissingCorrectionPanel); + void applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd); // Return true if correction was applied, false otherwise. bool applyAutocorrectionBeforeTypingIfAppropriate(); diff --git a/Source/WebCore/editing/EditorCommand.cpp b/Source/WebCore/editing/EditorCommand.cpp index 8ea37bb..290ee47 100644 --- a/Source/WebCore/editing/EditorCommand.cpp +++ b/Source/WebCore/editing/EditorCommand.cpp @@ -318,7 +318,7 @@ static bool executeDelete(Frame* frame, Event*, EditorCommandSource source, cons case CommandFromDOMWithUserInterface: // If the current selection is a caret, delete the preceding character. IE performs forwardDelete, but we currently side with Firefox. // Doesn't scroll to make the selection visible, or modify the kill ring (this time, siding with IE, not Firefox). - TypingCommand::deleteKeyPressed(frame->document(), frame->selection()->granularity() == WordGranularity); + TypingCommand::deleteKeyPressed(frame->document(), frame->selection()->granularity() == WordGranularity ? TypingCommand::SmartDelete : 0); return true; } ASSERT_NOT_REACHED(); @@ -1196,27 +1196,27 @@ static bool enabledCut(Frame* frame, Event*, EditorCommandSource) return frame->editor()->canDHTMLCut() || frame->editor()->canCut(); } +static bool enabledInEditableText(Frame* frame, Event* event, EditorCommandSource) +{ + return frame->editor()->selectionForCommand(event).rootEditableElement(); +} + static bool enabledDelete(Frame* frame, Event* event, EditorCommandSource source) { switch (source) { case CommandFromMenuOrKeyBinding: // "Delete" from menu only affects selected range, just like Cut but without affecting pasteboard - return frame->editor()->canDHTMLCut() || frame->editor()->canCut(); + return enabledCut(frame, event, source); case CommandFromDOM: case CommandFromDOMWithUserInterface: // "Delete" from DOM is like delete/backspace keypress, affects selected range if non-empty, // otherwise removes a character - return frame->editor()->selectionForCommand(event).isContentEditable(); + return enabledInEditableText(frame, event, source); } ASSERT_NOT_REACHED(); return false; } -static bool enabledInEditableText(Frame* frame, Event* event, EditorCommandSource) -{ - return frame->editor()->selectionForCommand(event).isContentEditable(); -} - static bool enabledInEditableTextOrCaretBrowsing(Frame* frame, Event* event, EditorCommandSource) { // The EditorCommandSource parameter is unused in enabledInEditableText, so just pass a dummy variable @@ -1225,7 +1225,7 @@ static bool enabledInEditableTextOrCaretBrowsing(Frame* frame, Event* event, Edi static bool enabledInRichlyEditableText(Frame* frame, Event*, EditorCommandSource) { - return frame->selection()->isCaretOrRange() && frame->selection()->isContentRichlyEditable(); + return frame->selection()->isCaretOrRange() && frame->selection()->isContentRichlyEditable() && frame->selection()->rootEditableElement(); } static bool enabledPaste(Frame* frame, Event*, EditorCommandSource) diff --git a/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp b/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp index 771c56a..2b7ee8c 100644 --- a/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp +++ b/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp @@ -239,7 +239,7 @@ void InsertParagraphSeparatorCommand::doApply() // Recreate the same structure in the new paragraph. Vector<Element*> ancestors; - getAncestorsInsideBlock(insertionPosition.deprecatedNode(), startBlock, ancestors); + getAncestorsInsideBlock(positionOutsideTabSpan(insertionPosition).deprecatedNode(), startBlock, ancestors); RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); appendBlockPlaceholder(parent); @@ -254,6 +254,9 @@ void InsertParagraphSeparatorCommand::doApply() // similar case where previous position is in another, presumeably nested, block. if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { Node *refNode; + + insertionPosition = positionOutsideTabSpan(insertionPosition); + if (isFirstInBlock && !nestNewBlock) refNode = startBlock; else if (insertionPosition.deprecatedNode() == startBlock && nestNewBlock) { @@ -270,7 +273,7 @@ void InsertParagraphSeparatorCommand::doApply() // Recreate the same structure in the new paragraph. Vector<Element*> ancestors; - getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(insertionPosition).deprecatedNode(), startBlock, ancestors); + getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(positionOutsideTabSpan(insertionPosition)).deprecatedNode(), startBlock, ancestors); appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); @@ -299,11 +302,7 @@ void InsertParagraphSeparatorCommand::doApply() // At this point, the insertionPosition's node could be a container, and we want to make sure we include // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position // before we walk the DOM tree. - insertionPosition = VisiblePosition(insertionPosition).deepEquivalent(); - - // Build up list of ancestors in between the start node and the start block. - Vector<Element*> ancestors; - getAncestorsInsideBlock(insertionPosition.deprecatedNode(), startBlock, ancestors); + insertionPosition = positionOutsideTabSpan(VisiblePosition(insertionPosition).deepEquivalent()); // Make sure we do not cause a rendered space to become unrendered. // FIXME: We need the affinity for pos, but pos.downstream() does not give it @@ -335,48 +334,35 @@ void InsertParagraphSeparatorCommand::doApply() insertNodeAfter(blockToInsert.get(), startBlock); updateLayout(); - - // Make clones of ancestors in between the start node and the outer block. - RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); // If the paragraph separator was inserted at the end of a paragraph, an empty line must be // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) appendNode(createBreakElement(document()).get(), blockToInsert.get()); - + // Move the start node and the siblings of the start node. - if (insertionPosition.deprecatedNode() != startBlock) { - Node* n = insertionPosition.deprecatedNode(); - if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n)) - n = n->nextSibling(); + if (VisiblePosition(insertionPosition) != VisiblePosition(positionBeforeNode(blockToInsert.get()))) { + Node* n; + if (insertionPosition.containerNode() == startBlock) + n = insertionPosition.computeNodeAfterPosition(); + else { + splitTreeToNode(insertionPosition.containerNode(), startBlock); + + for (n = startBlock->firstChild(); n; n = n->nextSibling()) { + if (comparePositions(VisiblePosition(insertionPosition), positionBeforeNode(n)) <= 0) + break; + } + } while (n && n != blockToInsert) { Node *next = n->nextSibling(); removeNode(n); - appendNode(n, parent.get()); + appendNode(n, blockToInsert); n = next; } } - // Move everything after the start node. - if (!ancestors.isEmpty()) { - Element* leftParent = ancestors.first(); - while (leftParent && leftParent != startBlock) { - parent = parent->parentElement(); - if (!parent) - break; - Node* n = leftParent->nextSibling(); - while (n && n != blockToInsert) { - Node* next = n->nextSibling(); - removeNode(n); - appendNode(n, parent.get()); - n = next; - } - leftParent = leftParent->parentElement(); - } - } - // Handle whitespace that occurs after the split if (splitText) { updateLayout(); diff --git a/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp index 7ab3aba..662415b 100644 --- a/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp +++ b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp @@ -56,8 +56,8 @@ static void swapInNodePreservingAttributesAndChildren(HTMLElement* newNode, HTML parentNode->insertBefore(newNode, nodeToReplace, ec); ASSERT(!ec); - Node* nextChild; - for (Node* child = nodeToReplace->firstChild(); child; child = nextChild) { + RefPtr<Node> nextChild; + for (Node* child = nodeToReplace->firstChild(); child; child = nextChild.get()) { nextChild = child->nextSibling(); newNode->appendChild(child, ec); ASSERT(!ec); diff --git a/Source/WebCore/editing/ReplaceSelectionCommand.cpp b/Source/WebCore/editing/ReplaceSelectionCommand.cpp index 94531a6..c3b1501 100644 --- a/Source/WebCore/editing/ReplaceSelectionCommand.cpp +++ b/Source/WebCore/editing/ReplaceSelectionCommand.cpp @@ -952,20 +952,17 @@ void ReplaceSelectionCommand::doApply() // We can skip this optimization for fragments not wrapped in one of // our style spans and for positions inside list items // since insertAsListItems already does the right thing. - if (!m_matchStyle && !enclosingList(insertionPos.anchorNode()) && isStyleSpan(fragment.firstChild())) { - Node* parentNode = insertionPos.anchorNode()->parentNode(); - while (parentNode && parentNode->renderer() && isInlineNodeWithStyle(parentNode)) { - // If we are in the middle of a text node, we need to split it before we can - // move the insertion position. - if (insertionPos.anchorNode()->isTextNode() && insertionPos.anchorType() == Position::PositionIsOffsetInAnchor && insertionPos.offsetInContainerNode() && !insertionPos.atLastEditingPositionForNode()) - splitTextNodeContainingElement(static_cast<Text*>(insertionPos.anchorNode()), insertionPos.offsetInContainerNode()); - - // If the style element has more than one child, we need to split it. - if (parentNode->firstChild()->nextSibling()) - splitElement(static_cast<Element*>(parentNode), insertionPos.computeNodeAfterPosition()); - - insertionPos = positionInParentBeforeNode(parentNode); - parentNode = parentNode->parentNode(); + if (!m_matchStyle && !enclosingList(insertionPos.containerNode()) && isStyleSpan(fragment.firstChild())) { + if (insertionPos.containerNode()->isTextNode() && insertionPos.offsetInContainerNode() && !insertionPos.atLastEditingPositionForNode()) { + splitTextNodeContainingElement(static_cast<Text*>(insertionPos.containerNode()), insertionPos.offsetInContainerNode()); + insertionPos = firstPositionInNode(insertionPos.containerNode()); + } + + // FIXME: isInlineNodeWithStyle does not check editability. + if (RefPtr<Node> nodeToSplitTo = highestEnclosingNodeOfType(insertionPos, isInlineNodeWithStyle)) { + if (insertionPos.containerNode() != nodeToSplitTo) + nodeToSplitTo = splitTreeToNode(insertionPos.anchorNode(), nodeToSplitTo.get(), true).get(); + insertionPos = positionInParentBeforeNode(nodeToSplitTo.get()); } } diff --git a/Source/WebCore/editing/SelectionController.cpp b/Source/WebCore/editing/SelectionController.cpp index 698ba2c..c5a33d3 100644 --- a/Source/WebCore/editing/SelectionController.cpp +++ b/Source/WebCore/editing/SelectionController.cpp @@ -254,9 +254,9 @@ void SelectionController::respondToNodeModification(Node* node, bool baseRemoved m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); else m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); - } else if (m_selection.firstRange()) { + } else if (RefPtr<Range> range = m_selection.firstRange()) { ExceptionCode ec = 0; - Range::CompareResults compareResult = m_selection.firstRange()->compareNode(node, ec); + Range::CompareResults compareResult = range->compareNode(node, ec); if (!ec && (compareResult == Range::NODE_BEFORE_AND_AFTER || compareResult == Range::NODE_INSIDE)) { // If we did nothing here, when this node's renderer was destroyed, the rect that it // occupied would be invalidated, but, selection gaps that change as a result of @@ -450,6 +450,9 @@ VisiblePosition SelectionController::modifyExtendingRight(TextGranularity granul case DocumentBoundary: // FIXME: implement all of the above? pos = modifyExtendingForward(granularity); + break; + case WebKitVisualWordGranularity: + break; } return pos; } @@ -489,6 +492,8 @@ VisiblePosition SelectionController::modifyExtendingForward(TextGranularity gran else pos = endOfDocument(pos); break; + case WebKitVisualWordGranularity: + break; } return pos; @@ -520,6 +525,9 @@ VisiblePosition SelectionController::modifyMovingRight(TextGranularity granulari case LineBoundary: pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); break; + case WebKitVisualWordGranularity: + pos = rightWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; } return pos; } @@ -568,6 +576,8 @@ VisiblePosition SelectionController::modifyMovingForward(TextGranularity granula else pos = endOfDocument(pos); break; + case WebKitVisualWordGranularity: + break; } return pos; } @@ -607,6 +617,9 @@ VisiblePosition SelectionController::modifyExtendingLeft(TextGranularity granula case ParagraphBoundary: case DocumentBoundary: pos = modifyExtendingBackward(granularity); + break; + case WebKitVisualWordGranularity: + break; } return pos; } @@ -651,6 +664,8 @@ VisiblePosition SelectionController::modifyExtendingBackward(TextGranularity gra else pos = startOfDocument(pos); break; + case WebKitVisualWordGranularity: + break; } return pos; } @@ -681,6 +696,9 @@ VisiblePosition SelectionController::modifyMovingLeft(TextGranularity granularit case LineBoundary: pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); break; + case WebKitVisualWordGranularity: + pos = leftWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; } return pos; } @@ -723,6 +741,8 @@ VisiblePosition SelectionController::modifyMovingBackward(TextGranularity granul else pos = startOfDocument(pos); break; + case WebKitVisualWordGranularity: + break; } return pos; } diff --git a/Source/WebCore/editing/SpellChecker.cpp b/Source/WebCore/editing/SpellChecker.cpp index 7988e41..fedcc07 100644 --- a/Source/WebCore/editing/SpellChecker.cpp +++ b/Source/WebCore/editing/SpellChecker.cpp @@ -100,13 +100,13 @@ bool SpellChecker::isCheckable(Node* node) const return node && node->renderer(); } -void SpellChecker::requestCheckingFor(Node* node) +void SpellChecker::requestCheckingFor(TextCheckingTypeMask mask, Node* node) { ASSERT(canCheckAsynchronously(node)); if (!initRequest(node)) return; - m_client->requestCheckingOfString(this, m_requestSequence, m_requestText); + m_client->requestCheckingOfString(this, m_requestSequence, mask, m_requestText); } static bool forwardIterator(PositionIterator& iterator, int distance) @@ -131,7 +131,16 @@ static bool forwardIterator(PositionIterator& iterator, int distance) return false; } -void SpellChecker::didCheck(int sequence, const Vector<SpellCheckingResult>& results) +static DocumentMarker::MarkerType toMarkerType(TextCheckingType type) +{ + if (type == TextCheckingTypeSpelling) + return DocumentMarker::Spelling; + ASSERT(type == TextCheckingTypeGrammar); + return DocumentMarker::Grammar; +} + +// Currenntly ignoring TextCheckingResult::details but should be handled. See Bug 56368. +void SpellChecker::didCheck(int sequence, const Vector<TextCheckingResult>& results) { if (!isValid(sequence)) return; @@ -144,16 +153,16 @@ void SpellChecker::didCheck(int sequence, const Vector<SpellCheckingResult>& res int startOffset = 0; PositionIterator start = firstPositionInOrBeforeNode(m_requestNode.get()); for (size_t i = 0; i < results.size(); ++i) { - if (results[i].type() != DocumentMarker::Spelling && results[i].type() != DocumentMarker::Grammar) + if (results[i].type != TextCheckingTypeSpelling && results[i].type != TextCheckingTypeGrammar) continue; // To avoid moving the position backward, we assume the given results are sorted with // startOffset as the ones returned by [NSSpellChecker requestCheckingOfString:]. - ASSERT(startOffset <= results[i].location()); - if (!forwardIterator(start, results[i].location() - startOffset)) + ASSERT(startOffset <= results[i].location); + if (!forwardIterator(start, results[i].location - startOffset)) break; PositionIterator end = start; - if (!forwardIterator(end, results[i].length())) + if (!forwardIterator(end, results[i].length)) break; // Users or JavaScript applications may change text while a spell-checker checks its @@ -163,11 +172,11 @@ void SpellChecker::didCheck(int sequence, const Vector<SpellCheckingResult>& res RefPtr<Range> range = Range::create(m_requestNode->document(), start, end); // FIXME: Use textContent() compatible string conversion. String destination = range->text(); - String source = m_requestText.substring(results[i].location(), results[i].length()); + String source = m_requestText.substring(results[i].location, results[i].length); if (destination == source) - m_requestNode->document()->markers()->addMarker(range.get(), results[i].type()); + m_requestNode->document()->markers()->addMarker(range.get(), toMarkerType(results[i].type)); - startOffset = results[i].location(); + startOffset = results[i].location; } clearRequest(); diff --git a/Source/WebCore/editing/SpellChecker.h b/Source/WebCore/editing/SpellChecker.h index d3940e5..4bcb89e 100644 --- a/Source/WebCore/editing/SpellChecker.h +++ b/Source/WebCore/editing/SpellChecker.h @@ -27,6 +27,7 @@ #define SpellChecker_h #include "DocumentMarker.h" +#include "TextCheckerClient.h" #include <wtf/Noncopyable.h> namespace WebCore { @@ -35,25 +36,6 @@ class TextCheckerClient; class Frame; class Node; -class SpellCheckingResult { -public: - explicit SpellCheckingResult(DocumentMarker::MarkerType type = DocumentMarker::Spelling, int location = 0, int length = 0) - : m_type(type) - , m_location(location) - , m_length(length) - { - } - - DocumentMarker::MarkerType type() const { return m_type; } - int location() const { return m_location; } - int length() const { return m_length; } - -private: - DocumentMarker::MarkerType m_type; - int m_location; - int m_length; -}; - class SpellChecker { WTF_MAKE_NONCOPYABLE(SpellChecker); public: @@ -65,8 +47,8 @@ public: bool isBusy() const; bool isValid(int sequence) const; bool isCheckable(Node*) const; - void requestCheckingFor(Node*); - void didCheck(int sequence, const Vector<SpellCheckingResult>&); + void requestCheckingFor(TextCheckingTypeMask, Node*); + void didCheck(int sequence, const Vector<TextCheckingResult>&); private: bool initRequest(Node*); diff --git a/Source/WebCore/editing/SpellingCorrectionCommand.cpp b/Source/WebCore/editing/SpellingCorrectionCommand.cpp index bad4a99..f0297c1 100644 --- a/Source/WebCore/editing/SpellingCorrectionCommand.cpp +++ b/Source/WebCore/editing/SpellingCorrectionCommand.cpp @@ -26,7 +26,7 @@ #include "config.h" #include "SpellingCorrectionCommand.h" -#include "CorrectionPanelInfo.h" +#include "SpellingCorrectionController.h" #include "DocumentFragment.h" #include "Frame.h" #include "ReplaceSelectionCommand.h" @@ -94,7 +94,7 @@ void SpellingCorrectionCommand::doApply() if (!fragment) return; - applyCommandToComposite(SetSelectionCommand::create(m_selectionToBeCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle)); + applyCommandToComposite(SetSelectionCommand::create(m_selectionToBeCorrected, SelectionController::SpellCorrectionTriggered | SelectionController::CloseTyping | SelectionController::ClearTypingStyle)); #if SUPPORT_AUTOCORRECTION_PANEL applyCommandToComposite(SpellingCorrectionRecordUndoCommand::create(document(), m_corrected, m_correction)); #endif diff --git a/Source/WebCore/editing/SpellingCorrectionController.cpp b/Source/WebCore/editing/SpellingCorrectionController.cpp new file mode 100644 index 0000000..c377fe8 --- /dev/null +++ b/Source/WebCore/editing/SpellingCorrectionController.cpp @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2006, 2007, 2008 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. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``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 COMPUTER, INC. OR + * 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. + */ + +#include "config.h" +#include "SpellingCorrectionController.h" + +#include "DocumentMarkerController.h" +#include "EditCommand.h" +#include "EditorClient.h" +#include "Frame.h" +#include "FrameView.h" +#include "SpellingCorrectionCommand.h" +#include "TextCheckerClient.h" +#include "TextCheckingHelper.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "markup.h" +#include "visible_units.h" + + +namespace WebCore { + +using namespace std; +using namespace WTF; + +#if SUPPORT_AUTOCORRECTION_PANEL + +static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection() +{ + DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForAutoCorrection, ()); + if (markerTypesForAutoCorrection.isEmpty()) { + markerTypesForAutoCorrection.append(DocumentMarker::Replacement); + markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator); + markerTypesForAutoCorrection.append(DocumentMarker::SpellCheckingExemption); + markerTypesForAutoCorrection.append(DocumentMarker::Autocorrected); + } + return markerTypesForAutoCorrection; +} + +static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement() +{ + DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ()); + if (markerTypesForReplacement.isEmpty()) { + markerTypesForReplacement.append(DocumentMarker::Replacement); + markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption); + } + return markerTypesForReplacement; +} + +static bool markersHaveIdenticalDescription(const Vector<DocumentMarker>& markers) +{ + if (markers.isEmpty()) + return true; + + const String& description = markers[0].description; + for (size_t i = 1; i < markers.size(); ++i) { + if (description != markers[i].description) + return false; + } + return true; +} + +SpellingCorrectionController::SpellingCorrectionController(Frame* frame) + : m_frame(frame) + , m_correctionPanelTimer(this, &SpellingCorrectionController::correctionPanelTimerFired) +{ +} + +SpellingCorrectionController::~SpellingCorrectionController() +{ + dismiss(ReasonForDismissingCorrectionPanelIgnored); +} + +void SpellingCorrectionController::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type) +{ + const double correctionPanelTimerInterval = 0.3; + if (!isAutomaticSpellingCorrectionEnabled()) + return; + + // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. + if (type == CorrectionPanelInfo::PanelTypeCorrection) + m_correctionPanelInfo.rangeToBeReplaced.clear(); + m_correctionPanelInfo.panelType = type; + m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval); +} + +void SpellingCorrectionController::stopCorrectionPanelTimer() +{ + m_correctionPanelTimer.stop(); + m_correctionPanelInfo.rangeToBeReplaced.clear(); +} + +void SpellingCorrectionController::stopPendingCorrection(const VisibleSelection& oldSelection) +{ + // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below. + VisibleSelection currentSelection(m_frame->selection()->selection()); + if (currentSelection == oldSelection) + return; + + stopCorrectionPanelTimer(); + dismiss(ReasonForDismissingCorrectionPanelIgnored); +} + +void SpellingCorrectionController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping) +{ + // Apply pending autocorrection before next round of spell checking. + bool doApplyCorrection = true; + VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); + VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); + if (currentWord.visibleEnd() == startOfSelection) { + String wordText = plainText(currentWord.toNormalizedRange().get()); + if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) + doApplyCorrection = false; + } + if (doApplyCorrection) + handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); + else + m_correctionPanelInfo.rangeToBeReplaced.clear(); +} + +bool SpellingCorrectionController::hasPendingCorrection() const +{ + return m_correctionPanelInfo.rangeToBeReplaced; +} + +bool SpellingCorrectionController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const +{ + return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption); +} + +void SpellingCorrectionController::show(PassRefPtr<Range> rangeToReplace, const String& replacement) +{ + FloatRect boundingBox = windowRectForRange(rangeToReplace.get()); + if (boundingBox.isEmpty()) + return; + m_correctionPanelInfo.replacedString = plainText(rangeToReplace.get()); + m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace; + m_correctionPanelInfo.replacementString = replacement; + m_correctionPanelInfo.isActive = true; + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, replacement, Vector<String>()); +} + +void SpellingCorrectionController::handleCancelOperation() +{ + if (!m_correctionPanelInfo.isActive) + return; + m_correctionPanelInfo.isActive = false; + dismiss(ReasonForDismissingCorrectionPanelCancelled); +} + +void SpellingCorrectionController::dismiss(ReasonForDismissingCorrectionPanel reasonForDismissing) +{ + if (!m_correctionPanelInfo.isActive) + return; + m_correctionPanelInfo.isActive = false; + m_correctionPanelIsDismissedByEditor = true; + if (client()) + client()->dismissCorrectionPanel(reasonForDismissing); +} + +String SpellingCorrectionController::dismissSoon(ReasonForDismissingCorrectionPanel reasonForDismissing) +{ + if (!m_correctionPanelInfo.isActive) + return String(); + m_correctionPanelInfo.isActive = false; + m_correctionPanelIsDismissedByEditor = true; + if (!client()) + return String(); + return client()->dismissCorrectionPanelSoon(reasonForDismissing); +} + +void SpellingCorrectionController::applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd) +{ + if (!m_correctionPanelInfo.rangeToBeReplaced) + return; + + ExceptionCode ec = 0; + RefPtr<Range> paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); + if (ec) + return; + + setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition())); + setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition())); + + // After we replace the word at range rangeToBeReplaced, we need to add markers to that range. + // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore. + // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced + // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph + // to store this value. In order to obtain this offset, we need to first create a range + // which spans from the start of paragraph to the start position of rangeToBeReplaced. + RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); + if (ec) + return; + + Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition(); + correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec); + if (ec) + return; + + // Take note of the location of autocorrection so that we can add marker after the replacement took place. + int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); + + // Clone the range, since the caller of this method may want to keep the original range around. + RefPtr<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); + applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString)); + setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start()); + RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, m_correctionPanelInfo.replacementString.length()); + String newText = plainText(replacementRange.get()); + + // Check to see if replacement succeeded. + if (newText != m_correctionPanelInfo.replacementString) + return; + + DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers(); + size_t size = markerTypesToAdd.size(); + for (size_t i = 0; i < size; ++i) { + DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; + String description; + if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeReversion && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)) + description = m_correctionPanelInfo.replacedString; + markers->addMarker(replacementRange.get(), markerType, description); + } +} + +bool SpellingCorrectionController::applyAutocorrectionBeforeTypingIfAppropriate() +{ + if (!m_correctionPanelInfo.rangeToBeReplaced || !m_correctionPanelInfo.isActive) + return false; + + if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeCorrection) + return false; + + Position caretPosition = m_frame->selection()->selection().start(); + + if (m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition) { + handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted)); + return true; + } + + // Pending correction should always be where caret is. But in case this is not always true, we still want to dismiss the panel without accepting the correction. + ASSERT(m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition); + dismiss(ReasonForDismissingCorrectionPanelIgnored); + return false; +} + +void SpellingCorrectionController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) +{ + client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); + m_frame->document()->updateLayout(); + m_frame->selection()->setSelection(selectionOfCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle | SelectionController::SpellCorrectionTriggered); + RefPtr<Range> range = Range::create(m_frame->document(), m_frame->selection()->selection().start(), m_frame->selection()->selection().end()); + + DocumentMarkerController* markers = m_frame->document()->markers(); + markers->removeMarkers(range.get(), DocumentMarker::Spelling | DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); + markers->addMarker(range.get(), DocumentMarker::Replacement); + markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption); +} + +void SpellingCorrectionController::correctionPanelTimerFired(Timer<SpellingCorrectionController>*) +{ + m_correctionPanelIsDismissedByEditor = false; + switch (m_correctionPanelInfo.panelType) { + case CorrectionPanelInfo::PanelTypeCorrection: { + VisibleSelection selection(m_frame->selection()->selection()); + VisiblePosition start(selection.start(), selection.affinity()); + VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); + VisibleSelection adjacentWords = VisibleSelection(p, start); + m_frame->editor()->markAllMisspellingsAndBadGrammarInRanges(Editor::MarkSpelling | Editor::ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); + } + break; + case CorrectionPanelInfo::PanelTypeReversion: { + m_correctionPanelInfo.isActive = true; + m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); + FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); + if (!boundingBox.isEmpty()) + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector<String>()); + } + break; + case CorrectionPanelInfo::PanelTypeSpellingSuggestions: { + if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString) + break; + String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get()); + Vector<String> suggestions; + textChecker()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions); + if (suggestions.isEmpty()) { + m_correctionPanelInfo.rangeToBeReplaced.clear(); + break; + } + String topSuggestion = suggestions.first(); + suggestions.remove(0); + m_correctionPanelInfo.isActive = true; + FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get()); + if (!boundingBox.isEmpty()) + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, topSuggestion, suggestions); + } + break; + } +} + +void SpellingCorrectionController::handleCorrectionPanelResult(const String& correction) +{ + Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get(); + if (!replacedRange || m_frame->document() != replacedRange->ownerDocument()) + return; + + String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); + // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. + if (currentWord != m_correctionPanelInfo.replacedString) + return; + + m_correctionPanelInfo.isActive = false; + + switch (m_correctionPanelInfo.panelType) { + case CorrectionPanelInfo::PanelTypeCorrection: + if (correction.length()) { + m_correctionPanelInfo.replacementString = correction; + applyCorrectionPanelInfo(markerTypesForAutocorrection()); + } else if (!m_correctionPanelIsDismissedByEditor) + replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString); + break; + case CorrectionPanelInfo::PanelTypeReversion: + case CorrectionPanelInfo::PanelTypeSpellingSuggestions: + if (correction.length()) { + m_correctionPanelInfo.replacementString = correction; + applyCorrectionPanelInfo(markerTypesForReplacement()); + } + break; + } + + m_correctionPanelInfo.rangeToBeReplaced.clear(); +} + +bool SpellingCorrectionController::isAutomaticSpellingCorrectionEnabled() +{ + return client() && client()->isAutomaticSpellingCorrectionEnabled(); +} + +FloatRect SpellingCorrectionController::windowRectForRange(const Range* range) const +{ + FrameView* view = m_frame->view(); + return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect(); +} + +void SpellingCorrectionController::respondToChangedSelection(const VisibleSelection& oldSelection) +{ + VisibleSelection currentSelection(m_frame->selection()->selection()); + // When user moves caret to the end of autocorrected word and pauses, we show the panel + // containing the original pre-correction word so that user can quickly revert the + // undesired autocorrection. Here, we start correction panel timer once we confirm that + // the new caret position is at the end of a word. + if (!currentSelection.isCaret() || currentSelection == oldSelection) + return; + + VisiblePosition selectionPosition = currentSelection.start(); + VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); + if (selectionPosition != endPositionOfWord) + return; + + Position position = endPositionOfWord.deepEquivalent(); + if (position.anchorType() != Position::PositionIsOffsetInAnchor) + return; + + Node* node = position.containerNode(); + int endOffset = position.offsetInContainerNode(); + Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); + size_t markerCount = markers.size(); + for (size_t i = 0; i < markerCount; ++i) { + const DocumentMarker& marker = markers[i]; + if (!shouldStartTimeFor(marker, endOffset)) + continue; + RefPtr<Range> wordRange = Range::create(m_frame->document(), node, marker.startOffset, node, marker.endOffset); + String currentWord = plainText(wordRange.get()); + if (!currentWord.length()) + continue; + + m_correctionPanelInfo.rangeToBeReplaced = wordRange; + m_correctionPanelInfo.replacedString = currentWord; + if (marker.type == DocumentMarker::Spelling) + startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions); + else { + m_correctionPanelInfo.replacementString = marker.description; + startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion); + } + + break; + } +} + +void SpellingCorrectionController::respondToAppliedEditing(PassRefPtr<EditCommand> command) +{ + if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator()) + m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); +} + +EditorClient* SpellingCorrectionController::client() +{ + return m_frame->page() ? m_frame->page()->editorClient() : 0; +} + +TextCheckerClient* SpellingCorrectionController::textChecker() +{ + if (EditorClient* owner = client()) + return owner->textChecker(); + return 0; +} + +void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString) +{ + client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacementString); +} + +void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange) +{ + recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get())); +} + +void SpellingCorrectionController::markReversed(PassRefPtr<Range> changedRange) +{ + changedRange->startContainer()->document()->markers()->removeMarkers(changedRange.get(), DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); + changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption); +} + +void SpellingCorrectionController::markCorrection(PassRefPtr<Range> replacedRange, const String& replacedString) +{ + Vector<DocumentMarker::MarkerType> markerTypesToAdd = markerTypesForAutocorrection(); + DocumentMarkerController* markers = replacedRange->startContainer()->document()->markers(); + for (size_t i = 0; i < markerTypesToAdd.size(); ++i) { + DocumentMarker::MarkerType markerType = markerTypesToAdd[i]; + if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected) + markers->addMarker(replacedRange.get(), markerType, replacedString); + else + markers->addMarker(replacedRange.get(), markerType); + } +} + +void SpellingCorrectionController::recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction) +{ + if (!rangeOfCorrection) + return; + DocumentMarkerController* markers = rangeOfCorrection->startContainer()->document()->markers(); + Vector<DocumentMarker> correctedOnceMarkers = markers->markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected); + if (correctedOnceMarkers.isEmpty()) + return; + + // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or + // edited it to something else, and notify spellchecker accordingly. + if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0].description == corrected) + client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction); + else + client()->recordAutocorrectionResponse(EditorClient::AutocorrectionEdited, corrected, correction); + markers->removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker); +} + +#endif + +} // namespace WebCore diff --git a/Source/WebCore/editing/SpellingCorrectionController.h b/Source/WebCore/editing/SpellingCorrectionController.h new file mode 100644 index 0000000..691510a --- /dev/null +++ b/Source/WebCore/editing/SpellingCorrectionController.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2010 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 COMPUTER, INC. ``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 COMPUTER, INC. OR + * 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. + */ + +#ifndef SpellingCorrectionController_h +#define SpellingCorrectionController_h + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) +// Some platforms provide UI for suggesting autocorrection. +#define SUPPORT_AUTOCORRECTION_PANEL 1 +// Some platforms use spelling and autocorrection markers to provide visual cue. +// On such platform, if word with marker is edited, we need to remove the marker. +#define REMOVE_MARKERS_UPON_EDITING 1 +#else +#define SUPPORT_AUTOCORRECTION_PANEL 0 +#define REMOVE_MARKERS_UPON_EDITING 0 +#endif // #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) + +#include "DocumentMarker.h" +#include "EditCommand.h" +#include "FloatRect.h" +#include "Range.h" +#include "Timer.h" +#include <wtf/Noncopyable.h> +#include <wtf/UnusedParam.h> + +namespace WebCore { + +class EditorClient; +class Range; +class TextCheckerClient; +class VisibleSelection; + +struct CorrectionPanelInfo { + enum PanelType { + PanelTypeCorrection = 0, + PanelTypeReversion, + PanelTypeSpellingSuggestions + }; + + RefPtr<Range> rangeToBeReplaced; + String replacedString; + String replacementString; + PanelType panelType; + bool isActive; +}; + +enum ReasonForDismissingCorrectionPanel { + ReasonForDismissingCorrectionPanelCancelled = 0, + ReasonForDismissingCorrectionPanelIgnored, + ReasonForDismissingCorrectionPanelAccepted +}; + +#if SUPPORT_AUTOCORRECTION_PANEL +#define UNLESS_ENABLED(functionBody) ; +#else +#define UNLESS_ENABLED(functionBody) functionBody +#endif + +class SpellingCorrectionController { + WTF_MAKE_NONCOPYABLE(SpellingCorrectionController); WTF_MAKE_FAST_ALLOCATED; +public: + SpellingCorrectionController(Frame*) UNLESS_ENABLED({}) + ~SpellingCorrectionController() UNLESS_ENABLED({}) + + void startCorrectionPanelTimer(CorrectionPanelInfo::PanelType) UNLESS_ENABLED({}) + void stopCorrectionPanelTimer() UNLESS_ENABLED({}) + + void dismiss(ReasonForDismissingCorrectionPanel) UNLESS_ENABLED({}) + String dismissSoon(ReasonForDismissingCorrectionPanel) UNLESS_ENABLED({ return String(); }) + void show(PassRefPtr<Range> rangeToReplace, const String& replacement) UNLESS_ENABLED({ UNUSED_PARAM(rangeToReplace); UNUSED_PARAM(replacement); }) + + void applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>&) UNLESS_ENABLED({}) + // Return true if correction was applied, false otherwise. + bool applyAutocorrectionBeforeTypingIfAppropriate() UNLESS_ENABLED({ return false; }) + + void respondToUnappliedSpellCorrection(const VisibleSelection&, const String& corrected, const String& correction) UNLESS_ENABLED({ UNUSED_PARAM(corrected); UNUSED_PARAM(correction); }) + void respondToAppliedEditing(PassRefPtr<EditCommand>) UNLESS_ENABLED({}) + void respondToChangedSelection(const VisibleSelection& oldSelection) UNLESS_ENABLED({ UNUSED_PARAM(oldSelection); }) + + void stopPendingCorrection(const VisibleSelection& oldSelection) UNLESS_ENABLED({ UNUSED_PARAM(oldSelection); }) + void applyPendingCorrection(const VisibleSelection& selectionAfterTyping) UNLESS_ENABLED({ UNUSED_PARAM(selectionAfterTyping); }) + + void handleCorrectionPanelResult(const String& correction) UNLESS_ENABLED({ UNUSED_PARAM(correction); }) + void handleCancelOperation() UNLESS_ENABLED({}) + + bool hasPendingCorrection() const UNLESS_ENABLED({ return false; }) + bool isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const UNLESS_ENABLED({ UNUSED_PARAM(misspellingRange); return true; }) + bool isAutomaticSpellingCorrectionEnabled() UNLESS_ENABLED({ return false; }) + bool shouldRemoveMarkersUponEditing() { return REMOVE_MARKERS_UPON_EDITING; } + + void correctionPanelTimerFired(Timer<SpellingCorrectionController>*) UNLESS_ENABLED({}) + void recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange) UNLESS_ENABLED({ UNUSED_PARAM(replacedString); UNUSED_PARAM(replacementRange); }) + void markReversed(PassRefPtr<Range> changedRange) UNLESS_ENABLED({ UNUSED_PARAM(changedRange); }) + void markCorrection(PassRefPtr<Range> replacedRange, const String& replacedString) UNLESS_ENABLED({ UNUSED_PARAM(replacedRange); UNUSED_PARAM(replacedString); }) + void recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction) UNLESS_ENABLED({ UNUSED_PARAM(rangeOfCorrection); UNUSED_PARAM(corrected); UNUSED_PARAM(correction); }) + +#if SUPPORT_AUTOCORRECTION_PANEL +private: + void recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString); + + bool shouldStartTimeFor(const DocumentMarker& marker, int endOffset) const + { + return (((marker.type == DocumentMarker::Replacement && !marker.description.isNull()) + || marker.type == DocumentMarker::Spelling) && static_cast<int>(marker.endOffset) == endOffset); + } + + EditorClient* client(); + TextCheckerClient* textChecker(); + FloatRect windowRectForRange(const Range*) const; + + EditorClient* m_client; + Frame* m_frame; + + Timer<SpellingCorrectionController> m_correctionPanelTimer; + CorrectionPanelInfo m_correctionPanelInfo; + bool m_correctionPanelIsDismissedByEditor; +#endif +}; + +#undef UNLESS_ENABLED + +} // namespace WebCore + +#endif // SpellingCorrectionController_h diff --git a/Source/WebCore/editing/TextCheckingHelper.cpp b/Source/WebCore/editing/TextCheckingHelper.cpp index 009c807..3912a49 100644 --- a/Source/WebCore/editing/TextCheckingHelper.cpp +++ b/Source/WebCore/editing/TextCheckingHelper.cpp @@ -274,7 +274,7 @@ String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, b unsigned grammarDetailIndex = 0; Vector<TextCheckingResult> results; - uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + TextCheckingTypeMask checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; m_client->textChecker()->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); for (unsigned i = 0; i < results.size(); i++) { @@ -524,7 +524,7 @@ Vector<String> TextCheckingHelper::guessesForMisspelledOrUngrammaticalRange(bool return guesses; Vector<TextCheckingResult> results; - uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + TextCheckingTypeMask checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; m_client->textChecker()->checkTextOfParagraph(paragraph.textCharacters(), paragraph.textLength(), checkingTypes, results); for (unsigned i = 0; i < results.size(); i++) { diff --git a/Source/WebCore/editing/TextGranularity.h b/Source/WebCore/editing/TextGranularity.h index 09cc4ed..965d7a0 100644 --- a/Source/WebCore/editing/TextGranularity.h +++ b/Source/WebCore/editing/TextGranularity.h @@ -39,7 +39,10 @@ enum TextGranularity { SentenceBoundary, LineBoundary, ParagraphBoundary, - DocumentBoundary + DocumentBoundary, + // FIXME: this is added temporarily for experiment with visually move + // caret by wordGranularity. Once all patches are landed, it should be removed. + WebKitVisualWordGranularity }; } diff --git a/Source/WebCore/editing/TextIterator.cpp b/Source/WebCore/editing/TextIterator.cpp index 1b25c87..c3be277 100644 --- a/Source/WebCore/editing/TextIterator.cpp +++ b/Source/WebCore/editing/TextIterator.cpp @@ -28,6 +28,7 @@ #include "TextIterator.h" #include "Document.h" +#include "Frame.h" #include "HTMLElement.h" #include "HTMLNames.h" #include "htmlediting.h" @@ -256,6 +257,7 @@ TextIterator::TextIterator() , m_emitsTextWithoutTranscoding(false) , m_handledFirstLetter(false) , m_ignoresStyleVisibility(false) + , m_emitsObjectReplacementCharacters(false) { } @@ -274,6 +276,7 @@ TextIterator::TextIterator(const Range* r, TextIteratorBehavior behavior) , m_emitsTextWithoutTranscoding(behavior & TextIteratorEmitsTextsWithoutTranscoding) , m_handledFirstLetter(false) , m_ignoresStyleVisibility(behavior & TextIteratorIgnoresStyleVisibility) + , m_emitsObjectReplacementCharacters(behavior & TextIteratorEmitsObjectReplacementCharacters) { if (!r) return; @@ -638,6 +641,11 @@ bool TextIterator::handleReplacedElement() m_hasEmitted = true; + if (m_emitsObjectReplacementCharacters && renderer && renderer->isReplaced()) { + emitCharacter(objectReplacementCharacter, m_node->parentNode(), m_node, 0, 1); + return true; + } + if (m_emitsCharactersBetweenAllVisiblePositions) { // We want replaced elements to behave like punctuation for boundary // finding, and to simply take up space for the selection preservation @@ -2369,6 +2377,38 @@ PassRefPtr<Range> TextIterator::rangeFromLocationAndLength(Element* scope, int r return resultRange.release(); } +bool TextIterator::locationAndLengthFromRange(const Range* range, size_t& location, size_t& length) +{ + location = notFound; + length = 0; + + if (!range->startContainer()) + return false; + + Element* selectionRoot = range->ownerDocument()->frame()->selection()->rootEditableElement(); + Element* scope = selectionRoot ? selectionRoot : range->ownerDocument()->documentElement(); + + // The critical assumption is that this only gets called with ranges that + // concentrate on a given area containing the selection root. This is done + // because of text fields and textareas. The DOM for those is not + // directly in the document DOM, so ensure that the range does not cross a + // boundary of one of those. + if (range->startContainer() != scope && !range->startContainer()->isDescendantOf(scope)) + return false; + if (range->endContainer() != scope && !range->endContainer()->isDescendantOf(scope)) + return false; + + RefPtr<Range> testRange = Range::create(scope->document(), scope, 0, range->startContainer(), range->startOffset()); + ASSERT(testRange->startContainer() == scope); + location = TextIterator::rangeLength(testRange.get()); + + ExceptionCode ec; + testRange->setEnd(range->endContainer(), range->endOffset(), ec); + ASSERT(testRange->startContainer() == scope); + length = TextIterator::rangeLength(testRange.get()) - location; + return true; +} + // -------- UChar* plainTextToMallocAllocatedBuffer(const Range* r, unsigned& bufferLength, bool isDisplayString, TextIteratorBehavior defaultBehavior) diff --git a/Source/WebCore/editing/TextIterator.h b/Source/WebCore/editing/TextIterator.h index 0f1a6fd..9fe4ceb 100644 --- a/Source/WebCore/editing/TextIterator.h +++ b/Source/WebCore/editing/TextIterator.h @@ -41,7 +41,8 @@ enum TextIteratorBehavior { TextIteratorEmitsCharactersBetweenAllVisiblePositions = 1 << 0, TextIteratorEntersTextControls = 1 << 1, TextIteratorEmitsTextsWithoutTranscoding = 1 << 2, - TextIteratorIgnoresStyleVisibility = 1 << 3 + TextIteratorIgnoresStyleVisibility = 1 << 3, + TextIteratorEmitsObjectReplacementCharacters = 1 << 4 }; // FIXME: Can't really answer this question correctly without knowing the white-space mode. @@ -98,6 +99,7 @@ public: static int rangeLength(const Range*, bool spacesForReplacedElements = false); static PassRefPtr<Range> rangeFromLocationAndLength(Element* scope, int rangeLocation, int rangeLength, bool spacesForReplacedElements = false); + static bool locationAndLengthFromRange(const Range*, size_t& location, size_t& length); static PassRefPtr<Range> subrange(Range* entireRange, int characterOffset, int characterCount); private: @@ -178,6 +180,8 @@ private: bool m_handledFirstLetter; // Used when the visibility of the style should not affect text gathering. bool m_ignoresStyleVisibility; + // Used when emitting the special 0xFFFC character is required. + bool m_emitsObjectReplacementCharacters; }; // Iterates through the DOM range, returning all the text, and 0-length boundaries diff --git a/Source/WebCore/editing/TypingCommand.cpp b/Source/WebCore/editing/TypingCommand.cpp index aedda31..da93fab 100644 --- a/Source/WebCore/editing/TypingCommand.cpp +++ b/Source/WebCore/editing/TypingCommand.cpp @@ -39,6 +39,7 @@ #include "InsertTextCommand.h" #include "RenderObject.h" #include "SelectionController.h" +#include "TextIterator.h" #include "VisiblePosition.h" #include "htmlediting.h" #include "visible_units.h" @@ -49,29 +50,34 @@ using namespace HTMLNames; static bool canAppendNewLineFeed(const VisibleSelection& selection) { - ExceptionCode ec = 0; + Node* node = selection.rootEditableElement(); + if (!node) + return false; + RefPtr<BeforeTextInsertedEvent> event = BeforeTextInsertedEvent::create(String("\n")); - selection.rootEditableElement()->dispatchEvent(event, ec); + ExceptionCode ec = 0; + node->dispatchEvent(event, ec); return event->text().length(); } -TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, TypingCommandOptions options, TextGranularity granularity, TextCompositionType compositionType) +TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, Options options, TextGranularity granularity, TextCompositionType compositionType) : CompositeEditCommand(document) , m_commandType(commandType) , m_textToInsert(textToInsert) , m_openForMoreTyping(true) , m_selectInsertedText(options & SelectInsertedText) - , m_smartDelete(false) + , m_smartDelete(options & SmartDelete) , m_granularity(granularity) , m_compositionType(compositionType) , m_killRing(options & KillRing) , m_openedByBackwardDelete(false) , m_shouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator) + , m_shouldPreventSpellChecking(options & PreventSpellChecking) { updatePreservesTypingStyle(m_commandType); } -void TypingCommand::deleteSelection(Document* document, bool smartDelete) +void TypingCommand::deleteSelection(Document* document, Options options) { ASSERT(document); @@ -83,16 +89,16 @@ void TypingCommand::deleteSelection(Document* document, bool smartDelete) EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); if (isOpenForMoreTypingCommand(lastEditCommand)) { - static_cast<TypingCommand*>(lastEditCommand)->deleteSelection(smartDelete); + TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand); + lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking); + lastTypingCommand->deleteSelection(options & SmartDelete); return; } - RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteSelection, "", 0); - typingCommand->setSmartDelete(smartDelete); - typingCommand->apply(); + TypingCommand::create(document, DeleteSelection, "", options)->apply(); } -void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing) +void TypingCommand::deleteKeyPressed(Document *document, Options options, TextGranularity granularity) { ASSERT(document); @@ -101,18 +107,17 @@ void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextG EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); if (granularity == CharacterGranularity && isOpenForMoreTypingCommand(lastEditCommand)) { - updateSelectionIfDifferentFromCurrentSelection(static_cast<TypingCommand*>(lastEditCommand), frame); - static_cast<TypingCommand*>(lastEditCommand)->deleteKeyPressed(granularity, killRing); + TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand); + updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand, frame); + lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking); + lastTypingCommand->deleteKeyPressed(granularity, options & KillRing); return; } - TypingCommandOptions options = killRing ? KillRing : 0; - RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteKey, "", options, granularity); - typingCommand->setSmartDelete(smartDelete); - typingCommand->apply(); + TypingCommand::create(document, DeleteKey, "", options, granularity)->apply(); } -void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing) +void TypingCommand::forwardDeleteKeyPressed(Document *document, Options options, TextGranularity granularity) { // FIXME: Forward delete in TextEdit appears to open and close a new typing command. ASSERT(document); @@ -122,15 +127,14 @@ void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); if (granularity == CharacterGranularity && isOpenForMoreTypingCommand(lastEditCommand)) { - updateSelectionIfDifferentFromCurrentSelection(static_cast<TypingCommand*>(lastEditCommand), frame); - static_cast<TypingCommand*>(lastEditCommand)->forwardDeleteKeyPressed(granularity, killRing); + TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand); + updateSelectionIfDifferentFromCurrentSelection(lastTypingCommand, frame); + lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking); + lastTypingCommand->forwardDeleteKeyPressed(granularity, options & KillRing); return; } - TypingCommandOptions options = killRing ? KillRing : 0; - RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, ForwardDeleteKey, "", options, granularity); - typingCommand->setSmartDelete(smartDelete); - typingCommand->apply(); + TypingCommand::create(document, ForwardDeleteKey, "", options, granularity)->apply(); } void TypingCommand::updateSelectionIfDifferentFromCurrentSelection(TypingCommand* typingCommand, Frame* frame) @@ -144,23 +148,21 @@ void TypingCommand::updateSelectionIfDifferentFromCurrentSelection(TypingCommand typingCommand->setEndingSelection(currentSelection); } -void TypingCommand::insertText(Document* document, const String& text, TypingCommandOptions options, TextCompositionType composition) +void TypingCommand::insertText(Document* document, const String& text, Options options, TextCompositionType composition) { ASSERT(document); Frame* frame = document->frame(); ASSERT(frame); -#if REMOVE_MARKERS_UPON_EDITING if (!text.isEmpty()) - document->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(isSpaceOrNewline(text.characters()[0])); -#endif - + document->frame()->editor()->updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text.characters()[0])); + insertText(document, text, frame->selection()->selection(), options, composition); } // FIXME: We shouldn't need to take selectionForInsertion. It should be identical to SelectionController's current selection. -void TypingCommand::insertText(Document* document, const String& text, const VisibleSelection& selectionForInsertion, TypingCommandOptions options, TextCompositionType compositionType) +void TypingCommand::insertText(Document* document, const String& text, const VisibleSelection& selectionForInsertion, Options options, TextCompositionType compositionType) { ASSERT(document); @@ -196,6 +198,7 @@ void TypingCommand::insertText(Document* document, const String& text, const Vis lastTypingCommand->setCompositionType(compositionType); lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator); + lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking); lastTypingCommand->insertText(newText, options & SelectInsertedText); return; } @@ -212,7 +215,7 @@ void TypingCommand::insertText(Document* document, const String& text, const Vis } } -void TypingCommand::insertLineBreak(Document *document, TypingCommandOptions options) +void TypingCommand::insertLineBreak(Document *document, Options options) { ASSERT(document); @@ -246,7 +249,7 @@ void TypingCommand::insertParagraphSeparatorInQuotedContent(Document *document) applyCommand(TypingCommand::create(document, InsertParagraphSeparatorInQuotedContent)); } -void TypingCommand::insertParagraphSeparator(Document *document, TypingCommandOptions options) +void TypingCommand::insertParagraphSeparator(Document *document, Options options) { ASSERT(document); @@ -338,14 +341,14 @@ void TypingCommand::markMisspellingsAfterTyping(ETypingCommand commandType) if (previous.isNotNull()) { VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary); VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary); - if (p1 != p2) - document()->frame()->editor()->markMisspellingsAfterTypingToWord(p1, endingSelection()); -#if SUPPORT_AUTOCORRECTION_PANEL - else if (commandType == TypingCommand::InsertText) - document()->frame()->editor()->startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeCorrection); -#else - UNUSED_PARAM(commandType); -#endif + if (p1 != p2) { + RefPtr<Range> range = makeRange(p1, p2); + String strippedPreviousWord; + if (range && (commandType == TypingCommand::InsertText || commandType == TypingCommand::InsertLineBreak || commandType == TypingCommand::InsertParagraphSeparator || commandType == TypingCommand::InsertParagraphSeparatorInQuotedContent)) + strippedPreviousWord = plainText(range.get()).stripWhiteSpace(); + document()->frame()->editor()->markMisspellingsAfterTypingToWord(p1, endingSelection(), !strippedPreviousWord.isEmpty()); + } else if (commandType == TypingCommand::InsertText) + document()->frame()->editor()->startCorrectionPanelTimer(); } } @@ -356,7 +359,8 @@ void TypingCommand::typingAddedToOpenCommand(ETypingCommand commandTypeForAddedT #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) document()->frame()->editor()->appliedEditing(this); // Since the spellchecking code may also perform corrections and other replacements, it should happen after the typing changes. - markMisspellingsAfterTyping(commandTypeForAddedTyping); + if (!m_shouldPreventSpellChecking) + markMisspellingsAfterTyping(commandTypeForAddedTyping); #else // The old spellchecking code requires that checking be done first, to prevent issues like that in 6864072, where <doesn't> is marked as misspelled. markMisspellingsAfterTyping(commandTypeForAddedTyping); @@ -463,9 +467,8 @@ bool TypingCommand::makeEditableRootEmpty() void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) { -#if REMOVE_MARKERS_UPON_EDITING - document()->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); -#endif + document()->frame()->editor()->updateMarkersForWordsAffectedByEditing(false); + VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; @@ -562,9 +565,8 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) { -#if REMOVE_MARKERS_UPON_EDITING - document()->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); -#endif + document()->frame()->editor()->updateMarkersForWordsAffectedByEditing(false); + VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; diff --git a/Source/WebCore/editing/TypingCommand.h b/Source/WebCore/editing/TypingCommand.h index beb4d02..a10634f 100644 --- a/Source/WebCore/editing/TypingCommand.h +++ b/Source/WebCore/editing/TypingCommand.h @@ -48,20 +48,22 @@ public: TextCompositionConfirm }; - enum TypingCommandOption { + enum Option { SelectInsertedText = 1 << 0, KillRing = 1 << 1, - RetainAutocorrectionIndicator = 1 << 2 + RetainAutocorrectionIndicator = 1 << 2, + PreventSpellChecking = 1 << 3, + SmartDelete = 1 << 4 }; - typedef unsigned TypingCommandOptions; - - static void deleteSelection(Document*, bool smartDelete = false); - static void deleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity, bool killRing = false); - static void forwardDeleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity, bool killRing = false); - static void insertText(Document*, const String&, TypingCommandOptions, TextCompositionType = TextCompositionNone); - static void insertText(Document*, const String&, const VisibleSelection&, TypingCommandOptions, TextCompositionType = TextCompositionNone); - static void insertLineBreak(Document*, TypingCommandOptions); - static void insertParagraphSeparator(Document*, TypingCommandOptions); + typedef unsigned Options; + + static void deleteSelection(Document*, Options = 0); + static void deleteKeyPressed(Document*, Options = 0, TextGranularity = CharacterGranularity); + static void forwardDeleteKeyPressed(Document*, Options = 0, TextGranularity = CharacterGranularity); + static void insertText(Document*, const String&, Options, TextCompositionType = TextCompositionNone); + static void insertText(Document*, const String&, const VisibleSelection&, Options, TextCompositionType = TextCompositionNone); + static void insertLineBreak(Document*, Options); + static void insertParagraphSeparator(Document*, Options); static void insertParagraphSeparatorInQuotedContent(Document*); static bool isOpenForMoreTypingCommand(const EditCommand*); static void closeTyping(EditCommand*); @@ -80,17 +82,17 @@ public: void setCompositionType(TextCompositionType type) { m_compositionType = type; } private: - static PassRefPtr<TypingCommand> create(Document* document, ETypingCommand command, const String& text = "", TypingCommandOptions options = 0, TextGranularity granularity = CharacterGranularity) + static PassRefPtr<TypingCommand> create(Document* document, ETypingCommand command, const String& text = "", Options options = 0, TextGranularity granularity = CharacterGranularity) { return adoptRef(new TypingCommand(document, command, text, options, granularity, TextCompositionNone)); } - static PassRefPtr<TypingCommand> create(Document* document, ETypingCommand command, const String& text, TypingCommandOptions options, TextCompositionType compositionType) + static PassRefPtr<TypingCommand> create(Document* document, ETypingCommand command, const String& text, Options options, TextCompositionType compositionType) { return adoptRef(new TypingCommand(document, command, text, options, CharacterGranularity, compositionType)); } - TypingCommand(Document*, ETypingCommand, const String& text, TypingCommandOptions, TextGranularity, TextCompositionType); + TypingCommand(Document*, ETypingCommand, const String& text, Options, TextGranularity, TextCompositionType); bool smartDelete() const { return m_smartDelete; } void setSmartDelete(bool smartDelete) { m_smartDelete = smartDelete; } @@ -101,6 +103,7 @@ private: virtual bool preservesTypingStyle() const { return m_preservesTypingStyle; } virtual bool shouldRetainAutocorrectionIndicator() const { return m_shouldRetainAutocorrectionIndicator; } virtual void setShouldRetainAutocorrectionIndicator(bool retain) { m_shouldRetainAutocorrectionIndicator = retain; } + void setShouldPreventSpellChecking(bool prevent) { m_shouldPreventSpellChecking = prevent; } static void updateSelectionIfDifferentFromCurrentSelection(TypingCommand*, Frame*); @@ -125,6 +128,7 @@ private: bool m_openedByBackwardDelete; bool m_shouldRetainAutocorrectionIndicator; + bool m_shouldPreventSpellChecking; }; } // namespace WebCore diff --git a/Source/WebCore/editing/VisibleSelection.cpp b/Source/WebCore/editing/VisibleSelection.cpp index 5633c90..9e9692e 100644 --- a/Source/WebCore/editing/VisibleSelection.cpp +++ b/Source/WebCore/editing/VisibleSelection.cpp @@ -380,6 +380,8 @@ void VisibleSelection::setStartAndEndFromBaseAndExtentRespectingGranularity(Text m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); break; + case WebKitVisualWordGranularity: + break; } // Make sure we do not have a dangling start or end. diff --git a/Source/WebCore/editing/htmlediting.cpp b/Source/WebCore/editing/htmlediting.cpp index 564eff6..d89e6c5 100644 --- a/Source/WebCore/editing/htmlediting.cpp +++ b/Source/WebCore/editing/htmlediting.cpp @@ -122,7 +122,8 @@ int comparePositions(const Position& a, const Position& b) } } - int result = Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB); + ExceptionCode ec; + int result = Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB, ec); return result ? result : bias; } @@ -638,7 +639,7 @@ Node* highestEnclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const N { Node* highest = 0; Node* root = rule == CannotCrossEditingBoundary ? highestEditableRoot(p) : 0; - for (Node* n = p.deprecatedNode(); n; n = n->parentNode()) { + for (Node* n = p.containerNode(); n; n = n->parentNode()) { if (root && !n->rendererIsEditable()) continue; if (nodeIsOfType(n)) @@ -894,14 +895,17 @@ bool isNodeInTextFormControl(Node* node) return ancestor->isElementNode() && static_cast<Element*>(ancestor)->isTextFormControl(); } -Position positionBeforeTabSpan(const Position& pos) +Position positionOutsideTabSpan(const Position& pos) { - Node* node = pos.deprecatedNode(); + Node* node = pos.containerNode(); if (isTabSpanTextNode(node)) node = tabSpanNode(node); else if (!isTabSpanNode(node)) return pos; - + + if (node && VisiblePosition(pos) == lastPositionInNode(node)) + return positionInParentAfterNode(node); + return positionInParentBeforeNode(node); } diff --git a/Source/WebCore/editing/htmlediting.h b/Source/WebCore/editing/htmlediting.h index cb2b2a4..a006da6 100644 --- a/Source/WebCore/editing/htmlediting.h +++ b/Source/WebCore/editing/htmlediting.h @@ -111,7 +111,7 @@ Position previousCandidate(const Position&); Position nextVisuallyDistinctCandidate(const Position&); Position previousVisuallyDistinctCandidate(const Position&); -Position positionBeforeTabSpan(const Position&); +Position positionOutsideTabSpan(const Position&); Position positionBeforeContainingSpecialElement(const Position&, Node** containingSpecialElement=0); Position positionAfterContainingSpecialElement(const Position&, Node** containingSpecialElement=0); Position positionOutsideContainingSpecialElement(const Position&, Node** containingSpecialElement=0); @@ -224,6 +224,15 @@ inline bool isWhitespace(UChar c) { return c == noBreakSpace || c == ' ' || c == '\n' || c == '\t'; } + +inline bool isAmbiguousBoundaryCharacter(UChar character) +{ + // These are characters that can behave as word boundaries, but can appear within words. + // If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed. + // FIXME: this is required until 6853027 is fixed and text checking can do this for us. + return character == '\'' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim; +} + String stringWithRebalancedWhitespace(const String&, bool startIsStartOfParagraph, bool endIsEndOfParagraph); const String& nonBreakingSpaceString(); diff --git a/Source/WebCore/editing/markup.cpp b/Source/WebCore/editing/markup.cpp index 316cec7..b93a78e 100644 --- a/Source/WebCore/editing/markup.cpp +++ b/Source/WebCore/editing/markup.cpp @@ -465,7 +465,7 @@ static bool isElementPresentational(const Node* node) RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(node); if (!style) return false; - return !propertyMissingOrEqualToNone(style.get(), CSSPropertyTextDecoration); + return !propertyMissingOrEqualToNone(style.get(), CSSPropertyTextDecoration) || !Editor::hasTransparentBackgroundColor(style.get()); } static bool shouldIncludeWrapperForFullySelectedRoot(Node* fullySelectedRoot, CSSMutableStyleDeclaration* style) @@ -571,11 +571,14 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc accumulator.appendString(interchangeNewlineString); startNode = visibleStart.next().deepEquivalent().deprecatedNode(); - if (pastEnd && Range::compareBoundaryPoints(startNode, 0, pastEnd, 0) >= 0) { + ExceptionCode ec = 0; + if (pastEnd && Range::compareBoundaryPoints(startNode, 0, pastEnd, 0, ec) >= 0) { + ASSERT(!ec); if (deleteButton) deleteButton->enable(); return interchangeNewlineString; } + ASSERT(!ec); } Node* body = enclosingNodeWithTag(firstPositionInNode(commonAncestor), bodyTag); diff --git a/Source/WebCore/editing/qt/EditorQt.cpp b/Source/WebCore/editing/qt/EditorQt.cpp index 7fb3634..ffb24e7 100644 --- a/Source/WebCore/editing/qt/EditorQt.cpp +++ b/Source/WebCore/editing/qt/EditorQt.cpp @@ -31,9 +31,9 @@ #include "ClipboardQt.h" #include "Document.h" #include "Element.h" -#include "VisibleSelection.h" #include "SelectionController.h" #include "TextIterator.h" +#include "VisibleSelection.h" #include "htmlediting.h" #include "visible_units.h" diff --git a/Source/WebCore/editing/qt/SmartReplaceQt.cpp b/Source/WebCore/editing/qt/SmartReplaceQt.cpp index 1436afe..dcd8bf7 100644 --- a/Source/WebCore/editing/qt/SmartReplaceQt.cpp +++ b/Source/WebCore/editing/qt/SmartReplaceQt.cpp @@ -40,15 +40,15 @@ bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter) if (!isPreviousCharacter && d.isPunct()) return true; - if ((c >= 0x1100 && c <= (0x1100 + 256)) // Hangul Jamo (0x1100 - 0x11FF) - || (c >= 0x2E80 && c <= (0x2E80 + 352)) // CJK & Kangxi Radicals (0x2E80 - 0x2FDF) - || (c >= 0x2FF0 && c <= (0x2FF0 + 464)) // Ideograph Deseriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF) - || (c >= 0x3200 && c <= (0x3200 + 29392)) // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF) - || (c >= 0xAC00 && c <= (0xAC00 + 11183)) // Hangul Syllables (0xAC00 - 0xD7AF) - || (c >= 0xF900 && c <= (0xF900 + 352)) // CJK Compatibility Ideographs (0xF900 - 0xFA5F) - || (c >= 0xFE30 && c <= (0xFE30 + 32)) // CJK Compatibility From (0xFE30 - 0xFE4F) - || (c >= 0xFF00 && c <= (0xFF00 + 240)) // Half/Full Width Form (0xFF00 - 0xFFEF) - || (c >= 0x20000 && c <= (0x20000 + 0xA6D7)) // CJK Ideograph Exntension B + if ((c >= 0x1100 && c <= (0x1100 + 256)) // Hangul Jamo (0x1100 - 0x11FF) + || (c >= 0x2E80 && c <= (0x2E80 + 352)) // CJK & Kangxi Radicals (0x2E80 - 0x2FDF) + || (c >= 0x2FF0 && c <= (0x2FF0 + 464)) // Ideograph Deseriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF) + || (c >= 0x3200 && c <= (0x3200 + 29392)) // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF) + || (c >= 0xAC00 && c <= (0xAC00 + 11183)) // Hangul Syllables (0xAC00 - 0xD7AF) + || (c >= 0xF900 && c <= (0xF900 + 352)) // CJK Compatibility Ideographs (0xF900 - 0xFA5F) + || (c >= 0xFE30 && c <= (0xFE30 + 32)) // CJK Compatibility From (0xFE30 - 0xFE4F) + || (c >= 0xFF00 && c <= (0xFF00 + 240)) // Half/Full Width Form (0xFF00 - 0xFFEF) + || (c >= 0x20000 && c <= (0x20000 + 0xA6D7)) // CJK Ideograph Exntension B || (c >= 0x2F800 && c <= (0x2F800 + 0x021E))) // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D) return true; diff --git a/Source/WebCore/editing/visible_units.cpp b/Source/WebCore/editing/visible_units.cpp index c9ca8c0..e4a84ba 100644 --- a/Source/WebCore/editing/visible_units.cpp +++ b/Source/WebCore/editing/visible_units.cpp @@ -37,6 +37,7 @@ #include "TextBreakIterator.h" #include "TextIterator.h" #include "VisiblePosition.h" +#include "VisibleSelection.h" #include "htmlediting.h" #include <wtf/unicode/Unicode.h> @@ -1146,4 +1147,396 @@ VisiblePosition rightBoundaryOfLine(const VisiblePosition& c, TextDirection dire return direction == LTR ? logicalEndOfLine(c) : logicalStartOfLine(c); } +static const int invalidOffset = -1; + +static VisiblePosition previousWordBreakInBoxInsideBlockWithSameDirectionality(const InlineBox* box, const VisiblePosition& previousWordBreak, int& offsetOfWordBreak) +{ + bool hasSeenWordBreakInThisBox = previousWordBreak.isNotNull(); + // In a LTR block, the word break should be on the left boundary of a word. + // In a RTL block, the word break should be on the right boundary of a word. + // Because nextWordPosition() returns the word break on the right boundary of the word for LTR text, + // we need to use previousWordPosition() to traverse words within the inline boxes from right to left + // to find the previous word break (i.e. the first word break on the left). The same applies to RTL text. + + VisiblePosition wordBreak = hasSeenWordBreakInThisBox ? previousWordBreak : Position(box->renderer()->node(), box->caretMaxOffset(), Position::PositionIsOffsetInAnchor); + + // FIXME: handle multi-spaces (http://webkit.org/b/57543). + + wordBreak = previousWordPosition(wordBreak); + if (previousWordBreak == wordBreak) + return VisiblePosition(); + + InlineBox* boxContainingPreviousWordBreak; + wordBreak.getInlineBoxAndOffset(boxContainingPreviousWordBreak, offsetOfWordBreak); + if (boxContainingPreviousWordBreak != box) + return VisiblePosition(); + return wordBreak; +} + +static VisiblePosition leftmostPositionInRTLBoxInLTRBlock(const InlineBox* box) +{ + // FIXME: Probably need to take care of bidi level too. + Node* node = box->renderer()->node(); + InlineBox* previousLeaf = box->prevLeafChild(); + InlineBox* nextLeaf = box->nextLeafChild(); + + if (previousLeaf && !previousLeaf->isLeftToRightDirection()) + return Position(node, box->caretMaxOffset(), Position::PositionIsOffsetInAnchor); + + if (nextLeaf && !nextLeaf->isLeftToRightDirection()) { + if (previousLeaf) + return Position(previousLeaf->renderer()->node(), previousLeaf->caretMaxOffset(), Position::PositionIsOffsetInAnchor); + + InlineBox* lastRTLLeaf; + do { + lastRTLLeaf = nextLeaf; + nextLeaf = nextLeaf->nextLeafChild(); + } while (nextLeaf && !nextLeaf->isLeftToRightDirection()); + return Position(lastRTLLeaf->renderer()->node(), lastRTLLeaf->caretMinOffset(), Position::PositionIsOffsetInAnchor); + } + + return Position(node, box->caretMinOffset(), Position::PositionIsOffsetInAnchor); +} + +static VisiblePosition rightmostPositionInLTRBoxInRTLBlock(const InlineBox* box) +{ + // FIXME: Probably need to take care of bidi level too. + Node* node = box->renderer()->node(); + InlineBox* previousLeaf = box->prevLeafChild(); + InlineBox* nextLeaf = box->nextLeafChild(); + + if (nextLeaf && nextLeaf->isLeftToRightDirection()) + return Position(node, box->caretMaxOffset(), Position::PositionIsOffsetInAnchor); + + if (previousLeaf && previousLeaf->isLeftToRightDirection()) { + if (nextLeaf) + return Position(nextLeaf->renderer()->node(), nextLeaf->caretMaxOffset(), Position::PositionIsOffsetInAnchor); + + InlineBox* firstLTRLeaf; + do { + firstLTRLeaf = previousLeaf; + previousLeaf = previousLeaf->prevLeafChild(); + } while (previousLeaf && previousLeaf->isLeftToRightDirection()); + return Position(firstLTRLeaf->renderer()->node(), firstLTRLeaf->caretMinOffset(), Position::PositionIsOffsetInAnchor); + } + + return Position(node, box->caretMinOffset(), Position::PositionIsOffsetInAnchor); +} + +static VisiblePosition lastWordBreakInBox(const InlineBox* box, int& offsetOfWordBreak) +{ + // Add the leftmost word break for RTL box or rightmost word break for LTR box. + InlineBox* previousLeaf = box->prevLeafChild(); + InlineBox* nextLeaf = box->nextLeafChild(); + VisiblePosition boundaryPosition; + if (box->direction() == RTL && (!previousLeaf || previousLeaf->isLeftToRightDirection())) + boundaryPosition = leftmostPositionInRTLBoxInLTRBlock(box); + else if (box->direction() == LTR && (!nextLeaf || !nextLeaf->isLeftToRightDirection())) + boundaryPosition = rightmostPositionInLTRBoxInRTLBlock(box); + + if (boundaryPosition.isNull()) + return VisiblePosition(); + + VisiblePosition wordBreak = nextWordPosition(boundaryPosition); + if (wordBreak != boundaryPosition) + wordBreak = previousWordPosition(wordBreak); + + InlineBox* boxOfWordBreak; + wordBreak.getInlineBoxAndOffset(boxOfWordBreak, offsetOfWordBreak); + if (boxOfWordBreak == box) + return wordBreak; + return VisiblePosition(); +} + +static bool positionIsVisuallyOrderedInBoxInBlockWithDifferentDirectionality(const VisiblePosition& wordBreak, const InlineBox* box, int& offsetOfWordBreak) +{ + int previousOffset = offsetOfWordBreak; + InlineBox* boxOfWordBreak; + wordBreak.getInlineBoxAndOffset(boxOfWordBreak, offsetOfWordBreak); + if (boxOfWordBreak == box && (previousOffset == invalidOffset || previousOffset < offsetOfWordBreak)) + return true; + return false; +} + +static VisiblePosition nextWordBreakInBoxInsideBlockWithDifferentDirectionality( + const InlineBox* box, const VisiblePosition& previousWordBreak, int& offsetOfWordBreak, bool& isLastWordBreakInBox) +{ + // FIXME: Probably need to take care of bidi level too. + + // In a LTR block, the word break should be on the left boundary of a word. + // In a RTL block, the word break should be on the right boundary of a word. + // Because previousWordPosition() returns the word break on the right boundary of the word for RTL text, + // we need to use nextWordPosition() to traverse words within the inline boxes from right to left to find the next word break. + // The same applies to LTR text, in which words are traversed within the inline boxes from left to right. + + // FIXME: handle multi-spaces (http://webkit.org/b/57543). + + bool hasSeenWordBreakInThisBox = previousWordBreak.isNotNull(); + VisiblePosition wordBreak = hasSeenWordBreakInThisBox ? previousWordBreak : Position(box->renderer()->node(), box->caretMinOffset(), Position::PositionIsOffsetInAnchor); + wordBreak = nextWordPosition(wordBreak); + + if (wordBreak == previousWordBreak) { + isLastWordBreakInBox = true; + return VisiblePosition(); + } + + + // Given RTL box "ABC DEF" either follows a LTR box or is the first visual box in an LTR block as an example, + // the visual display of the RTL box is: "(0)J(10)I(9)H(8) (7)F(6)E(5)D(4) (3)C(2)B(1)A(11)", + // where the number in parenthesis represents offset in visiblePosition. + // Start at offset 0, the first word break is at offset 3, the 2nd word break is at offset 7, and the 3rd word break should be at offset 0. + // But nextWordPosition() of offset 7 is offset 11, which should be ignored, + // and the position at offset 0 should be manually added as the last word break within the box. + if (positionIsVisuallyOrderedInBoxInBlockWithDifferentDirectionality(wordBreak, box, offsetOfWordBreak)) { + isLastWordBreakInBox = false; + return wordBreak; + } + + isLastWordBreakInBox = true; + return lastWordBreakInBox(box, offsetOfWordBreak); +} + +struct WordBoundaryEntry { + WordBoundaryEntry() + : offsetInInlineBox(invalidOffset) + { + } + + WordBoundaryEntry(const VisiblePosition& position, int offset) + : visiblePosition(position) + , offsetInInlineBox(offset) + { + } + + VisiblePosition visiblePosition; + int offsetInInlineBox; +}; + +typedef Vector<WordBoundaryEntry, 50> WordBoundaryVector; + +static void collectWordBreaksInBoxInsideBlockWithSameDirectionality(const InlineBox* box, WordBoundaryVector& orderedWordBoundaries) +{ + orderedWordBoundaries.clear(); + + VisiblePosition wordBreak; + int offsetOfWordBreak = invalidOffset; + while (1) { + wordBreak = previousWordBreakInBoxInsideBlockWithSameDirectionality(box, wordBreak, offsetOfWordBreak); + if (wordBreak.isNull()) + break; + WordBoundaryEntry wordBoundaryEntry(wordBreak, offsetOfWordBreak); + orderedWordBoundaries.append(wordBoundaryEntry); + } +} + +static void collectWordBreaksInBoxInsideBlockWithDifferntDirectionality(const InlineBox* box, WordBoundaryVector& orderedWordBoundaries) +{ + orderedWordBoundaries.clear(); + + VisiblePosition wordBreak; + int offsetOfWordBreak = invalidOffset; + while (1) { + bool isLastWordBreakInBox = false; + wordBreak = nextWordBreakInBoxInsideBlockWithDifferentDirectionality(box, wordBreak, offsetOfWordBreak, isLastWordBreakInBox); + if (wordBreak.isNotNull()) { + WordBoundaryEntry wordBoundaryEntry(wordBreak, offsetOfWordBreak); + orderedWordBoundaries.append(wordBoundaryEntry); + } + if (isLastWordBreakInBox) + break; + } +} + +static VisiblePosition previousWordBreakInBox(const InlineBox* box, int offset, TextDirection blockDirection) +{ + int offsetOfWordBreak = 0; + VisiblePosition wordBreak; + while (true) { + if (box->direction() == blockDirection) + wordBreak = previousWordBreakInBoxInsideBlockWithSameDirectionality(box, wordBreak, offsetOfWordBreak); + // FIXME: Implement the 'else' case when the box direction is not equal to the block direction. + if (wordBreak.isNull()) + break; + if (offset == invalidOffset || offsetOfWordBreak != offset) + return wordBreak; + } + return VisiblePosition(); +} + +static int greatestValueUnder(int offset, bool boxAndBlockAreInSameDirection, const WordBoundaryVector& orderedWordBoundaries) +{ + if (!orderedWordBoundaries.size()) + return invalidOffset; + // FIXME: binary search. + if (boxAndBlockAreInSameDirection) { + for (unsigned i = 0; i < orderedWordBoundaries.size(); ++i) { + if (orderedWordBoundaries[i].offsetInInlineBox < offset) + return i; + } + return invalidOffset; + } + for (int i = orderedWordBoundaries.size() - 1; i >= 0; --i) { + if (orderedWordBoundaries[i].offsetInInlineBox < offset) + return i; + } + return invalidOffset; +} + +static int smallestOffsetAbove(int offset, bool boxAndBlockAreInSameDirection, const WordBoundaryVector& orderedWordBoundaries) +{ + if (!orderedWordBoundaries.size()) + return invalidOffset; + // FIXME: binary search. + if (boxAndBlockAreInSameDirection) { + for (int i = orderedWordBoundaries.size() - 1; i >= 0; --i) { + if (orderedWordBoundaries[i].offsetInInlineBox > offset) + return i; + } + return invalidOffset; + } + for (unsigned i = 0; i < orderedWordBoundaries.size(); ++i) { + if (orderedWordBoundaries[i].offsetInInlineBox > offset) + return i; + } + return invalidOffset; +} + +static VisiblePosition leftWordBoundary(const InlineBox* box, int offset, TextDirection blockDirection) +{ + VisiblePosition wordBreak; + for (const InlineBox* adjacentBox = box; adjacentBox; adjacentBox = adjacentBox->prevLeafChild()) { + if (blockDirection == LTR) + wordBreak = previousWordBreakInBox(adjacentBox, adjacentBox == box ? offset : invalidOffset, blockDirection); + // FIXME: Implement the "else" case. + if (wordBreak.isNotNull()) + return wordBreak; + } + return VisiblePosition(); +} + +static VisiblePosition rightWordBoundary(const InlineBox* box, int offset, TextDirection blockDirection) +{ + + VisiblePosition wordBreak; + for (const InlineBox* adjacentBox = box; adjacentBox; adjacentBox = adjacentBox->nextLeafChild()) { + if (blockDirection == RTL) + wordBreak = previousWordBreakInBox(adjacentBox, adjacentBox == box ? offset : invalidOffset, blockDirection); + // FIXME: Implement the "else" case. + if (!wordBreak.isNull()) + return wordBreak; + } + return VisiblePosition(); +} + +static bool positionIsInsideBox(const VisiblePosition& wordBreak, const InlineBox* box) +{ + InlineBox* boxOfWordBreak; + int offsetOfWordBreak; + wordBreak.getInlineBoxAndOffset(boxOfWordBreak, offsetOfWordBreak); + return box == boxOfWordBreak && offsetOfWordBreak != box->caretMaxOffset() && offsetOfWordBreak != box->caretMinOffset(); +} + +static VisiblePosition positionBeforeNextWord(const VisiblePosition& position) +{ + VisiblePosition positionAfterCurrentWord = nextWordPosition(position); + VisiblePosition positionAfterNextWord = nextWordPosition(positionAfterCurrentWord); + if (positionAfterCurrentWord == positionAfterNextWord) + return positionAfterCurrentWord; + return previousWordPosition(positionAfterNextWord); +} + +static VisiblePosition positionAfterPreviousWord(const VisiblePosition& position) +{ + VisiblePosition positionBeforeCurrentWord = previousWordPosition(position); + VisiblePosition positionBeforePreviousWord = previousWordPosition(positionBeforeCurrentWord); + if (positionBeforeCurrentWord == positionBeforePreviousWord) + return positionBeforeCurrentWord; + return nextWordPosition(positionBeforePreviousWord); +} + +VisiblePosition leftWordPosition(const VisiblePosition& visiblePosition) +{ + InlineBox* box; + int offset; + visiblePosition.getInlineBoxAndOffset(box, offset); + TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent()); + + // FIXME: If the box's directionality is the same as that of the enclosing block, when the offset is at the box boundary + // and the direction is towards inside the box, do I still need to make it a special case? For example, a LTR box inside a LTR block, + // when offset is at box's caretMinOffset and the direction is DirectionRight, should it be taken care as a general case? + if (offset == box->caretLeftmostOffset()) + return leftWordBoundary(box->prevLeafChild(), invalidOffset, blockDirection); + if (offset == box->caretRightmostOffset()) + return leftWordBoundary(box, offset, blockDirection); + + + VisiblePosition wordBreak; + if (box->direction() == blockDirection) { + if (blockDirection == RTL) + wordBreak = positionBeforeNextWord(visiblePosition); + else + wordBreak = previousWordPosition(visiblePosition); + } else { + if (blockDirection == RTL) + wordBreak = positionAfterPreviousWord(visiblePosition); + else + wordBreak = nextWordPosition(visiblePosition); + } + if (positionIsInsideBox(wordBreak, box)) + return wordBreak; + + WordBoundaryVector orderedWordBoundaries; + if (box->direction() == blockDirection) + collectWordBreaksInBoxInsideBlockWithSameDirectionality(box, orderedWordBoundaries); + else + collectWordBreaksInBoxInsideBlockWithDifferntDirectionality(box, orderedWordBoundaries); + + int index = box->isLeftToRightDirection() ? greatestValueUnder(offset, blockDirection == LTR, orderedWordBoundaries) : + smallestOffsetAbove(offset, blockDirection == RTL, orderedWordBoundaries); + if (index != invalidOffset) + return orderedWordBoundaries[index].visiblePosition; + + return leftWordBoundary(box->prevLeafChild(), invalidOffset, blockDirection); +} + +VisiblePosition rightWordPosition(const VisiblePosition& visiblePosition) +{ + InlineBox* box; + int offset; + visiblePosition.getInlineBoxAndOffset(box, offset); + TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent()); + + if (offset == box->caretLeftmostOffset()) + return rightWordBoundary(box, offset, blockDirection); + if (offset == box->caretRightmostOffset()) + return rightWordBoundary(box->nextLeafChild(), invalidOffset, blockDirection); + + VisiblePosition wordBreak; + if (box->direction() == blockDirection) { + if (blockDirection == LTR) + wordBreak = positionBeforeNextWord(visiblePosition); + else + wordBreak = previousWordPosition(visiblePosition); + } else { + if (blockDirection == LTR) + wordBreak = positionAfterPreviousWord(visiblePosition); + else + wordBreak = nextWordPosition(visiblePosition); + } + if (positionIsInsideBox(wordBreak, box)) + return wordBreak; + + WordBoundaryVector orderedWordBoundaries; + if (box->direction() == blockDirection) + collectWordBreaksInBoxInsideBlockWithSameDirectionality(box, orderedWordBoundaries); + else + collectWordBreaksInBoxInsideBlockWithDifferntDirectionality(box, orderedWordBoundaries); + int index = box->isLeftToRightDirection() ? smallestOffsetAbove(offset, blockDirection == LTR, orderedWordBoundaries) : + greatestValueUnder(offset, blockDirection == RTL, orderedWordBoundaries); + if (index != invalidOffset) + return orderedWordBoundaries[index].visiblePosition; + + return rightWordBoundary(box->nextLeafChild(), invalidOffset, blockDirection); +} + } diff --git a/Source/WebCore/editing/visible_units.h b/Source/WebCore/editing/visible_units.h index 18b9665..d734f78 100644 --- a/Source/WebCore/editing/visible_units.h +++ b/Source/WebCore/editing/visible_units.h @@ -42,6 +42,8 @@ VisiblePosition startOfWord(const VisiblePosition &, EWordSide = RightWordIfOnBo VisiblePosition endOfWord(const VisiblePosition &, EWordSide = RightWordIfOnBoundary); VisiblePosition previousWordPosition(const VisiblePosition &); VisiblePosition nextWordPosition(const VisiblePosition &); +VisiblePosition rightWordPosition(const VisiblePosition&); +VisiblePosition leftWordPosition(const VisiblePosition&); // sentences VisiblePosition startOfSentence(const VisiblePosition &); |