diff options
author | Feng Qian <fqian@google.com> | 2009-06-17 12:12:20 -0700 |
---|---|---|
committer | Feng Qian <fqian@google.com> | 2009-06-17 12:12:20 -0700 |
commit | 5f1ab04193ad0130ca8204aadaceae083aca9881 (patch) | |
tree | 5a92cd389e2cfe7fb67197ce14b38469462379f8 /WebCore/editing | |
parent | 194315e5a908cc8ed67d597010544803eef1ac59 (diff) | |
download | external_webkit-5f1ab04193ad0130ca8204aadaceae083aca9881.zip external_webkit-5f1ab04193ad0130ca8204aadaceae083aca9881.tar.gz external_webkit-5f1ab04193ad0130ca8204aadaceae083aca9881.tar.bz2 |
Get WebKit r44544.
Diffstat (limited to 'WebCore/editing')
43 files changed, 1628 insertions, 456 deletions
diff --git a/WebCore/editing/ApplyStyleCommand.cpp b/WebCore/editing/ApplyStyleCommand.cpp index d43cc81..8d0312b 100644 --- a/WebCore/editing/ApplyStyleCommand.cpp +++ b/WebCore/editing/ApplyStyleCommand.cpp @@ -27,6 +27,7 @@ #include "ApplyStyleCommand.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSParser.h" #include "CSSProperty.h" #include "CSSPropertyNames.h" @@ -228,7 +229,11 @@ bool StyleChange::currentlyHasStyle(const Position &pos, const CSSProperty *prop { ASSERT(pos.isNotNull()); RefPtr<CSSComputedStyleDeclaration> style = pos.computedStyle(); - RefPtr<CSSValue> value = style->getPropertyCSSValue(property->id(), DoNotUpdateLayout); + RefPtr<CSSValue> value; + if (property->id() == CSSPropertyFontSize) + value = style->getFontSizeCSSValuePreferringKeyword(); + else + value = style->getPropertyCSSValue(property->id(), DoNotUpdateLayout); if (!value) return false; return equalIgnoringCase(value->cssText(), property->value()->cssText()); @@ -256,7 +261,7 @@ static bool isUnstyledStyleSpan(const Node* node) const HTMLElement* elem = static_cast<const HTMLElement*>(node); CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); - return (!inlineStyleDecl || inlineStyleDecl->length() == 0) && elem->getAttribute(classAttr) == styleSpanClassString(); + return (!inlineStyleDecl || inlineStyleDecl->isEmpty()) && elem->getAttribute(classAttr) == styleSpanClassString(); } static bool isSpanWithoutAttributesOrUnstyleStyleSpan(const Node* node) @@ -265,8 +270,8 @@ static bool isSpanWithoutAttributesOrUnstyleStyleSpan(const Node* node) return false; const HTMLElement* elem = static_cast<const HTMLElement*>(node); - NamedAttrMap* attributes = elem->attributes(true); // readonly - if (attributes->length() == 0) + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (attributes->isEmpty()) return true; return isUnstyledStyleSpan(node); @@ -278,7 +283,7 @@ static bool isEmptyFontTag(const Node *node) return false; const Element *elem = static_cast<const Element *>(node); - NamedAttrMap *map = elem->attributes(true); // true for read-only + NamedNodeMap *map = elem->attributes(true); // true for read-only return (!map || map->length() == 1) && elem->getAttribute(classAttr) == styleSpanClassString(); } @@ -337,7 +342,7 @@ ApplyStyleCommand::ApplyStyleCommand(PassRefPtr<Element> element, bool removeOnl void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd) { - ASSERT(Range::compareBoundaryPoints(newEnd, newStart) >= 0); + ASSERT(comparePositions(newEnd, newStart) >= 0); if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end)) m_useEndingSelection = true; @@ -403,7 +408,7 @@ void ApplyStyleCommand::applyBlockStyle(CSSMutableStyleDeclaration *style) // get positions we want to use for applying style Position start = startPosition(); Position end = endPosition(); - if (Range::compareBoundaryPoints(end, start) < 0) { + if (comparePositions(end, start) < 0) { Position swap = start; start = end; end = swap; @@ -426,7 +431,7 @@ void ApplyStyleCommand::applyBlockStyle(CSSMutableStyleDeclaration *style) VisiblePosition beyondEnd(endOfParagraph(visibleEnd).next()); while (paragraphStart.isNotNull() && paragraphStart != beyondEnd) { StyleChange styleChange(style, paragraphStart.deepEquivalent()); - if (styleChange.cssStyle().length() > 0 || m_removeOnly) { + if (styleChange.cssStyle().length() || m_removeOnly) { RefPtr<Node> block = enclosingBlock(paragraphStart.deepEquivalent().node()); RefPtr<Node> newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent()); if (newBlock) @@ -479,7 +484,7 @@ void ApplyStyleCommand::applyRelativeFontStyleChange(CSSMutableStyleDeclaration Position start = startPosition(); Position end = endPosition(); - if (Range::compareBoundaryPoints(end, start) < 0) { + if (comparePositions(end, start) < 0) { Position swap = start; start = end; end = swap; @@ -520,7 +525,7 @@ void ApplyStyleCommand::applyRelativeFontStyleChange(CSSMutableStyleDeclaration start = start.upstream(); // Move upstream to ensure we do not add redundant spans. Node *startNode = start.node(); - if (startNode->isTextNode() && start.m_offset >= caretMaxOffset(startNode)) // Move out of text node if range does not include its characters. + if (startNode->isTextNode() && start.deprecatedEditingOffset() >= caretMaxOffset(startNode)) // Move out of text node if range does not include its characters. startNode = startNode->traverseNextNode(); // Store away font size before making any changes to the document. @@ -564,7 +569,7 @@ void ApplyStyleCommand::applyRelativeFontStyleChange(CSSMutableStyleDeclaration inlineStyleDecl->setProperty(CSSPropertyFontSize, String::number(desiredFontSize) + "px", false, false); setNodeAttribute(element.get(), styleAttr, inlineStyleDecl->cssText()); } - if (inlineStyleDecl->length() == 0) { + if (inlineStyleDecl->isEmpty()) { removeNodeAttribute(element.get(), styleAttr); // FIXME: should this be isSpanWithoutAttributesOrUnstyleStyleSpan? Need a test. if (isUnstyledStyleSpan(element.get())) @@ -714,7 +719,7 @@ void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) // adjust to the positions we want to use for applying style Position start = startPosition(); Position end = endPosition(); - if (Range::compareBoundaryPoints(end, start) < 0) { + if (comparePositions(end, start) < 0) { Position swap = start; start = end; end = swap; @@ -775,7 +780,7 @@ void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) RefPtr<CSSMutableStyleDeclaration> embeddingStyle = CSSMutableStyleDeclaration::create(); embeddingStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); embeddingStyle->setProperty(CSSPropertyDirection, static_cast<CSSPrimitiveValue*>(direction.get())->getIdent()); - if (Range::compareBoundaryPoints(embeddingRemoveStart, embeddingRemoveEnd) <= 0) + if (comparePositions(embeddingRemoveStart, embeddingRemoveEnd) <= 0) removeInlineStyle(embeddingStyle, embeddingRemoveStart, embeddingRemoveEnd); RefPtr<CSSMutableStyleDeclaration> styleWithoutEmbedding = style->copy(); @@ -868,17 +873,17 @@ void ApplyStyleCommand::applyInlineStyleToRange(CSSMutableStyleDeclaration* styl bool rangeIsEmpty = false; - if (start.m_offset >= caretMaxOffset(start.node())) { + if (start.deprecatedEditingOffset() >= caretMaxOffset(start.node())) { node = node->traverseNextNode(); Position newStart = Position(node, 0); - if (!node || Range::compareBoundaryPoints(end, newStart) < 0) + if (!node || comparePositions(end, newStart) < 0) rangeIsEmpty = true; } if (!rangeIsEmpty) { // pastEndNode is the node after the last fully selected node. Node* pastEndNode = end.node(); - if (end.m_offset >= caretMaxOffset(end.node())) + if (end.deprecatedEditingOffset() >= caretMaxOffset(end.node())) pastEndNode = end.node()->traverseNextSibling(); // FIXME: Callers should perform this operation on a Range that includes the br // if they want style applied to the empty line. @@ -896,7 +901,7 @@ void ApplyStyleCommand::applyInlineStyleToRange(CSSMutableStyleDeclaration* styl // This is a plaintext-only region. Only proceed if it's fully selected. // pastEndNode is the node after the last fully selected node, so if it's inside node then // node isn't fully selected. - if (pastEndNode->isDescendantOf(node)) + if (pastEndNode && pastEndNode->isDescendantOf(node)) break; // Add to this element's inline style and skip over its contents. HTMLElement* element = static_cast<HTMLElement*>(node); @@ -935,11 +940,14 @@ void ApplyStyleCommand::applyInlineStyleToRange(CSSMutableStyleDeclaration* styl // This function maps from styling tags to CSS styles. Used for knowing which // styling tags should be removed when toggling styles. -bool ApplyStyleCommand::isHTMLStyleNode(CSSMutableStyleDeclaration* style, HTMLElement* elem) +bool ApplyStyleCommand::implicitlyStyledElementShouldBeRemovedWhenApplyingStyle(HTMLElement* elem, CSSMutableStyleDeclaration* style) { CSSMutableStyleDeclaration::const_iterator end = style->end(); for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { - switch ((*it).id()) { + const CSSProperty& property = *it; + // FIXME: This should probably be re-written to lookup the tagname in a + // hash and match against an expected property/value pair. + switch (property.id()) { case CSSPropertyFontWeight: // IE inserts "strong" tags for execCommand("bold"), so we remove them, even though they're not strictly presentational if (elem->hasLocalName(bTag) || elem->hasLocalName(strongTag)) @@ -953,20 +961,34 @@ bool ApplyStyleCommand::isHTMLStyleNode(CSSMutableStyleDeclaration* style, HTMLE // IE inserts "em" tags for execCommand("italic"), so we remove them, even though they're not strictly presentational if (elem->hasLocalName(iTag) || elem->hasLocalName(emTag)) return true; + break; } } - return false; } -void ApplyStyleCommand::removeHTMLStyleNode(HTMLElement *elem) +void ApplyStyleCommand::replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*& elem) { - // This node can be removed. - // EDIT FIXME: This does not handle the case where the node - // has attributes. But how often do people add attributes to <B> tags? - // Not so often I think. - ASSERT(elem); - removeNodePreservingChildren(elem); + bool removeNode = false; + + // Similar to isSpanWithoutAttributesOrUnstyleStyleSpan, but does not look for Apple-style-span. + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (!attributes || attributes->isEmpty()) + removeNode = true; + else if (attributes->length() == 1 && elem->hasAttribute(styleAttr)) { + // Remove the element even if it has just style='' (this might be redundantly checked later too) + CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); + if (!inlineStyleDecl || inlineStyleDecl->isEmpty()) + removeNode = true; + } + + if (removeNode) + removeNodePreservingChildren(elem); + else { + HTMLElement* newSpanElement = replaceNodeWithSpanPreservingChildrenAndAttributes(elem); + ASSERT(newSpanElement && newSpanElement->inDocument()); + elem = newSpanElement; + } } void ApplyStyleCommand::removeHTMLFontStyle(CSSMutableStyleDeclaration *style, HTMLElement *elem) @@ -1035,7 +1057,7 @@ void ApplyStyleCommand::removeCSSStyle(CSSMutableStyleDeclaration* style, HTMLEl } // No need to serialize <foo style=""> if we just removed the last css property - if (decl->length() == 0) + if (decl->isEmpty()) removeNodeAttribute(elem, styleAttr); if (isSpanWithoutAttributesOrUnstyleStyleSpan(elem)) @@ -1116,7 +1138,7 @@ void ApplyStyleCommand::applyTextDecorationStyle(Node *node, CSSMutableStyleDecl { ASSERT(node); - if (!style || !style->cssText().length()) + if (!style || style->cssText().isEmpty()) return; if (node->isTextNode()) { @@ -1131,7 +1153,7 @@ void ApplyStyleCommand::applyTextDecorationStyle(Node *node, CSSMutableStyleDecl HTMLElement *element = static_cast<HTMLElement *>(node); StyleChange styleChange(style, Position(element, 0)); - if (styleChange.cssStyle().length() > 0) { + if (styleChange.cssStyle().length()) { String cssText = styleChange.cssStyle(); CSSMutableStyleDeclaration *decl = element->inlineStyleDecl(); if (decl) @@ -1203,7 +1225,7 @@ void ApplyStyleCommand::removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration> ASSERT(end.isNotNull()); ASSERT(start.node()->inDocument()); ASSERT(end.node()->inDocument()); - ASSERT(Range::compareBoundaryPoints(start, end) <= 0); + ASSERT(comparePositions(start, end) <= 0); RefPtr<CSSValue> textDecorationSpecialProperty = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); @@ -1228,9 +1250,13 @@ void ApplyStyleCommand::removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration> Node* next = elem->traverseNextNode(); if (m_styledInlineElement && elem->hasTagName(m_styledInlineElement->tagQName())) removeNodePreservingChildren(elem); - if (isHTMLStyleNode(style.get(), elem)) - removeHTMLStyleNode(elem); - else { + + if (implicitlyStyledElementShouldBeRemovedWhenApplyingStyle(elem, style.get())) + replaceWithSpanOrRemoveIfWithoutAttributes(elem); + + // If the node was converted to a span, the span may still contain relevant + // styles which must be removed (e.g. <b style='font-weight: bold'>) + if (elem->inDocument()) { removeHTMLFontStyle(style.get(), elem); removeHTMLBidiEmbeddingStyle(style.get(), elem); removeCSSStyle(style.get(), elem); @@ -1239,14 +1265,14 @@ void ApplyStyleCommand::removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration> if (s.node() == elem) { // Since elem must have been fully selected, and it is at the start // of the selection, it is clear we can set the new s offset to 0. - ASSERT(s.m_offset <= caretMinOffset(s.node())); + ASSERT(s.deprecatedEditingOffset() <= caretMinOffset(s.node())); s = Position(next, 0); } if (e.node() == elem) { // Since elem must have been fully selected, and it is at the end // of the selection, it is clear we can set the new e offset to // the max range offset of prev. - ASSERT(e.m_offset >= maxRangeOffset(e.node())); + ASSERT(e.deprecatedEditingOffset() >= maxRangeOffset(e.node())); e = Position(prev, maxRangeOffset(prev)); } } @@ -1267,8 +1293,7 @@ bool ApplyStyleCommand::nodeFullySelected(Node *node, const Position &start, con ASSERT(node->isElementNode()); Position pos = Position(node, node->childNodeCount()).upstream(); - return Range::compareBoundaryPoints(node, 0, start.node(), start.m_offset) >= 0 && - Range::compareBoundaryPoints(pos, end) <= 0; + return comparePositions(Position(node, 0), start) >= 0 && comparePositions(pos, end) <= 0; } bool ApplyStyleCommand::nodeFullyUnselected(Node *node, const Position &start, const Position &end) const @@ -1277,8 +1302,8 @@ bool ApplyStyleCommand::nodeFullyUnselected(Node *node, const Position &start, c ASSERT(node->isElementNode()); Position pos = Position(node, node->childNodeCount()).upstream(); - bool isFullyBeforeStart = Range::compareBoundaryPoints(pos, start) < 0; - bool isFullyAfterEnd = Range::compareBoundaryPoints(node, 0, end.node(), end.m_offset) > 0; + bool isFullyBeforeStart = comparePositions(pos, start) < 0; + bool isFullyAfterEnd = comparePositions(Position(node, 0), end) > 0; return isFullyBeforeStart || isFullyAfterEnd; } @@ -1286,11 +1311,11 @@ bool ApplyStyleCommand::nodeFullyUnselected(Node *node, const Position &start, c bool ApplyStyleCommand::splitTextAtStartIfNeeded(const Position &start, const Position &end) { - if (start.node()->isTextNode() && start.m_offset > caretMinOffset(start.node()) && start.m_offset < caretMaxOffset(start.node())) { - int endOffsetAdjustment = start.node() == end.node() ? start.m_offset : 0; + if (start.node()->isTextNode() && start.deprecatedEditingOffset() > caretMinOffset(start.node()) && start.deprecatedEditingOffset() < caretMaxOffset(start.node())) { + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; Text *text = static_cast<Text *>(start.node()); - splitTextNode(text, start.m_offset); - updateStartEnd(Position(start.node(), 0), Position(end.node(), end.m_offset - endOffsetAdjustment)); + splitTextNode(text, start.deprecatedEditingOffset()); + updateStartEnd(Position(start.node(), 0), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); return true; } return false; @@ -1298,15 +1323,15 @@ bool ApplyStyleCommand::splitTextAtStartIfNeeded(const Position &start, const Po bool ApplyStyleCommand::splitTextAtEndIfNeeded(const Position &start, const Position &end) { - if (end.node()->isTextNode() && end.m_offset > caretMinOffset(end.node()) && end.m_offset < caretMaxOffset(end.node())) { + if (end.node()->isTextNode() && end.deprecatedEditingOffset() > caretMinOffset(end.node()) && end.deprecatedEditingOffset() < caretMaxOffset(end.node())) { Text *text = static_cast<Text *>(end.node()); - splitTextNode(text, end.m_offset); + splitTextNode(text, end.deprecatedEditingOffset()); Node *prevNode = text->previousSibling(); ASSERT(prevNode); Node *startNode = start.node() == end.node() ? prevNode : start.node(); ASSERT(startNode); - updateStartEnd(Position(startNode, start.m_offset), Position(prevNode, caretMaxOffset(prevNode))); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode, caretMaxOffset(prevNode))); return true; } return false; @@ -1314,12 +1339,12 @@ bool ApplyStyleCommand::splitTextAtEndIfNeeded(const Position &start, const Posi bool ApplyStyleCommand::splitTextElementAtStartIfNeeded(const Position &start, const Position &end) { - if (start.node()->isTextNode() && start.m_offset > caretMinOffset(start.node()) && start.m_offset < caretMaxOffset(start.node())) { - int endOffsetAdjustment = start.node() == end.node() ? start.m_offset : 0; + if (start.node()->isTextNode() && start.deprecatedEditingOffset() > caretMinOffset(start.node()) && start.deprecatedEditingOffset() < caretMaxOffset(start.node())) { + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; Text *text = static_cast<Text *>(start.node()); - splitTextNodeContainingElement(text, start.m_offset); + splitTextNodeContainingElement(text, start.deprecatedEditingOffset()); - updateStartEnd(Position(start.node()->parentNode(), start.node()->nodeIndex()), Position(end.node(), end.m_offset - endOffsetAdjustment)); + updateStartEnd(Position(start.node()->parentNode(), start.node()->nodeIndex()), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); return true; } return false; @@ -1327,15 +1352,15 @@ bool ApplyStyleCommand::splitTextElementAtStartIfNeeded(const Position &start, c bool ApplyStyleCommand::splitTextElementAtEndIfNeeded(const Position &start, const Position &end) { - if (end.node()->isTextNode() && end.m_offset > caretMinOffset(end.node()) && end.m_offset < caretMaxOffset(end.node())) { + if (end.node()->isTextNode() && end.deprecatedEditingOffset() > caretMinOffset(end.node()) && end.deprecatedEditingOffset() < caretMaxOffset(end.node())) { Text *text = static_cast<Text *>(end.node()); - splitTextNodeContainingElement(text, end.m_offset); + splitTextNodeContainingElement(text, end.deprecatedEditingOffset()); Node *prevNode = text->parent()->previousSibling()->lastChild(); ASSERT(prevNode); Node *startNode = start.node() == end.node() ? prevNode : start.node(); ASSERT(startNode); - updateStartEnd(Position(startNode, start.m_offset), Position(prevNode->parent(), prevNode->nodeIndex() + 1)); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode->parent(), prevNode->nodeIndex() + 1)); return true; } return false; @@ -1357,8 +1382,8 @@ static bool areIdenticalElements(Node *first, Node *second) if (!firstElement->tagQName().matches(secondElement->tagQName())) return false; - NamedAttrMap *firstMap = firstElement->attributes(); - NamedAttrMap *secondMap = secondElement->attributes(); + NamedNodeMap *firstMap = firstElement->attributes(); + NamedNodeMap *secondMap = secondElement->attributes(); unsigned firstLength = firstMap->length(); @@ -1379,10 +1404,10 @@ static bool areIdenticalElements(Node *first, Node *second) bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position &start, const Position &end) { Node *startNode = start.node(); - int startOffset = start.m_offset; + int startOffset = start.deprecatedEditingOffset(); if (isAtomicNode(start.node())) { - if (start.m_offset != 0) + if (start.deprecatedEditingOffset() != 0) return false; // note: prior siblings could be unrendered elements. it's silly to miss the @@ -1411,7 +1436,7 @@ bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position &start, int startOffsetAdjustment = startChild->nodeIndex(); int endOffsetAdjustment = startNode == end.node() ? startOffsetAdjustment : 0; - updateStartEnd(Position(startNode, startOffsetAdjustment), Position(end.node(), end.m_offset + endOffsetAdjustment)); + updateStartEnd(Position(startNode, startOffsetAdjustment), Position(end.node(), end.deprecatedEditingOffset() + endOffsetAdjustment)); return true; } @@ -1421,7 +1446,7 @@ bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position &start, bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position &start, const Position &end) { Node *endNode = end.node(); - int endOffset = end.m_offset; + int endOffset = end.deprecatedEditingOffset(); if (isAtomicNode(endNode)) { if (endOffset < caretMaxOffset(endNode)) @@ -1451,7 +1476,7 @@ bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position &start, const ASSERT(startNode); int endOffset = nextChild ? nextChild->nodeIndex() : nextElement->childNodes()->length(); - updateStartEnd(Position(startNode, start.m_offset), Position(nextElement, endOffset)); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(nextElement, endOffset)); return true; } @@ -1554,7 +1579,7 @@ void ApplyStyleCommand::addInlineStyleIfNeeded(CSSMutableStyleDeclaration *style } } - if (styleChange.cssStyle().length() > 0) { + if (styleChange.cssStyle().length()) { RefPtr<Element> styleElement = createStyleSpanElement(document()); styleElement->setAttribute(styleAttr, styleChange.cssStyle()); surroundNodeRangeWithElement(startNode, endNode, styleElement.release()); @@ -1607,9 +1632,9 @@ void ApplyStyleCommand::joinChildTextNodes(Node *node, const Position &start, co Text *childText = static_cast<Text *>(child); Text *nextText = static_cast<Text *>(next); if (next == start.node()) - newStart = Position(childText, childText->length() + start.m_offset); + newStart = Position(childText, childText->length() + start.deprecatedEditingOffset()); if (next == end.node()) - newEnd = Position(childText, childText->length() + end.m_offset); + newEnd = Position(childText, childText->length() + end.deprecatedEditingOffset()); String textToMove = nextText->data(); insertTextIntoNode(childText, childText->length(), textToMove); removeNode(next); diff --git a/WebCore/editing/ApplyStyleCommand.h b/WebCore/editing/ApplyStyleCommand.h index a42225d..74fe605 100644 --- a/WebCore/editing/ApplyStyleCommand.h +++ b/WebCore/editing/ApplyStyleCommand.h @@ -62,8 +62,8 @@ private: CSSMutableStyleDeclaration* style() const { return m_style.get(); } // style-removal helpers - bool isHTMLStyleNode(CSSMutableStyleDeclaration*, HTMLElement*); - void removeHTMLStyleNode(HTMLElement*); + bool implicitlyStyledElementShouldBeRemovedWhenApplyingStyle(HTMLElement*, CSSMutableStyleDeclaration*); + void replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*&); void removeHTMLFontStyle(CSSMutableStyleDeclaration*, HTMLElement*); void removeHTMLBidiEmbeddingStyle(CSSMutableStyleDeclaration*, HTMLElement*); void removeCSSStyle(CSSMutableStyleDeclaration*, HTMLElement*); diff --git a/WebCore/editing/BreakBlockquoteCommand.cpp b/WebCore/editing/BreakBlockquoteCommand.cpp index 2a513a5..e3d66ba 100644 --- a/WebCore/editing/BreakBlockquoteCommand.cpp +++ b/WebCore/editing/BreakBlockquoteCommand.cpp @@ -26,12 +26,12 @@ #include "config.h" #include "BreakBlockquoteCommand.h" -#include "Element.h" +#include "HTMLElement.h" #include "HTMLNames.h" +#include "RenderListItem.h" #include "Text.h" #include "VisiblePosition.h" #include "htmlediting.h" -#include "RenderListItem.h" namespace WebCore { @@ -56,42 +56,59 @@ void BreakBlockquoteCommand::doApply() // be in the first node that we need to move (there are a few exceptions to this, see below). Position pos = endingSelection().start().downstream(); - // startNode is the first node that we need to move to the new blockquote. - Node* startNode = pos.node(); // Find the top-most blockquote from the start. Element* topBlockquote = 0; - for (Node *node = startNode->parentNode(); node; node = node->parentNode()) { + for (Node *node = pos.node()->parentNode(); node; node = node->parentNode()) { if (isMailBlockquote(node)) topBlockquote = static_cast<Element*>(node); } if (!topBlockquote || !topBlockquote->parentNode()) return; - // Insert a break after the top blockquote. RefPtr<Element> breakNode = createBreakElement(document()); + + // If the position is at the beginning of the top quoted content, we don't need to break the quote. + // Instead, insert the break before the blockquote. + if (isFirstVisiblePositionInNode(visiblePos, topBlockquote)) { + insertNodeBefore(breakNode.get(), topBlockquote); + setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); + rebalanceWhitespace(); + return; + } + + // Insert a break after the top blockquote. insertNodeAfter(breakNode.get(), topBlockquote); + // If we're inserting the break at the end of the quoted content, we don't need to break the quote. if (isLastVisiblePositionInNode(visiblePos, topBlockquote)) { setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); - rebalanceWhitespace(); + rebalanceWhitespace(); return; } // Don't move a line break just after the caret. Doing so would create an extra, empty paragraph // in the new blockquote. - if (lineBreakExistsAtPosition(visiblePos)) + if (lineBreakExistsAtVisiblePosition(visiblePos)) pos = pos.next(); + // Adjust the position so we don't split at the beginning of a quote. + while (isFirstVisiblePositionInNode(VisiblePosition(pos), nearestMailBlockquote(pos.node()))) + pos = pos.previous(); + + // startNode is the first node that we need to move to the new blockquote. + Node* startNode = pos.node(); + // Split at pos if in the middle of a text node. if (startNode->isTextNode()) { Text* textNode = static_cast<Text*>(startNode); - if ((unsigned)pos.m_offset >= textNode->length()) { + if ((unsigned)pos.deprecatedEditingOffset() >= textNode->length()) { startNode = startNode->traverseNextNode(); ASSERT(startNode); - } else if (pos.m_offset > 0) - splitTextNode(textNode, pos.m_offset); - } else if (pos.m_offset > 0) { - startNode = startNode->traverseNextNode(); + } else if (pos.deprecatedEditingOffset() > 0) + splitTextNode(textNode, pos.deprecatedEditingOffset()); + } else if (pos.deprecatedEditingOffset() > 0) { + Node* childAtOffset = startNode->childNode(pos.deprecatedEditingOffset()); + startNode = childAtOffset ? childAtOffset : startNode->traverseNextNode(); ASSERT(startNode); } @@ -141,10 +158,7 @@ void BreakBlockquoteCommand::doApply() moveNode = next; } - // Hold open startNode's original parent if we emptied it if (!ancestors.isEmpty()) { - addBlockPlaceholderIfNeeded(ancestors.first()); - // Split the tree up the ancestor chain until the topBlockquote // Throughout this loop, clonedParent is the clone of ancestor's parent. // This is so we can clone ancestor's siblings and place the clones @@ -162,6 +176,11 @@ void BreakBlockquoteCommand::doApply() moveNode = next; } } + + // If the startNode's original parent is now empty, remove it + Node* originalParent = ancestors.first(); + if (!originalParent->hasChildNodes()) + removeNode(originalParent); } // Make sure the cloned block quote renders. diff --git a/WebCore/editing/CompositeEditCommand.cpp b/WebCore/editing/CompositeEditCommand.cpp index 9052582..89a0f8a 100644 --- a/WebCore/editing/CompositeEditCommand.cpp +++ b/WebCore/editing/CompositeEditCommand.cpp @@ -36,7 +36,7 @@ #include "Document.h" #include "DocumentFragment.h" #include "EditorInsertAction.h" -#include "Element.h" +#include "HTMLElement.h" #include "HTMLNames.h" #include "InlineTextBox.h" #include "InsertIntoTextNodeCommand.h" @@ -50,6 +50,7 @@ #include "RemoveCSSPropertyCommand.h" #include "RemoveNodeCommand.h" #include "RemoveNodePreservingChildrenCommand.h" +#include "ReplaceNodeWithSpanCommand.h" #include "ReplaceSelectionCommand.h" #include "RenderBlock.h" #include "RenderText.h" @@ -157,7 +158,7 @@ void CompositeEditCommand::insertNodeAt(PassRefPtr<Node> insertChild, const Posi // likewise for replaced elements, brs, etc. Position p = rangeCompliantEquivalent(editingPosition); Node* refChild = p.node(); - int offset = p.m_offset; + int offset = p.deprecatedEditingOffset(); if (canHaveChildrenForEditing(refChild)) { Node* child = refChild->firstChild(); @@ -215,6 +216,20 @@ void CompositeEditCommand::removeNodeAndPruneAncestors(PassRefPtr<Node> node) prune(parent.release()); } +HTMLElement* CompositeEditCommand::replaceNodeWithSpanPreservingChildrenAndAttributes(PassRefPtr<Node> node) +{ + // It would also be possible to implement all of ReplaceNodeWithSpanCommand + // as a series of existing smaller edit commands. Someone who wanted to + // reduce the number of edit commands could do so here. + RefPtr<ReplaceNodeWithSpanCommand> command = ReplaceNodeWithSpanCommand::create(node); + applyCommandToComposite(command); + // Returning a raw pointer here is OK because the command is retained by + // applyCommandToComposite (thus retaining the span), and the span is also + // in the DOM tree, and thus alive whie it has a parent. + ASSERT(command->spanElement()->inDocument()); + return command->spanElement(); +} + static bool hasARenderedDescendant(Node* node) { Node* n = node->firstChild(); @@ -327,13 +342,13 @@ Position CompositeEditCommand::positionOutsideTabSpan(const Position& pos) Node* tabSpan = tabSpanNode(pos.node()); - if (pos.m_offset <= caretMinOffset(pos.node())) + if (pos.deprecatedEditingOffset() <= caretMinOffset(pos.node())) return positionBeforeNode(tabSpan); - if (pos.m_offset >= caretMaxOffset(pos.node())) + if (pos.deprecatedEditingOffset() >= caretMaxOffset(pos.node())) return positionAfterNode(tabSpan); - splitTextNodeContainingElement(static_cast<Text *>(pos.node()), pos.m_offset); + splitTextNodeContainingElement(static_cast<Text *>(pos.node()), pos.deprecatedEditingOffset()); return positionBeforeNode(tabSpan); } @@ -392,7 +407,7 @@ void CompositeEditCommand::rebalanceWhitespaceAt(const Position& position) String text = textNode->data(); ASSERT(!text.isEmpty()); - int offset = position.m_offset; + int offset = position.deprecatedEditingOffset(); // If neither text[offset] nor text[offset - 1] are some form of whitespace, do nothing. if (!isWhitespace(text[offset])) { offset--; @@ -449,9 +464,9 @@ void CompositeEditCommand::prepareWhitespaceAtPositionForSplit(Position& positio Position previous(previousVisiblePos.deepEquivalent()); if (isCollapsibleWhitespace(previousVisiblePos.characterAfter()) && previous.node()->isTextNode() && !previous.node()->hasTagName(brTag)) - replaceTextInNode(static_cast<Text*>(previous.node()), previous.m_offset, 1, nonBreakingSpaceString()); + replaceTextInNode(static_cast<Text*>(previous.node()), previous.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); if (isCollapsibleWhitespace(visiblePos.characterAfter()) && position.node()->isTextNode() && !position.node()->hasTagName(brTag)) - replaceTextInNode(static_cast<Text*>(position.node()), position.m_offset, 1, nonBreakingSpaceString()); + replaceTextInNode(static_cast<Text*>(position.node()), position.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); } void CompositeEditCommand::rebalanceWhitespace() @@ -534,7 +549,7 @@ void CompositeEditCommand::deleteInsignificantText(const Position& start, const if (start.isNull() || end.isNull()) return; - if (Range::compareBoundaryPoints(start, end) >= 0) + if (comparePositions(start, end) >= 0) return; Node* next; @@ -542,8 +557,8 @@ void CompositeEditCommand::deleteInsignificantText(const Position& start, const next = node->traverseNextNode(); if (node->isTextNode()) { Text* textNode = static_cast<Text*>(node); - int startOffset = node == start.node() ? start.m_offset : 0; - int endOffset = node == end.node() ? end.m_offset : textNode->length(); + int startOffset = node == start.node() ? start.deprecatedEditingOffset() : 0; + int endOffset = node == end.node() ? end.deprecatedEditingOffset() : textNode->length(); deleteInsignificantText(textNode, startOffset, endOffset); } if (node == end.node()) @@ -603,24 +618,18 @@ PassRefPtr<Node> CompositeEditCommand::addBlockPlaceholderIfNeeded(Element* cont return 0; } -// Removes '\n's and brs that will collapse when content is inserted just before them. -// FIXME: We shouldn't really have to remove placeholders, but removing them is a workaround for 9661. -void CompositeEditCommand::removePlaceholderAt(const VisiblePosition& visiblePosition) +// Assumes that the position is at a placeholder and does the removal without much checking. +void CompositeEditCommand::removePlaceholderAt(const Position& p) { - if (visiblePosition.isNull()) + ASSERT(lineBreakExistsAtPosition(p)); + + // We are certain that the position is at a line break, but it may be a br or a preserved newline. + if (p.anchorNode()->hasTagName(brTag)) { + removeNode(p.anchorNode()); return; - - Position p = visiblePosition.deepEquivalent().downstream(); - // If a br or '\n' is at the end of a block and not at the start of a paragraph, - // then it is superfluous, so adding content before a br or '\n' that is at - // the start of a paragraph will render it superfluous. - // FIXME: This doesn't remove placeholders at the end of anonymous blocks. - if (isEndOfBlock(visiblePosition) && isStartOfParagraph(visiblePosition)) { - if (p.node()->hasTagName(brTag) && p.m_offset == 0) - removeNode(p.node()); - else if (lineBreakExistsAtPosition(visiblePosition)) - deleteTextFromNode(static_cast<Text*>(p.node()), p.m_offset, 1); } + + deleteTextFromNode(static_cast<Text*>(p.anchorNode()), p.offsetInContainerNode(), 1); } PassRefPtr<Node> CompositeEditCommand::insertNewDefaultParagraphElementAt(const Position& position) @@ -654,7 +663,7 @@ PassRefPtr<Node> CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessar // If there are no VisiblePositions in the same block as pos then // upstreamStart will be outside the paragraph - if (Range::compareBoundaryPoints(pos, upstreamStart) < 0) + if (comparePositions(pos, upstreamStart) < 0) return 0; // Perform some checks to see if we need to perform work in this function. @@ -662,9 +671,9 @@ PassRefPtr<Node> CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessar // If the block is the root editable element, always move content to a new block, // since it is illegal to modify attributes on the root editable element for editing. if (upstreamStart.node() == editableRootForPosition(upstreamStart)) { - // If the block is the root editable element and there is nothing insde of it, create a new - // block but don't try and move content into it, since there's nothing to move. - if (upstreamStart == upstreamEnd) + // If the block is the root editable element and it contains no visible content, create a new + // block but don't try and move content into it, since there's nothing for moveParagraphs to move. + if (!Position::hasRenderedNonAnonymousDescendantsWithHeight(upstreamStart.node()->renderer())) return insertNewDefaultParagraphElementAt(upstreamStart); } else if (isBlock(upstreamEnd.node())) { if (!upstreamEnd.node()->isDescendantOf(upstreamStart.node())) { @@ -750,12 +759,12 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap VisiblePosition visibleStart = endingSelection().visibleStart(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); - bool startAfterParagraph = Range::compareBoundaryPoints(visibleStart.deepEquivalent(), endOfParagraphToMove.deepEquivalent()) > 0; - bool endBeforeParagraph = Range::compareBoundaryPoints(visibleEnd.deepEquivalent(), startOfParagraphToMove.deepEquivalent()) < 0; + bool startAfterParagraph = comparePositions(visibleStart, endOfParagraphToMove) > 0; + bool endBeforeParagraph = comparePositions(visibleEnd, startOfParagraphToMove) < 0; if (!startAfterParagraph && !endBeforeParagraph) { - bool startInParagraph = Range::compareBoundaryPoints(visibleStart.deepEquivalent(), startOfParagraphToMove.deepEquivalent()) >= 0; - bool endInParagraph = Range::compareBoundaryPoints(visibleEnd.deepEquivalent(), endOfParagraphToMove.deepEquivalent()) <= 0; + bool startInParagraph = comparePositions(visibleStart, startOfParagraphToMove) >= 0; + bool endInParagraph = comparePositions(visibleEnd, endOfParagraphToMove) <= 0; startIndex = 0; if (startInParagraph) { @@ -782,7 +791,7 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap // start and end can't be used directly to create a Range; they are "editing positions" Position startRangeCompliant = rangeCompliantEquivalent(start); Position endRangeCompliant = rangeCompliantEquivalent(end); - RefPtr<Range> range = Range::create(document(), startRangeCompliant.node(), startRangeCompliant.m_offset, endRangeCompliant.node(), endRangeCompliant.m_offset); + RefPtr<Range> range = Range::create(document(), startRangeCompliant.node(), startRangeCompliant.deprecatedEditingOffset(), endRangeCompliant.node(), endRangeCompliant.deprecatedEditingOffset()); // FIXME: This is an inefficient way to preserve style on nodes in the paragraph to move. It // shouldn't matter though, since moved paragraphs will usually be quite small. @@ -825,13 +834,13 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap // expects this behavior). else if (isBlock(node)) removeNodeAndPruneAncestors(node); - else if (lineBreakExistsAtPosition(caretAfterDelete)) { + else if (lineBreakExistsAtVisiblePosition(caretAfterDelete)) { // There is a preserved '\n' at caretAfterDelete. Text* textNode = static_cast<Text*>(node); if (textNode->length() == 1) removeNodeAndPruneAncestors(node); else - deleteTextFromNode(textNode, position.m_offset, 1); + deleteTextFromNode(textNode, position.deprecatedEditingOffset(), 1); } } @@ -856,8 +865,10 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap setEndingSelection(destination); applyCommandToComposite(ReplaceSelectionCommand::create(document(), fragment, true, false, !preserveStyle, false, true)); - // Restore styles from an empty paragraph to the new empty paragraph. - if (styleInEmptyParagraph) + + // If the selection is in an empty paragraph, restore styles from the old empty paragraph to the new empty paragraph. + bool selectionIsEmptyParagraph = endingSelection().isCaret() && isStartOfParagraph(endingSelection().visibleStart()) && isEndOfParagraph(endingSelection().visibleStart()); + if (styleInEmptyParagraph && selectionIsEmptyParagraph) applyStyle(styleInEmptyParagraph.get()); if (preserveSelection && startIndex != -1) { @@ -941,7 +952,7 @@ bool CompositeEditCommand::breakOutOfEmptyMailBlockquotedParagraph() setEndingSelection(VisibleSelection(atBR)); // If this is an empty paragraph there must be a line break here. - if (!lineBreakExistsAtPosition(caret)) + if (!lineBreakExistsAtVisiblePosition(caret)) return false; Position caretPos(caret.deepEquivalent()); @@ -953,7 +964,7 @@ bool CompositeEditCommand::breakOutOfEmptyMailBlockquotedParagraph() removeNode(caretPos.node()); prune(beforeBR.node()); } else { - ASSERT(caretPos.m_offset == 0); + ASSERT(caretPos.deprecatedEditingOffset() == 0); Text* textNode = static_cast<Text*>(caretPos.node()); Node* parentNode = textNode->parentNode(); // The preserved newline must be the first thing in the node, since otherwise the previous @@ -995,7 +1006,7 @@ Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Posi // Don't insert outside an anchor if doing so would skip over a line break. It would // probably be safe to move the line break so that we could still avoid the anchor here. Position downstream(visiblePos.deepEquivalent().downstream()); - if (lineBreakExistsAtPosition(visiblePos) && downstream.node()->isDescendantOf(enclosingAnchor)) + if (lineBreakExistsAtVisiblePosition(visiblePos) && downstream.node()->isDescendantOf(enclosingAnchor)) return original; result = positionAfterNode(enclosingAnchor); @@ -1030,8 +1041,10 @@ PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, b if (positionInParent != positionInNode) applyCommandToComposite(SplitElementCommand::create(static_cast<Element*>(node->parent()), node)); } - if (splitAncestor) - return splitTreeToNode(end, end->parent()); + if (splitAncestor) { + splitElement(static_cast<Element*>(end), node); + return node->parent(); + } return node.release(); } diff --git a/WebCore/editing/CompositeEditCommand.h b/WebCore/editing/CompositeEditCommand.h index 4a3defd..2c6403e 100644 --- a/WebCore/editing/CompositeEditCommand.h +++ b/WebCore/editing/CompositeEditCommand.h @@ -33,6 +33,7 @@ namespace WebCore { class CSSStyleDeclaration; +class HTMLElement; class Text; class CompositeEditCommand : public EditCommand { @@ -71,6 +72,7 @@ protected: void removeNodeAttribute(PassRefPtr<Element>, const QualifiedName& attribute); void removeChildrenInRange(PassRefPtr<Node>, unsigned from, unsigned to); virtual void removeNode(PassRefPtr<Node>); + HTMLElement* replaceNodeWithSpanPreservingChildrenAndAttributes(PassRefPtr<Node>); void removeNodePreservingChildren(PassRefPtr<Node>); void removeNodeAndPruneAncestors(PassRefPtr<Node>); void prune(PassRefPtr<Node>); @@ -89,7 +91,7 @@ protected: PassRefPtr<Node> appendBlockPlaceholder(PassRefPtr<Element>); PassRefPtr<Node> insertBlockPlaceholder(const Position&); PassRefPtr<Node> addBlockPlaceholderIfNeeded(Element*); - void removePlaceholderAt(const VisiblePosition&); + void removePlaceholderAt(const Position&); PassRefPtr<Node> insertNewDefaultParagraphElementAt(const Position&); diff --git a/WebCore/editing/DeleteButtonController.cpp b/WebCore/editing/DeleteButtonController.cpp index c0775e3..725c01d 100644 --- a/WebCore/editing/DeleteButtonController.cpp +++ b/WebCore/editing/DeleteButtonController.cpp @@ -66,36 +66,75 @@ static bool isDeletableElement(const Node* node) if (!node || !node->isHTMLElement() || !node->inDocument() || !node->isContentEditable()) return false; - const int minimumWidth = 25; - const int minimumHeight = 25; - const unsigned minimumVisibleBorders = 3; + // In general we want to only draw the UI arround object of a certain area, but we still keep the min width/height to + // make sure we don't end up with very thin or very short elements getting the UI. + const int minimumArea = 2500; + const int minimumWidth = 48; + const int minimumHeight = 16; + const unsigned minimumVisibleBorders = 1; RenderObject* renderer = node->renderer(); if (!renderer || !renderer->isBox()) return false; + // Disallow the body element since it isn't practical to delete, and the deletion UI would be clipped. + if (node->hasTagName(bodyTag)) + return false; + + // Disallow elements with any overflow clip, since the deletion UI would be clipped as well. <rdar://problem/6840161> + if (renderer->hasOverflowClip()) + return false; + + // Disallow Mail blockquotes since the deletion UI would get in the way of editing for these. + if (isMailBlockquote(node)) + return false; + RenderBox* box = toRenderBox(renderer); IntRect borderBoundingBox = box->borderBoundingBox(); if (borderBoundingBox.width() < minimumWidth || borderBoundingBox.height() < minimumHeight) return false; + if ((borderBoundingBox.width() * borderBoundingBox.height()) < minimumArea) + return false; + if (renderer->isTable()) return true; - if (node->hasTagName(ulTag) || node->hasTagName(olTag)) + if (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(iframeTag)) return true; if (renderer->isPositioned()) return true; - // allow block elements (excluding table cells) that have some non-transparent borders if (renderer->isRenderBlock() && !renderer->isTableCell()) { RenderStyle* style = renderer->style(); - if (style && style->hasBorder()) { - unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible(); - if (visibleBorders >= minimumVisibleBorders) - return true; - } + if (!style) + return false; + + // Allow blocks that have background images + if (style->hasBackgroundImage() && style->backgroundImage()->canRender(1.0f)) + 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(); + if (visibleBorders >= minimumVisibleBorders) + return true; + + // Allow blocks that have a different background from it's parent + Node* parentNode = node->parentNode(); + if (!parentNode) + return false; + + RenderObject* parentRenderer = parentNode->renderer(); + if (!parentRenderer) + return false; + + RenderStyle* parentStyle = parentRenderer->style(); + if (!parentStyle) + return false; + + if (style->hasBackground() && (!parentStyle->hasBackground() || style->backgroundColor() != parentStyle->backgroundColor())) + return true; } return false; @@ -288,7 +327,7 @@ void DeleteButtonController::enable() // Determining if the element is deletable currently depends on style // because whether something is editable depends on style, so we need // to recalculate style before calling enclosingDeletableElement. - m_frame->document()->updateRendering(); + m_frame->document()->updateStyleIfNeeded(); show(enclosingDeletableElement(m_frame->selection()->selection())); } } diff --git a/WebCore/editing/DeleteSelectionCommand.cpp b/WebCore/editing/DeleteSelectionCommand.cpp index 09288ee..5a0d8fc 100644 --- a/WebCore/editing/DeleteSelectionCommand.cpp +++ b/WebCore/editing/DeleteSelectionCommand.cpp @@ -26,6 +26,7 @@ #include "config.h" #include "DeleteSelectionCommand.h" +#include "CSSMutableStyleDeclaration.h" #include "Document.h" #include "DocumentFragment.h" #include "Editor.h" @@ -79,6 +80,7 @@ DeleteSelectionCommand::DeleteSelectionCommand(Document *document, bool smartDel m_replace(replace), m_expandForSpecialElements(expandForSpecialElements), m_pruneStartBlockIfNecessary(false), + m_startsAtEmptyLine(false), m_startBlock(0), m_endBlock(0), m_typingStyle(0), @@ -94,6 +96,7 @@ DeleteSelectionCommand::DeleteSelectionCommand(const VisibleSelection& selection m_replace(replace), m_expandForSpecialElements(expandForSpecialElements), m_pruneStartBlockIfNecessary(false), + m_startsAtEmptyLine(false), m_selectionToDelete(selection), m_startBlock(0), m_endBlock(0), @@ -135,11 +138,12 @@ void DeleteSelectionCommand::initializeStartEnd(Position& start, Position& end) break; // If we're going to expand to include the startSpecialContainer, it must be fully selected. - if (startSpecialContainer && !endSpecialContainer && Range::compareBoundaryPoints(positionAfterNode(startSpecialContainer), end) > -1) + + if (startSpecialContainer && !endSpecialContainer && comparePositions(positionAfterNode(startSpecialContainer), end) > -1) break; - // If we're going to expand to include the endSpecialContainer, it must be fully selected. - if (endSpecialContainer && !startSpecialContainer && Range::compareBoundaryPoints(start, positionBeforeNode(endSpecialContainer)) > -1) + // If we're going to expand to include the endSpecialContainer, it must be fully selected. + if (endSpecialContainer && !startSpecialContainer && comparePositions(start, positionBeforeNode(endSpecialContainer)) > -1) break; if (startSpecialContainer && startSpecialContainer->isDescendantOf(endSpecialContainer)) @@ -195,7 +199,11 @@ void DeleteSelectionCommand::initializePositionData() // selections that contain a whole number paragraphs plus a line break, since it is unclear to most users // that such a selection actually ends at the start of the next paragraph. This matches TextEdit behavior // for indented paragraphs. - if (numEnclosingMailBlockquotes(start) != numEnclosingMailBlockquotes(end) && isStartOfParagraph(visibleEnd) && isStartOfParagraph(VisiblePosition(start))) { + // Only apply this rule if the endingSelection is a range selection. If it is a caret, then other operations have created + // the selection we're deleting (like the process of creating a selection to delete during a backspace), and the user isn't in the situation described above. + if (numEnclosingMailBlockquotes(start) != numEnclosingMailBlockquotes(end) + && isStartOfParagraph(visibleEnd) && isStartOfParagraph(VisiblePosition(start)) + && endingSelection().isRange()) { m_mergeBlocksAfterDelete = false; m_pruneStartBlockIfNecessary = true; } @@ -299,7 +307,7 @@ bool DeleteSelectionCommand::handleSpecialCaseBRDelete() // Not a special-case delete per se, but we can detect that the merging of content between blocks // should not be done. if (upstreamStartIsBR && downstreamStartIsBR) { - m_mergeBlocksAfterDelete = false; + m_startsAtEmptyLine = true; m_endingPosition = m_downstreamEnd; } @@ -310,8 +318,8 @@ static void updatePositionForNodeRemoval(Node* node, Position& position) { if (position.isNull()) return; - if (node->parent() == position.node() && node->nodeIndex() < (unsigned)position.m_offset) - position = Position(position.node(), position.m_offset - 1); + if (node->parent() == position.node() && node->nodeIndex() < (unsigned)position.deprecatedEditingOffset()) + position = Position(position.node(), position.deprecatedEditingOffset() - 1); if (position.node() == node || position.node()->isDescendantOf(node)) position = positionBeforeNode(node); } @@ -377,9 +385,9 @@ void DeleteSelectionCommand::removeNode(PassRefPtr<Node> node) static void updatePositionForTextRemoval(Node* node, int offset, int count, Position& position) { if (position.node() == node) { - if (position.m_offset > offset + count) - position = Position(position.node(), position.m_offset - count); - else if (position.m_offset > offset) + if (position.deprecatedEditingOffset() > offset + count) + position = Position(position.node(), position.deprecatedEditingOffset() - count); + else if (position.deprecatedEditingOffset() > offset) position = Position(position.node(), offset); } } @@ -390,13 +398,14 @@ void DeleteSelectionCommand::deleteTextFromNode(PassRefPtr<Text> node, unsigned updatePositionForTextRemoval(node.get(), offset, count, m_endingPosition); updatePositionForTextRemoval(node.get(), offset, count, m_leadingWhitespace); updatePositionForTextRemoval(node.get(), offset, count, m_trailingWhitespace); + updatePositionForTextRemoval(node.get(), offset, count, m_downstreamEnd); CompositeEditCommand::deleteTextFromNode(node, offset, count); } void DeleteSelectionCommand::handleGeneralDelete() { - int startOffset = m_upstreamStart.m_offset; + int startOffset = m_upstreamStart.deprecatedEditingOffset(); Node* startNode = m_upstreamStart.node(); // Never remove the start block unless it's a table, in which case we won't merge content in. @@ -425,13 +434,13 @@ void DeleteSelectionCommand::handleGeneralDelete() if (!startNode->renderer() || (startOffset == 0 && m_downstreamEnd.atLastEditingPositionForNode())) { // just delete removeNode(startNode); - } else if (m_downstreamEnd.m_offset - startOffset > 0) { + } else if (m_downstreamEnd.deprecatedEditingOffset() - startOffset > 0) { if (startNode->isTextNode()) { // in a text node that needs to be trimmed Text* text = static_cast<Text*>(startNode); - deleteTextFromNode(text, startOffset, m_downstreamEnd.m_offset - startOffset); + deleteTextFromNode(text, startOffset, m_downstreamEnd.deprecatedEditingOffset() - startOffset); } else { - removeChildrenInRange(startNode, startOffset, m_downstreamEnd.m_offset); + removeChildrenInRange(startNode, startOffset, m_downstreamEnd.deprecatedEditingOffset()); m_endingPosition = m_upstreamStart; } } @@ -454,7 +463,7 @@ void DeleteSelectionCommand::handleGeneralDelete() // handle deleting all nodes that are completely selected while (node && node != m_downstreamEnd.node()) { - if (Range::compareBoundaryPoints(Position(node.get(), 0), m_downstreamEnd) >= 0) { + if (comparePositions(Position(node.get(), 0), m_downstreamEnd) >= 0) { // traverseNextSibling just blew past the end position, so stop deleting node = 0; } else if (!m_downstreamEnd.node()->isDescendantOf(node.get())) { @@ -462,14 +471,14 @@ void DeleteSelectionCommand::handleGeneralDelete() // if we just removed a node from the end container, update end position so the // check above will work if (node->parentNode() == m_downstreamEnd.node()) { - ASSERT(node->nodeIndex() < (unsigned)m_downstreamEnd.m_offset); - m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.m_offset - 1); + ASSERT(node->nodeIndex() < (unsigned)m_downstreamEnd.deprecatedEditingOffset()); + m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.deprecatedEditingOffset() - 1); } removeNode(node.get()); node = nextNode.get(); } else { Node* n = node->lastDescendant(); - if (m_downstreamEnd.node() == n && m_downstreamEnd.m_offset >= caretMaxOffset(n)) { + if (m_downstreamEnd.node() == n && m_downstreamEnd.deprecatedEditingOffset() >= caretMaxOffset(n)) { removeNode(node.get()); node = 0; } else @@ -477,7 +486,7 @@ void DeleteSelectionCommand::handleGeneralDelete() } } - if (m_downstreamEnd.node() != startNode && !m_upstreamStart.node()->isDescendantOf(m_downstreamEnd.node()) && m_downstreamEnd.node()->inDocument() && m_downstreamEnd.m_offset >= caretMinOffset(m_downstreamEnd.node())) { + if (m_downstreamEnd.node() != startNode && !m_upstreamStart.node()->isDescendantOf(m_downstreamEnd.node()) && m_downstreamEnd.node()->inDocument() && m_downstreamEnd.deprecatedEditingOffset() >= caretMinOffset(m_downstreamEnd.node())) { if (m_downstreamEnd.atLastEditingPositionForNode() && !canHaveChildrenForEditing(m_downstreamEnd.node())) { // The node itself is fully selected, not just its contents. Delete it. removeNode(m_downstreamEnd.node()); @@ -485,9 +494,8 @@ void DeleteSelectionCommand::handleGeneralDelete() if (m_downstreamEnd.node()->isTextNode()) { // in a text node that needs to be trimmed Text *text = static_cast<Text *>(m_downstreamEnd.node()); - if (m_downstreamEnd.m_offset > 0) { - deleteTextFromNode(text, 0, m_downstreamEnd.m_offset); - m_downstreamEnd = Position(text, 0); + if (m_downstreamEnd.deprecatedEditingOffset() > 0) { + deleteTextFromNode(text, 0, m_downstreamEnd.deprecatedEditingOffset()); } // Remove children of m_downstreamEnd.node() that come after m_upstreamStart. // Don't try to remove children if m_upstreamStart was inside m_downstreamEnd.node() @@ -504,7 +512,7 @@ void DeleteSelectionCommand::handleGeneralDelete() if (n) offset = n->nodeIndex() + 1; } - removeChildrenInRange(m_downstreamEnd.node(), offset, m_downstreamEnd.m_offset); + removeChildrenInRange(m_downstreamEnd.node(), offset, m_downstreamEnd.deprecatedEditingOffset()); m_downstreamEnd = Position(m_downstreamEnd.node(), offset); } } @@ -519,12 +527,12 @@ void DeleteSelectionCommand::fixupWhitespace() if (m_leadingWhitespace.isNotNull() && !m_leadingWhitespace.isRenderedCharacter()) { Text* textNode = static_cast<Text*>(m_leadingWhitespace.node()); ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); - replaceTextInNode(textNode, m_leadingWhitespace.m_offset, 1, nonBreakingSpaceString()); + replaceTextInNode(textNode, m_leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); } if (m_trailingWhitespace.isNotNull() && !m_trailingWhitespace.isRenderedCharacter()) { Text* textNode = static_cast<Text*>(m_trailingWhitespace.node()); ASSERT(!textNode->renderer() ||textNode->renderer()->style()->collapseWhiteSpace()); - replaceTextInNode(textNode, m_trailingWhitespace.m_offset, 1, nonBreakingSpaceString()); + replaceTextInNode(textNode, m_trailingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); } } @@ -537,7 +545,7 @@ void DeleteSelectionCommand::mergeParagraphs() // Make sure that the ending position isn't inside the block we're about to prune. m_endingPosition = m_downstreamEnd; // We aren't going to merge into the start block, so remove it if it's empty. - prune(m_upstreamStart.node()); + prune(m_startBlock); // Removing the start block during a deletion is usually an indication that we need // a placeholder, but not in this case. m_needPlaceholder = false; @@ -553,12 +561,11 @@ void DeleteSelectionCommand::mergeParagraphs() return; // FIXME: The deletion algorithm shouldn't let this happen. - if (Range::compareBoundaryPoints(m_upstreamStart, m_downstreamEnd) > 0) + if (comparePositions(m_upstreamStart, m_downstreamEnd) > 0) return; - // FIXME: Merging will always be unnecessary in this case, but we really bail here because this is a case where - // deletion commonly fails to adjust its endpoints, which would cause the visible position comparison below to false negative. - if (m_endBlock == m_startBlock) + // There's nothing to merge. + if (m_upstreamStart == m_downstreamEnd) return; VisiblePosition startOfParagraphToMove(m_downstreamEnd); @@ -573,7 +580,7 @@ void DeleteSelectionCommand::mergeParagraphs() } // We need to merge into m_upstreamStart's block, but it's been emptied out and collapsed by deletion. - if (!mergeDestination.deepEquivalent().node() || !mergeDestination.deepEquivalent().node()->isDescendantOf(m_upstreamStart.node()->enclosingBlockFlowElement())) { + if (!mergeDestination.deepEquivalent().node() || !mergeDestination.deepEquivalent().node()->isDescendantOf(m_upstreamStart.node()->enclosingBlockFlowElement()) || m_startsAtEmptyLine) { insertNodeAt(createBreakElement(document()).get(), m_upstreamStart); mergeDestination = VisiblePosition(m_upstreamStart); } @@ -588,8 +595,7 @@ void DeleteSelectionCommand::mergeParagraphs() // The rule for merging into an empty block is: only do so if its farther to the right. // FIXME: Consider RTL. - // FIXME: handleSpecialCaseBRDelete prevents us from getting here in a case like <ul><li>foo<br><br></li></ul>^foo - if (isStartOfParagraph(mergeDestination) && startOfParagraphToMove.absoluteCaretBounds().x() > mergeDestination.absoluteCaretBounds().x()) { + if (!m_startsAtEmptyLine && isStartOfParagraph(mergeDestination) && startOfParagraphToMove.absoluteCaretBounds().x() > mergeDestination.absoluteCaretBounds().x()) { ASSERT(mergeDestination.deepEquivalent().downstream().node()->hasTagName(brTag)); removeNodeAndPruneAncestors(mergeDestination.deepEquivalent().downstream().node()); m_endingPosition = startOfParagraphToMove.deepEquivalent(); @@ -671,7 +677,7 @@ void DeleteSelectionCommand::calculateTypingStyleAfterDelete() if (m_typingStyle && isStartOfParagraph(visibleEnd) && isEndOfParagraph(visibleEnd) && - lineBreakExistsAtPosition(visibleEnd)) { + lineBreakExistsAtVisiblePosition(visibleEnd)) { // Apply style to the placeholder that is now holding open the empty paragraph. // This makes sure that the paragraph has the right height, and that the paragraph // takes on the right style and retains it even if you move the selection away and @@ -729,7 +735,7 @@ void DeleteSelectionCommand::doApply() Position downstreamEnd = m_selectionToDelete.end().downstream(); m_needPlaceholder = isStartOfParagraph(m_selectionToDelete.visibleStart()) && isEndOfParagraph(m_selectionToDelete.visibleEnd()) && - !lineBreakExistsAtPosition(m_selectionToDelete.visibleEnd()); + !lineBreakExistsAtVisiblePosition(m_selectionToDelete.visibleEnd()); if (m_needPlaceholder) { // Don't need a placeholder when deleting a selection that starts just before a table // and ends inside it (we do need placeholders to hold open empty cells, but that's diff --git a/WebCore/editing/DeleteSelectionCommand.h b/WebCore/editing/DeleteSelectionCommand.h index 640c549..c8872ef 100644 --- a/WebCore/editing/DeleteSelectionCommand.h +++ b/WebCore/editing/DeleteSelectionCommand.h @@ -72,6 +72,7 @@ private: bool m_replace; bool m_expandForSpecialElements; bool m_pruneStartBlockIfNecessary; + bool m_startsAtEmptyLine; // This data is transient and should be cleared at the end of the doApply function. VisibleSelection m_selectionToDelete; @@ -90,6 +91,7 @@ private: RefPtr<Node> m_endRoot; RefPtr<Node> m_startTableRow; RefPtr<Node> m_endTableRow; + RefPtr<Node> m_temporaryPlaceholder; }; } // namespace WebCore diff --git a/WebCore/editing/Editor.cpp b/WebCore/editing/Editor.cpp index f44449b..2c303f9 100644 --- a/WebCore/editing/Editor.cpp +++ b/WebCore/editing/Editor.cpp @@ -29,7 +29,10 @@ #include "AXObjectCache.h" #include "ApplyStyleCommand.h" +#include "CharacterNames.h" +#include "CreateLinkCommand.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSProperty.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" @@ -388,7 +391,7 @@ void Editor::respondToChangedContents(const VisibleSelection& endingSelection) if (AXObjectCache::accessibilityEnabled()) { Node* node = endingSelection.start().node(); if (node) - m_frame->document()->axObjectCache()->postNotification(node->renderer(), "AXValueChanged"); + m_frame->document()->axObjectCache()->postNotification(node->renderer(), "AXValueChanged", false); } if (client()) @@ -1122,6 +1125,110 @@ int Editor::spellCheckerDocumentTag() return client() ? client()->spellCheckerDocumentTag() : 0; } +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +void Editor::uppercaseWord() +{ + if (client()) + client()->uppercaseWord(); +} + +void Editor::lowercaseWord() +{ + if (client()) + client()->lowercaseWord(); +} + +void Editor::capitalizeWord() +{ + if (client()) + client()->capitalizeWord(); +} + +void Editor::showSubstitutionsPanel() +{ + if (!client()) { + LOG_ERROR("No NSSpellChecker"); + return; + } + + if (client()->substitutionsPanelIsShowing()) { + client()->showSubstitutionsPanel(false); + return; + } + client()->showSubstitutionsPanel(true); +} + +bool Editor::substitutionsPanelIsShowing() +{ + if (!client()) + return false; + return client()->substitutionsPanelIsShowing(); +} + +void Editor::toggleSmartInsertDelete() +{ + if (client()) + client()->toggleSmartInsertDelete(); +} + +bool Editor::isAutomaticQuoteSubstitutionEnabled() +{ + return client() && client()->isAutomaticQuoteSubstitutionEnabled(); +} + +void Editor::toggleAutomaticQuoteSubstitution() +{ + if (client()) + client()->toggleAutomaticQuoteSubstitution(); +} + +bool Editor::isAutomaticLinkDetectionEnabled() +{ + return client() && client()->isAutomaticLinkDetectionEnabled(); +} + +void Editor::toggleAutomaticLinkDetection() +{ + if (client()) + client()->toggleAutomaticLinkDetection(); +} + +bool Editor::isAutomaticDashSubstitutionEnabled() +{ + return client() && client()->isAutomaticDashSubstitutionEnabled(); +} + +void Editor::toggleAutomaticDashSubstitution() +{ + if (client()) + client()->toggleAutomaticDashSubstitution(); +} + +bool Editor::isAutomaticTextReplacementEnabled() +{ + return client() && client()->isAutomaticTextReplacementEnabled(); +} + +void Editor::toggleAutomaticTextReplacement() +{ + if (client()) + client()->toggleAutomaticTextReplacement(); +} + +bool Editor::isAutomaticSpellingCorrectionEnabled() +{ + return client() && client()->isAutomaticSpellingCorrectionEnabled(); +} + +void Editor::toggleAutomaticSpellingCorrection() +{ + if (client()) + client()->toggleAutomaticSpellingCorrection(); +} + +#endif + bool Editor::shouldEndEditing(Range* range) { return client() && client()->shouldEndEditing(range); @@ -1197,7 +1304,7 @@ void Editor::setBaseWritingDirection(WritingDirection direction) if (direction == NaturalWritingDirection) return; static_cast<HTMLElement*>(focusedNode)->setAttribute(dirAttr, direction == LeftToRightWritingDirection ? "ltr" : "rtl"); - frame()->document()->updateRendering(); + frame()->document()->updateStyleIfNeeded(); return; } @@ -1290,9 +1397,9 @@ void Editor::setComposition(const String& text, const Vector<CompositionUnderlin TypingCommand::insertText(m_frame->document(), text, true, true); Node* baseNode = m_frame->selection()->base().node(); - unsigned baseOffset = m_frame->selection()->base().m_offset; + unsigned baseOffset = m_frame->selection()->base().deprecatedEditingOffset(); Node* extentNode = m_frame->selection()->extent().node(); - unsigned extentOffset = m_frame->selection()->extent().m_offset; + unsigned extentOffset = m_frame->selection()->extent().deprecatedEditingOffset(); if (baseNode && baseNode == extentNode && baseNode->isTextNode() && baseOffset + text.length() == extentOffset) { m_compositionNode = static_cast<Text*>(baseNode); @@ -1344,7 +1451,7 @@ void Editor::learnSpelling() client()->learnWord(text); } -static String findFirstMisspellingInRange(EditorClient* client, Range* searchRange, int& firstMisspellingOffset, bool markAll) +static String findFirstMisspellingInRange(EditorClient* client, Range* searchRange, int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange) { ASSERT_ARG(client, client); ASSERT_ARG(searchRange, searchRange); @@ -1378,23 +1485,24 @@ static String findFirstMisspellingInRange(EditorClient* client, Range* searchRan if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) { - // Remember first-encountered misspelling and its offset + // Compute range of misspelled word + RefPtr<Range> misspellingRange = TextIterator::subrange(searchRange, currentChunkOffset + misspellingLocation, misspellingLength); + + // Remember first-encountered misspelling and its offset. if (!firstMisspelling) { firstMisspellingOffset = currentChunkOffset + misspellingLocation; firstMisspelling = String(chars + misspellingLocation, misspellingLength); + firstMisspellingRange = misspellingRange; } - - // Mark this instance if we're marking all instances. Otherwise bail out because we found the first one. - if (!markAll) - break; - - // Compute range of misspelled word - RefPtr<Range> misspellingRange = TextIterator::subrange(searchRange, currentChunkOffset + misspellingLocation, misspellingLength); - - // Store marker for misspelled word + + // Store marker for misspelled word. ExceptionCode ec = 0; misspellingRange->startContainer(ec)->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); ASSERT(ec == 0); + + // Bail out if we're marking only the first misspelling, and not all instances. + if (!markAll) + break; } } @@ -1592,17 +1700,18 @@ static String findFirstMisspellingOrBadGrammarInRange(EditorClient* client, Rang unsigned grammarDetailIndex = 0; Vector<TextCheckingResult> results; - client->checkSpellingAndGrammarOfParagraph(paragraphString.characters(), paragraphString.length(), checkGrammar, results); + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); for (unsigned i = 0; i < results.size(); i++) { const TextCheckingResult* result = &results[i]; - if (result->resultType == 1 && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) { + if (result->type == TextCheckingTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) { ASSERT(result->length > 0 && result->location >= 0); spellingLocation = result->location; misspelledWord = paragraphString.substring(result->location, result->length); ASSERT(misspelledWord.length() != 0); break; - } else if (checkGrammar && result->resultType == 2 && result->location < currentEndOffset && result->location + result->length > currentStartOffset) { + } else if (checkGrammar && result->type == TextCheckingTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) { ASSERT(result->length > 0 && result->location >= 0); // We can't stop after the first grammar result, since there might still be a spelling result after // it begins but before the first detail in it, but we can stop if we find a second grammar result. @@ -1696,7 +1805,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) return; Position rangeCompliantPosition = rangeCompliantEquivalent(position); - spellingSearchRange->setStart(rangeCompliantPosition.node(), rangeCompliantPosition.m_offset, ec); + spellingSearchRange->setStart(rangeCompliantPosition.node(), rangeCompliantPosition.deprecatedEditingOffset(), ec); startedWithSelection = false; // won't need to wrap } @@ -1745,8 +1854,8 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) grammarPhraseOffset = foundOffset; } #else - String misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false); - + RefPtr<Range> firstMisspellingRange; + String misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false, firstMisspellingRange); String badGrammarPhrase; #ifndef BUILDING_ON_TIGER @@ -1785,7 +1894,7 @@ void Editor::advanceToNextMisspelling(bool startBeforeSelection) grammarPhraseOffset = foundOffset; } #else - misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false); + misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false, firstMisspellingRange); #ifndef BUILDING_ON_TIGER grammarSearchRange = spellingSearchRange->cloneRange(ec); @@ -1961,11 +2070,12 @@ static Vector<String> guessesForMisspelledOrUngrammaticalRange(EditorClient* cli return guesses; Vector<TextCheckingResult> results; - client->checkSpellingAndGrammarOfParagraph(paragraphString.characters(), paragraphString.length(), checkGrammar, results); + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); for (unsigned i = 0; i < results.size(); i++) { const TextCheckingResult* result = &results[i]; - if (result->resultType == 1 && result->location == rangeStartOffset && result->length == rangeLength) { + if (result->type == TextCheckingTypeSpelling && result->location == rangeStartOffset && result->length == rangeLength) { String misspelledWord = paragraphString.substring(rangeStartOffset, rangeLength); ASSERT(misspelledWord.length() != 0); client->getGuessesForWord(misspelledWord, guesses); @@ -1980,7 +2090,7 @@ static Vector<String> guessesForMisspelledOrUngrammaticalRange(EditorClient* cli for (unsigned i = 0; i < results.size(); i++) { const TextCheckingResult* result = &results[i]; - if (result->resultType == 2 && result->location <= rangeStartOffset && result->location + result->length >= rangeStartOffset + rangeLength) { + if (result->type == TextCheckingTypeGrammar && result->location <= rangeStartOffset && result->location + result->length >= rangeStartOffset + rangeLength) { for (unsigned j = 0; j < result->details.size(); j++) { const GrammarDetail* detail = &result->details[j]; ASSERT(detail->length > 0 && detail->location >= 0); @@ -2049,21 +2159,58 @@ bool Editor::spellingPanelIsShowing() void Editor::markMisspellingsAfterTypingToPosition(const VisiblePosition &p) { - if (!isContinuousSpellCheckingEnabled()) +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + bool markSpelling = isContinuousSpellCheckingEnabled(); + bool markGrammar = markSpelling && isGrammarCheckingEnabled(); + bool performTextCheckingReplacements = isAutomaticQuoteSubstitutionEnabled() + || isAutomaticLinkDetectionEnabled() + || isAutomaticDashSubstitutionEnabled() + || isAutomaticTextReplacementEnabled() + || (markSpelling && isAutomaticSpellingCorrectionEnabled()); + if (!markSpelling && !performTextCheckingReplacements) return; -#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) VisibleSelection adjacentWords = VisibleSelection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary)); - if (isGrammarCheckingEnabled()) { + if (markGrammar) { VisibleSelection selectedSentence = VisibleSelection(startOfSentence(p), endOfSentence(p)); - markMisspellingsAndBadGrammar(adjacentWords, true, selectedSentence); + markAllMisspellingsAndBadGrammarInRanges(true, adjacentWords.toNormalizedRange().get(), true, selectedSentence.toNormalizedRange().get(), performTextCheckingReplacements); } else { - markMisspellingsAndBadGrammar(adjacentWords, false, adjacentWords); + markAllMisspellingsAndBadGrammarInRanges(markSpelling, adjacentWords.toNormalizedRange().get(), false, adjacentWords.toNormalizedRange().get(), performTextCheckingReplacements); } #else + if (!isContinuousSpellCheckingEnabled()) + return; + // Check spelling of one word - markMisspellings(VisibleSelection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary))); + RefPtr<Range> misspellingRange; + markMisspellings(VisibleSelection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary)), misspellingRange); + + // Autocorrect the misspelled word. + if (misspellingRange == 0) + return; + // Get the misspelled word. + const String misspelledWord = plainText(misspellingRange.get()); + String autocorrectedString = client()->getAutoCorrectSuggestionForMisspelledWord(misspelledWord); + + // If autocorrected word is non empty, replace the misspelled word by this word. + if (!autocorrectedString.isEmpty()) { + VisibleSelection newSelection(misspellingRange.get(), DOWNSTREAM); + if (newSelection != frame()->selection()->selection()) { + if (!frame()->shouldChangeSelection(newSelection)) + return; + frame()->selection()->setSelection(newSelection); + } + + if (!frame()->editor()->shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) + return; + frame()->editor()->replaceSelectionWithText(autocorrectedString, false, true); + + // Reset the charet one character further. + frame()->selection()->moveTo(frame()->selection()->end()); + frame()->selection()->modify(SelectionController::MOVE, SelectionController::FORWARD, CharacterGranularity); + } + if (!isGrammarCheckingEnabled()) return; @@ -2072,12 +2219,12 @@ void Editor::markMisspellingsAfterTypingToPosition(const VisiblePosition &p) #endif } -static void markAllMisspellingsInRange(EditorClient* client, Range* searchRange) +static void markAllMisspellingsInRange(EditorClient* client, Range* searchRange, RefPtr<Range>& firstMisspellingRange) { // Use the "markAll" feature of findFirstMisspellingInRange. Ignore the return value and the "out parameter"; // all we need to do is mark every instance. int ignoredOffset; - findFirstMisspellingInRange(client, searchRange, ignoredOffset, true); + findFirstMisspellingInRange(client, searchRange, ignoredOffset, true, firstMisspellingRange); } #ifndef BUILDING_ON_TIGER @@ -2091,7 +2238,7 @@ static void markAllBadGrammarInRange(EditorClient* client, Range* searchRange) } #endif -static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& selection, bool checkSpelling) +static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& selection, bool checkSpelling, RefPtr<Range>& firstMisspellingRange) { // This function is called with a selection already expanded to word boundaries. // Might be nice to assert that here. @@ -2110,12 +2257,26 @@ static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& if (!editableNode || !editableNode->isContentEditable()) return; + // Ascend the DOM tree to find a "spellcheck" attribute. + // When we find a "spellcheck" attribute, retrieve its value and exit if its value is "false". + const Node* node = editor->frame()->document()->focusedNode(); + while (node) { + if (node->isElementNode()) { + const WebCore::AtomicString& value = static_cast<const Element*>(node)->getAttribute(spellcheckAttr); + if (equalIgnoringCase(value, "true")) + break; + if (equalIgnoringCase(value, "false")) + return; + } + node = node->parent(); + } + // Get the spell checker if it is available if (!editor->client()) return; if (checkSpelling) - markAllMisspellingsInRange(editor->client(), searchRange.get()); + markAllMisspellingsInRange(editor->client(), searchRange.get(), firstMisspellingRange); else { #ifdef BUILDING_ON_TIGER ASSERT_NOT_REACHED(); @@ -2126,15 +2287,16 @@ static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& } } -void Editor::markMisspellings(const VisibleSelection& selection) +void Editor::markMisspellings(const VisibleSelection& selection, RefPtr<Range>& firstMisspellingRange) { - markMisspellingsOrBadGrammar(this, selection, true); + markMisspellingsOrBadGrammar(this, selection, true, firstMisspellingRange); } void Editor::markBadGrammar(const VisibleSelection& selection) { #ifndef BUILDING_ON_TIGER - markMisspellingsOrBadGrammar(this, selection, false); + RefPtr<Range> firstMisspellingRange; + markMisspellingsOrBadGrammar(this, selection, false, firstMisspellingRange); #else UNUSED_PARAM(selection); #endif @@ -2142,11 +2304,19 @@ void Editor::markBadGrammar(const VisibleSelection& selection) #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) -static void markAllMisspellingsAndBadGrammarInRanges(EditorClient* client, Range *spellingRange, bool markGrammar, Range *grammarRange) +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; +} + +void Editor::markAllMisspellingsAndBadGrammarInRanges(bool markSpelling, Range* spellingRange, bool markGrammar, Range* grammarRange, bool performTextCheckingReplacements) { // This function is called with selections already expanded to word boundaries. - ExceptionCode ec; - if (!client || !spellingRange || (markGrammar && !grammarRange)) + ExceptionCode ec = 0; + if (!client() || !spellingRange || (markGrammar && !grammarRange)) return; // If we're not in an editable node, bail. @@ -2159,42 +2329,188 @@ static void markAllMisspellingsAndBadGrammarInRanges(EditorClient* client, Range int spellingRangeEndOffset = 0; int grammarRangeStartOffset = 0; int grammarRangeEndOffset = 0; + int offsetDueToReplacement = 0; + int paragraphLength = 0; + int selectionOffset = 0; + int ambiguousBoundaryOffset = -1; + bool selectionChanged = false; + bool restoreSelectionAfterChange = false; + bool adjustSelectionForParagraphBoundaries = false; String paragraphString; + RefPtr<Range> paragraphRange; if (markGrammar) { // The spelling range should be contained in the paragraph-aligned extension of the grammar range. - RefPtr<Range> paragraphRange = paragraphAlignedRangeForRange(grammarRange, grammarRangeStartOffset, paragraphString); + paragraphRange = paragraphAlignedRangeForRange(grammarRange, grammarRangeStartOffset, paragraphString); RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), spellingRange->startPosition()); spellingRangeStartOffset = TextIterator::rangeLength(offsetAsRange.get()); grammarRangeEndOffset = grammarRangeStartOffset + TextIterator::rangeLength(grammarRange); } else { - RefPtr<Range> paragraphRange = paragraphAlignedRangeForRange(spellingRange, spellingRangeStartOffset, paragraphString); + paragraphRange = paragraphAlignedRangeForRange(spellingRange, spellingRangeStartOffset, paragraphString); } spellingRangeEndOffset = spellingRangeStartOffset + TextIterator::rangeLength(spellingRange); - if (paragraphString.length() == 0 || (spellingRangeStartOffset >= spellingRangeEndOffset && (!markGrammar || grammarRangeStartOffset >= grammarRangeEndOffset))) + paragraphLength = paragraphString.length(); + if (paragraphLength <= 0 || (spellingRangeStartOffset >= spellingRangeEndOffset && (!markGrammar || grammarRangeStartOffset >= grammarRangeEndOffset))) return; + if (performTextCheckingReplacements) { + if (m_frame->selection()->selectionType() == VisibleSelection::CaretSelection) { + // Attempt to save the caret position so we can restore it later if needed + RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), paragraphRange->startPosition()); + Position caretPosition = m_frame->selection()->end(); + offsetAsRange->setEnd(caretPosition.containerNode(), caretPosition.computeOffsetInContainerNode(), ec); + if (!ec) { + selectionOffset = TextIterator::rangeLength(offsetAsRange.get()); + restoreSelectionAfterChange = true; + if (selectionOffset > 0 && (selectionOffset > paragraphLength || paragraphString[selectionOffset - 1] == newlineCharacter)) + adjustSelectionForParagraphBoundaries = true; + if (selectionOffset > 0 && selectionOffset <= paragraphLength && isAmbiguousBoundaryCharacter(paragraphString[selectionOffset - 1])) + ambiguousBoundaryOffset = selectionOffset - 1; + } + } + } + Vector<TextCheckingResult> results; - client->checkSpellingAndGrammarOfParagraph(paragraphString.characters(), paragraphString.length(), markGrammar, results); + uint64_t checkingTypes = 0; + if (markSpelling) + checkingTypes |= TextCheckingTypeSpelling; + if (markGrammar) + checkingTypes |= TextCheckingTypeGrammar; + if (performTextCheckingReplacements) { + if (isAutomaticLinkDetectionEnabled()) + checkingTypes |= TextCheckingTypeLink; + if (isAutomaticQuoteSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeQuote; + if (isAutomaticDashSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeDash; + if (isAutomaticTextReplacementEnabled()) + checkingTypes |= TextCheckingTypeReplacement; + if (markSpelling && isAutomaticSpellingCorrectionEnabled()) + checkingTypes |= TextCheckingTypeCorrection; + } + client()->checkTextOfParagraph(paragraphString.characters(), paragraphLength, checkingTypes, results); for (unsigned i = 0; i < results.size(); i++) { const TextCheckingResult* result = &results[i]; - if (result->resultType == 1 && result->location >= spellingRangeStartOffset && result->location + result->length <= spellingRangeEndOffset) { - ASSERT(result->length > 0 && result->location >= 0); - RefPtr<Range> misspellingRange = TextIterator::subrange(spellingRange, result->location - spellingRangeStartOffset, result->length); + int resultLocation = result->location + offsetDueToReplacement; + int resultLength = result->length; + if (markSpelling && result->type == TextCheckingTypeSpelling && resultLocation >= spellingRangeStartOffset && resultLocation + resultLength <= spellingRangeEndOffset) { + ASSERT(resultLength > 0 && resultLocation >= 0); + RefPtr<Range> misspellingRange = TextIterator::subrange(spellingRange, resultLocation - spellingRangeStartOffset, resultLength); misspellingRange->startContainer(ec)->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); - } else if (markGrammar && result->resultType == 2 && result->location < grammarRangeEndOffset && result->location + result->length > grammarRangeStartOffset) { - ASSERT(result->length > 0 && result->location >= 0); + } else if (markGrammar && result->type == TextCheckingTypeGrammar && resultLocation < grammarRangeEndOffset && resultLocation + resultLength > grammarRangeStartOffset) { + ASSERT(resultLength > 0 && resultLocation >= 0); for (unsigned j = 0; j < result->details.size(); j++) { const GrammarDetail* detail = &result->details[j]; ASSERT(detail->length > 0 && detail->location >= 0); - if (result->location + detail->location >= grammarRangeStartOffset && result->location + detail->location + detail->length <= grammarRangeEndOffset) { - RefPtr<Range> badGrammarRange = TextIterator::subrange(grammarRange, result->location + detail->location - grammarRangeStartOffset, detail->length); + if (resultLocation + detail->location >= grammarRangeStartOffset && resultLocation + detail->location + detail->length <= grammarRangeEndOffset) { + RefPtr<Range> badGrammarRange = TextIterator::subrange(grammarRange, resultLocation + detail->location - grammarRangeStartOffset, detail->length); grammarRange->startContainer(ec)->document()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); } } + } else if (performTextCheckingReplacements && resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingRangeStartOffset && + (result->type == TextCheckingTypeLink + || result->type == TextCheckingTypeQuote + || result->type == TextCheckingTypeDash + || result->type == TextCheckingTypeReplacement + || result->type == TextCheckingTypeCorrection)) { + // In this case the result range just has to touch the spelling range, so we can handle replacing non-word text such as punctuation. + ASSERT(resultLength > 0 && resultLocation >= 0); + int replacementLength = result->replacement.length(); + bool doReplacement = (replacementLength > 0); + RefPtr<Range> rangeToReplace = TextIterator::subrange(paragraphRange.get(), resultLocation, resultLength); + VisibleSelection selectionToReplace(rangeToReplace.get(), DOWNSTREAM); + + // avoid correcting text after an ambiguous boundary character has been typed + // FIXME: this is required until 6853027 is fixed and text checking can do this for us + if (ambiguousBoundaryOffset >= 0 && resultLocation + resultLength == ambiguousBoundaryOffset) + doReplacement = false; + + // adding links should be done only immediately after they are typed + if (result->type == TextCheckingTypeLink && selectionOffset > resultLocation + resultLength + 1) + doReplacement = false; + + // Don't correct spelling in an already-corrected word. + if (doReplacement && result->type == TextCheckingTypeCorrection) { + Node* node = rangeToReplace->startContainer(); + int startOffset = rangeToReplace->startOffset(); + int endOffset = startOffset + replacementLength; + Vector<DocumentMarker> markers = node->document()->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 && static_cast<int>(marker.startOffset) < endOffset && static_cast<int>(marker.endOffset) > startOffset) { + doReplacement = false; + break; + } + if (static_cast<int>(marker.startOffset) >= endOffset) + break; + } + } + if (doReplacement && selectionToReplace != m_frame->selection()->selection()) { + if (m_frame->shouldChangeSelection(selectionToReplace)) { + m_frame->selection()->setSelection(selectionToReplace); + selectionChanged = true; + } else { + doReplacement = false; + } + } + if (doReplacement) { + if (result->type == TextCheckingTypeLink) { + restoreSelectionAfterChange = false; + 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()); + replaceSelectionWithText(result->replacement, false, false); + spellingRangeEndOffset += replacementLength - resultLength; + offsetDueToReplacement += replacementLength - resultLength; + if (resultLocation < selectionOffset) + selectionOffset += replacementLength - resultLength; + if (result->type == TextCheckingTypeCorrection) { + // Add a marker so that corrections can easily be undone and won't be re-corrected. + RefPtr<Range> replacedRange = TextIterator::subrange(paragraphRange.get(), resultLocation, replacementLength); + replacedRange->startContainer()->document()->addMarker(replacedRange.get(), DocumentMarker::Replacement, replacedString); + } + } + } } } + + if (selectionChanged) { + // Restore the caret position if we have made any replacements + setEnd(paragraphRange.get(), endOfParagraph(startOfNextParagraph(paragraphRange->startPosition()))); + int newLength = TextIterator::rangeLength(paragraphRange.get()); + if (restoreSelectionAfterChange && selectionOffset >= 0 && selectionOffset <= newLength) { + RefPtr<Range> selectionRange = TextIterator::subrange(paragraphRange.get(), 0, selectionOffset); + m_frame->selection()->moveTo(selectionRange->endPosition(), DOWNSTREAM); + if (adjustSelectionForParagraphBoundaries) + m_frame->selection()->modify(SelectionController::MOVE, SelectionController::FORWARD, CharacterGranularity); + } else { + // If this fails for any reason, the fallback is to go one position beyond the last replacement + m_frame->selection()->moveTo(m_frame->selection()->end()); + m_frame->selection()->modify(SelectionController::MOVE, SelectionController::FORWARD, CharacterGranularity); + } + } +} + +void Editor::changeBackToReplacedString(const String& replacedString) +{ + if (replacedString.isEmpty()) + return; + + RefPtr<Range> selection = selectedRange(); + if (!shouldInsertText(replacedString, selection.get(), EditorInsertActionPasted)) + return; + + String paragraphString; + int selectionOffset; + RefPtr<Range> paragraphRange = paragraphAlignedRangeForRange(selection.get(), selectionOffset, paragraphString); + replaceSelectionWithText(replacedString, false, false); + RefPtr<Range> changedRange = TextIterator::subrange(paragraphRange.get(), selectionOffset, replacedString.length()); + changedRange->startContainer()->document()->addMarker(changedRange.get(), DocumentMarker::Replacement, String()); } #endif @@ -2204,9 +2520,10 @@ void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelec #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) if (!isContinuousSpellCheckingEnabled()) return; - markAllMisspellingsAndBadGrammarInRanges(client(), spellingSelection.toNormalizedRange().get(), markGrammar && isGrammarCheckingEnabled(), grammarSelection.toNormalizedRange().get()); + markAllMisspellingsAndBadGrammarInRanges(true, spellingSelection.toNormalizedRange().get(), markGrammar && isGrammarCheckingEnabled(), grammarSelection.toNormalizedRange().get(), false); #else - markMisspellings(spellingSelection); + RefPtr<Range> firstMisspellingRange; + markMisspellings(spellingSelection, firstMisspellingRange); if (markGrammar) markBadGrammar(grammarSelection); #endif @@ -2269,13 +2586,13 @@ bool Editor::getCompositionSelection(unsigned& selectionStart, unsigned& selecti if (end.node() != m_compositionNode) return false; - if (static_cast<unsigned>(start.m_offset) < m_compositionStart) + if (static_cast<unsigned>(start.deprecatedEditingOffset()) < m_compositionStart) return false; - if (static_cast<unsigned>(end.m_offset) > m_compositionEnd) + if (static_cast<unsigned>(end.deprecatedEditingOffset()) > m_compositionEnd) return false; - selectionStart = start.m_offset - m_compositionStart; - selectionEnd = start.m_offset - m_compositionEnd; + selectionStart = start.deprecatedEditingOffset() - m_compositionStart; + selectionEnd = start.deprecatedEditingOffset() - m_compositionEnd; return true; } diff --git a/WebCore/editing/Editor.h b/WebCore/editing/Editor.h index ce8fa0f..67a4b59 100644 --- a/WebCore/editing/Editor.h +++ b/WebCore/editing/Editor.h @@ -197,9 +197,29 @@ public: Vector<String> guessesForUngrammaticalSelection(); Vector<String> guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical); void markMisspellingsAfterTypingToPosition(const VisiblePosition&); - void markMisspellings(const VisibleSelection&); + void markMisspellings(const VisibleSelection&, RefPtr<Range>& firstMisspellingRange); void markBadGrammar(const VisibleSelection&); void markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection); +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + void uppercaseWord(); + void lowercaseWord(); + void capitalizeWord(); + void showSubstitutionsPanel(); + bool substitutionsPanelIsShowing(); + void toggleSmartInsertDelete(); + bool isAutomaticQuoteSubstitutionEnabled(); + void toggleAutomaticQuoteSubstitution(); + bool isAutomaticLinkDetectionEnabled(); + void toggleAutomaticLinkDetection(); + bool isAutomaticDashSubstitutionEnabled(); + void toggleAutomaticDashSubstitution(); + bool isAutomaticTextReplacementEnabled(); + void toggleAutomaticTextReplacement(); + bool isAutomaticSpellingCorrectionEnabled(); + void toggleAutomaticSpellingCorrection(); + void markAllMisspellingsAndBadGrammarInRanges(bool markSpelling, Range* spellingRange, bool markGrammar, Range* grammarRange, bool performTextCheckingReplacements); + void changeBackToReplacedString(const String& replacedString); +#endif void advanceToNextMisspelling(bool startBeforeSelection = false); void showSpellingGuessPanel(); bool spellingPanelIsShowing(); diff --git a/WebCore/editing/EditorCommand.cpp b/WebCore/editing/EditorCommand.cpp index 0090df7..5a189d4 100644 --- a/WebCore/editing/EditorCommand.cpp +++ b/WebCore/editing/EditorCommand.cpp @@ -27,6 +27,7 @@ #include "config.h" #include "AtomicString.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" #include "CreateLinkCommand.h" diff --git a/WebCore/editing/FormatBlockCommand.cpp b/WebCore/editing/FormatBlockCommand.cpp index 88169be..d92f365 100644 --- a/WebCore/editing/FormatBlockCommand.cpp +++ b/WebCore/editing/FormatBlockCommand.cpp @@ -124,7 +124,7 @@ void FormatBlockCommand::doApply() appendNode(placeholder, blockNode); VisiblePosition destination(Position(placeholder.get(), 0)); - if (paragraphStart == paragraphEnd && !lineBreakExistsAtPosition(paragraphStart)) { + if (paragraphStart == paragraphEnd && !lineBreakExistsAtVisiblePosition(paragraphStart)) { setEndingSelection(destination); return; } diff --git a/WebCore/editing/IndentOutdentCommand.cpp b/WebCore/editing/IndentOutdentCommand.cpp index 9444b11..0f9b106 100644 --- a/WebCore/editing/IndentOutdentCommand.cpp +++ b/WebCore/editing/IndentOutdentCommand.cpp @@ -200,7 +200,7 @@ void IndentOutdentCommand::outdentParagraph() VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote); - if (!enclosingNode) + if (!enclosingNode || !isContentEditable(enclosingNode->parentNode())) // We can't outdent if there is no place to go! return; // Use InsertListCommand to remove the selection from the list @@ -216,11 +216,24 @@ void IndentOutdentCommand::outdentParagraph() // The selection is inside a blockquote VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0)); VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock); - VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock); + VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount())); + VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock); if (visibleStartOfParagraph == startOfEnclosingBlock && visibleEndOfParagraph == endOfEnclosingBlock) { // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed. + Node* splitPoint = enclosingNode->nextSibling(); removeNodePreservingChildren(enclosingNode); + // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've + // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true + if (splitPoint) { + if (Node* splitPointParent = splitPoint->parentNode()) { + if (isIndentBlockquote(splitPointParent) + && !isIndentBlockquote(splitPoint) + && isContentEditable(splitPointParent->parentNode())) // We can't outdent if there is no place to go! + splitElement(static_cast<Element*>(splitPointParent), splitPoint); + } + } + updateLayout(); visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent()); visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent()); @@ -228,6 +241,7 @@ void IndentOutdentCommand::outdentParagraph() insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent()); if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph)) insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent()); + return; } Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph); diff --git a/WebCore/editing/InsertLineBreakCommand.cpp b/WebCore/editing/InsertLineBreakCommand.cpp index a5c588d..f020459 100644 --- a/WebCore/editing/InsertLineBreakCommand.cpp +++ b/WebCore/editing/InsertLineBreakCommand.cpp @@ -28,8 +28,8 @@ #include "CSSMutableStyleDeclaration.h" #include "Document.h" -#include "Element.h" #include "Frame.h" +#include "HTMLElement.h" #include "HTMLNames.h" #include "Range.h" #include "RenderObject.h" @@ -108,7 +108,7 @@ void InsertLineBreakCommand::doApply() // FIXME: Need to merge text nodes when inserting just after or before text. - if (isEndOfParagraph(caret) && !lineBreakExistsAtPosition(caret)) { + if (isEndOfParagraph(caret) && !lineBreakExistsAtVisiblePosition(caret)) { bool needExtraLineBreak = !pos.node()->hasTagName(hrTag) && !pos.node()->hasTagName(tableTag); insertNodeAt(nodeToInsert.get(), pos); @@ -118,7 +118,7 @@ void InsertLineBreakCommand::doApply() VisiblePosition endingPosition(Position(nodeToInsert.get(), 0)); setEndingSelection(VisibleSelection(endingPosition)); - } else if (pos.m_offset <= caretMinOffset(pos.node())) { + } else if (pos.deprecatedEditingOffset() <= caretMinOffset(pos.node())) { insertNodeAt(nodeToInsert.get(), pos); // Insert an extra br or '\n' if the just inserted one collapsed. @@ -128,7 +128,7 @@ void InsertLineBreakCommand::doApply() setEndingSelection(VisibleSelection(positionAfterNode(nodeToInsert.get()), DOWNSTREAM)); // If we're inserting after all of the rendered text in a text node, or into a non-text node, // a simple insertion is sufficient. - } else if (pos.m_offset >= caretMaxOffset(pos.node()) || !pos.node()->isTextNode()) { + } else if (pos.deprecatedEditingOffset() >= caretMaxOffset(pos.node()) || !pos.node()->isTextNode()) { insertNodeAt(nodeToInsert.get(), pos); setEndingSelection(VisibleSelection(positionAfterNode(nodeToInsert.get()), DOWNSTREAM)); } else { @@ -137,7 +137,7 @@ void InsertLineBreakCommand::doApply() // Do the split Text* textNode = static_cast<Text*>(pos.node()); - splitTextNode(textNode, pos.m_offset); + splitTextNode(textNode, pos.deprecatedEditingOffset()); insertNodeBefore(nodeToInsert, textNode); Position endingPosition = Position(textNode, 0); diff --git a/WebCore/editing/InsertListCommand.cpp b/WebCore/editing/InsertListCommand.cpp index aa48761..ec707d2 100644 --- a/WebCore/editing/InsertListCommand.cpp +++ b/WebCore/editing/InsertListCommand.cpp @@ -202,7 +202,8 @@ void InsertListCommand::doApply() } if (!listChildNode || switchListType || m_forceCreateList) { // Create list. - VisiblePosition start = startOfParagraph(endingSelection().visibleStart()); + VisiblePosition originalStart = endingSelection().visibleStart(); + VisiblePosition start = startOfParagraph(originalStart); VisiblePosition end = endOfParagraph(endingSelection().visibleEnd()); // Check for adjoining lists. @@ -251,12 +252,17 @@ void InsertListCommand::doApply() Node* listChild = enclosingListChild(insertionPos.node()); if (listChild && listChild->hasTagName(liTag)) insertionPos = positionBeforeNode(listChild); - + insertNodeAt(listElement, insertionPos); + + // We inserted the list at the start of the content we're about to move + // Update the start of content, so we don't try to move the list into itself. bug 19066 + if (insertionPos == start.deepEquivalent()) + start = startOfParagraph(originalStart); } moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true); if (nextList && previousList) - mergeIdenticalElements(static_cast<Element*>(previousList), static_cast<Element*>(nextList)); + mergeIdenticalElements(previousList, nextList); } } diff --git a/WebCore/editing/InsertParagraphSeparatorCommand.cpp b/WebCore/editing/InsertParagraphSeparatorCommand.cpp index 0e9c291..734d8fc 100644 --- a/WebCore/editing/InsertParagraphSeparatorCommand.cpp +++ b/WebCore/editing/InsertParagraphSeparatorCommand.cpp @@ -26,16 +26,17 @@ #include "config.h" #include "InsertParagraphSeparatorCommand.h" -#include "Document.h" -#include "Logging.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSPropertyNames.h" -#include "Text.h" -#include "htmlediting.h" +#include "Document.h" #include "HTMLElement.h" #include "HTMLNames.h" #include "InsertLineBreakCommand.h" +#include "Logging.h" #include "RenderObject.h" +#include "Text.h" +#include "htmlediting.h" #include "visible_units.h" namespace WebCore { @@ -164,13 +165,13 @@ void InsertParagraphSeparatorCommand::doApply() blockToInsert = createDefaultParagraphElement(document()); else blockToInsert = startBlock->cloneElementWithoutChildren(); - + //--------------------------------------------------------------------- // Handle case when position is in the last visible position in its block, // including when the block is empty. if (isLastInBlock) { if (nestNewBlock) { - if (isFirstInBlock && !lineBreakExistsAtPosition(visiblePos)) { + if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { // The block is empty. Create an empty block to // represent the paragraph that we're leaving. RefPtr<Element> extraBlock = createDefaultParagraphElement(document()); @@ -178,8 +179,12 @@ void InsertParagraphSeparatorCommand::doApply() appendBlockPlaceholder(extraBlock); } appendNode(blockToInsert, startBlock); - } else - insertNodeAfter(blockToInsert, startBlock); + } else { + // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it + // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. + Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote); + insertNodeAfter(blockToInsert, highestBlockquote ? highestBlockquote : startBlock); + } appendBlockPlaceholder(blockToInsert); setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM)); @@ -195,7 +200,7 @@ void InsertParagraphSeparatorCommand::doApply() if (isFirstInBlock && !nestNewBlock) refNode = startBlock; else if (insertionPosition.node() == startBlock && nestNewBlock) { - refNode = startBlock->childNode(insertionPosition.m_offset); + refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset()); ASSERT(refNode); // must be true or we'd be in the end of block case } else refNode = insertionPosition.node(); @@ -248,16 +253,16 @@ void InsertParagraphSeparatorCommand::doApply() if (leadingWhitespace.isNotNull()) { Text* textNode = static_cast<Text*>(leadingWhitespace.node()); ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); - replaceTextInNode(textNode, leadingWhitespace.m_offset, 1, nonBreakingSpaceString()); + replaceTextInNode(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); } // Split at pos if in the middle of a text node. if (insertionPosition.node()->isTextNode()) { Text* textNode = static_cast<Text*>(insertionPosition.node()); - bool atEnd = (unsigned)insertionPosition.m_offset >= textNode->length(); - if (insertionPosition.m_offset > 0 && !atEnd) { - splitTextNode(textNode, insertionPosition.m_offset); - insertionPosition.m_offset = 0; + bool atEnd = (unsigned)insertionPosition.deprecatedEditingOffset() >= textNode->length(); + if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { + splitTextNode(textNode, insertionPosition.deprecatedEditingOffset()); + insertionPosition.moveToOffset(0); visiblePos = VisiblePosition(insertionPosition); splitText = true; } @@ -282,13 +287,13 @@ void InsertParagraphSeparatorCommand::doApply() // 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) && !lineBreakExistsAtPosition(visiblePos)) + if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) appendNode(createBreakElement(document()).get(), blockToInsert.get()); // Move the start node and the siblings of the start node. if (insertionPosition.node() != startBlock) { Node* n = insertionPosition.node(); - if (insertionPosition.m_offset >= caretMaxOffset(n)) + if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n)) n = n->nextSibling(); while (n && n != blockToInsert) { diff --git a/WebCore/editing/InsertTextCommand.cpp b/WebCore/editing/InsertTextCommand.cpp index 52da5d0..bf6fd19 100644 --- a/WebCore/editing/InsertTextCommand.cpp +++ b/WebCore/editing/InsertTextCommand.cpp @@ -89,9 +89,9 @@ bool InsertTextCommand::performTrivialReplace(const String& text, bool selectIns if (start.node() != end.node() || !start.node()->isTextNode() || isTabSpanTextNode(start.node())) return false; - replaceTextInNode(static_cast<Text*>(start.node()), start.m_offset, end.m_offset - start.m_offset, text); + replaceTextInNode(static_cast<Text*>(start.node()), start.deprecatedEditingOffset(), end.deprecatedEditingOffset() - start.deprecatedEditingOffset(), text); - Position endPosition(start.node(), start.m_offset + text.length()); + Position endPosition(start.node(), start.deprecatedEditingOffset() + text.length()); // We could have inserted a part of composed character sequence, // so we are basically treating ending selection as a range to avoid validation. @@ -130,8 +130,27 @@ void InsertTextCommand::input(const String& originalText, bool selectInsertedTex deleteSelection(false, true, true, false); } + Position startPosition(endingSelection().start()); + + Position placeholder; + // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content + // is inserted just before them. + // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. + // If the caret is just before a placeholder, downstream will normalize the caret to it. + Position downstream(startPosition.downstream()); + if (lineBreakExistsAtPosition(downstream)) { + // FIXME: This doesn't handle placeholders at the end of anonymous blocks. + VisiblePosition caret(startPosition); + if (isEndOfBlock(caret) && isStartOfParagraph(caret)) + placeholder = downstream; + // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before + // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires + // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. + } + // Insert the character at the leftmost candidate. - Position startPosition = endingSelection().start().upstream(); + startPosition = startPosition.upstream(); + // It is possible for the node that contains startPosition to contain only unrendered whitespace, // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. Position positionBeforeStartNode(positionBeforeNode(startPosition.node())); @@ -148,14 +167,16 @@ void InsertTextCommand::input(const String& originalText, bool selectInsertedTex if (text == "\t") { endPosition = insertTab(startPosition); startPosition = endPosition.previous(); - removePlaceholderAt(VisiblePosition(startPosition)); + if (placeholder.isNotNull()) + removePlaceholderAt(placeholder); m_charactersAdded += 1; } else { // Make sure the document is set up to receive text startPosition = prepareForTextInsertion(startPosition); - removePlaceholderAt(VisiblePosition(startPosition)); + if (placeholder.isNotNull()) + removePlaceholderAt(placeholder); Text *textNode = static_cast<Text *>(startPosition.node()); - int offset = startPosition.m_offset; + int offset = startPosition.deprecatedEditingOffset(); insertTextIntoNode(textNode, offset, text); endPosition = Position(textNode, offset + text.length()); @@ -207,7 +228,7 @@ Position InsertTextCommand::insertTab(const Position& pos) Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent(); Node *node = insertPos.node(); - unsigned int offset = insertPos.m_offset; + unsigned int offset = insertPos.deprecatedEditingOffset(); // keep tabs coalesced in tab span if (isTabSpanTextNode(node)) { diff --git a/WebCore/editing/ModifySelectionListLevel.cpp b/WebCore/editing/ModifySelectionListLevel.cpp index 5ea658c..9a7e105 100644 --- a/WebCore/editing/ModifySelectionListLevel.cpp +++ b/WebCore/editing/ModifySelectionListLevel.cpp @@ -27,8 +27,8 @@ #include "ModifySelectionListLevel.h" #include "Document.h" -#include "Element.h" #include "Frame.h" +#include "HTMLElement.h" #include "RenderObject.h" #include "SelectionController.h" #include "htmlediting.h" diff --git a/WebCore/editing/MoveSelectionCommand.cpp b/WebCore/editing/MoveSelectionCommand.cpp index 2c656e7..0a2d3f4 100644 --- a/WebCore/editing/MoveSelectionCommand.cpp +++ b/WebCore/editing/MoveSelectionCommand.cpp @@ -48,14 +48,14 @@ void MoveSelectionCommand::doApply() // Update the position otherwise it may become invalid after the selection is deleted. Node *positionNode = m_position.node(); - int positionOffset = m_position.m_offset; + int positionOffset = m_position.deprecatedEditingOffset(); Position selectionEnd = selection.end(); - int selectionEndOffset = selectionEnd.m_offset; + int selectionEndOffset = selectionEnd.deprecatedEditingOffset(); if (selectionEnd.node() == positionNode && selectionEndOffset < positionOffset) { positionOffset -= selectionEndOffset; Position selectionStart = selection.start(); if (selectionStart.node() == positionNode) { - positionOffset += selectionStart.m_offset; + positionOffset += selectionStart.deprecatedEditingOffset(); } pos = Position(positionNode, positionOffset); } diff --git a/WebCore/editing/RemoveCSSPropertyCommand.h b/WebCore/editing/RemoveCSSPropertyCommand.h index fd81307..836f9d7 100644 --- a/WebCore/editing/RemoveCSSPropertyCommand.h +++ b/WebCore/editing/RemoveCSSPropertyCommand.h @@ -27,6 +27,7 @@ #define RemoveCSSPropertyCommand_h #include "EditCommand.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSPropertyNames.h" namespace WebCore { diff --git a/WebCore/editing/RemoveFormatCommand.cpp b/WebCore/editing/RemoveFormatCommand.cpp index 609ab0e..6d681ee 100644 --- a/WebCore/editing/RemoveFormatCommand.cpp +++ b/WebCore/editing/RemoveFormatCommand.cpp @@ -27,6 +27,7 @@ #include "RemoveFormatCommand.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "Editor.h" #include "Frame.h" #include "HTMLNames.h" diff --git a/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp b/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp index 98f4282..1452f88 100644 --- a/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp +++ b/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp @@ -32,7 +32,8 @@ namespace WebCore { RemoveNodePreservingChildrenCommand::RemoveNodePreservingChildrenCommand(PassRefPtr<Node> node) - : CompositeEditCommand(node->document()), m_node(node) + : CompositeEditCommand(node->document()) + , m_node(node) { ASSERT(m_node); } diff --git a/WebCore/editing/ReplaceNodeWithSpanCommand.cpp b/WebCore/editing/ReplaceNodeWithSpanCommand.cpp new file mode 100644 index 0000000..21ca924 --- /dev/null +++ b/WebCore/editing/ReplaceNodeWithSpanCommand.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009 Google 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER 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 "ReplaceNodeWithSpanCommand.h" + +#include "htmlediting.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "NamedAttrMap.h" + +#include <wtf/Assertions.h> + +namespace WebCore { + +using namespace HTMLNames; + +ReplaceNodeWithSpanCommand::ReplaceNodeWithSpanCommand(PassRefPtr<Node> node) + : CompositeEditCommand(node->document()) + , m_node(node) +{ + ASSERT(m_node); +} + +static void swapInNodePreservingAttributesAndChildren(Node* newNode, Node* nodeToReplace) +{ + ASSERT(nodeToReplace->inDocument()); + ExceptionCode ec = 0; + Node* parentNode = nodeToReplace->parentNode(); + parentNode->insertBefore(newNode, nodeToReplace, ec); + ASSERT(!ec); + + for (Node* child = nodeToReplace->firstChild(); child; child = child->nextSibling()) { + newNode->appendChild(child, ec); + ASSERT(!ec); + } + + newNode->attributes()->setAttributes(*nodeToReplace->attributes()); + + parentNode->removeChild(nodeToReplace, ec); + ASSERT(!ec); +} + +void ReplaceNodeWithSpanCommand::doApply() +{ + if (!m_node->inDocument()) + return; + if (!m_spanElement) + m_spanElement = createHTMLElement(m_node->document(), spanTag); + swapInNodePreservingAttributesAndChildren(m_spanElement.get(), m_node.get()); +} + +void ReplaceNodeWithSpanCommand::doUnapply() +{ + if (!m_spanElement->inDocument()) + return; + swapInNodePreservingAttributesAndChildren(m_node.get(), m_spanElement.get()); +} + +} // namespace WebCore diff --git a/WebCore/editing/ReplaceNodeWithSpanCommand.h b/WebCore/editing/ReplaceNodeWithSpanCommand.h new file mode 100644 index 0000000..7b375b6 --- /dev/null +++ b/WebCore/editing/ReplaceNodeWithSpanCommand.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2009 Google 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER 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 ReplaceNodeWithSpanCommand_h +#define ReplaceNodeWithSpanCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class HTMLElement; + +// More accurately, this is ReplaceNodeWithSpanPreservingChildrenAndAttributesCommand +class ReplaceNodeWithSpanCommand : public CompositeEditCommand { +public: + static PassRefPtr<ReplaceNodeWithSpanCommand> create(PassRefPtr<Node> node) + { + return adoptRef(new ReplaceNodeWithSpanCommand(node)); + } + + HTMLElement* spanElement() { return m_spanElement.get(); } + +private: + ReplaceNodeWithSpanCommand(PassRefPtr<Node>); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Node> m_node; + RefPtr<HTMLElement> m_spanElement; +}; + +} // namespace WebCore + +#endif // ReplaceNodeWithSpanCommand diff --git a/WebCore/editing/ReplaceSelectionCommand.cpp b/WebCore/editing/ReplaceSelectionCommand.cpp index 09ad985..c6da864 100644 --- a/WebCore/editing/ReplaceSelectionCommand.cpp +++ b/WebCore/editing/ReplaceSelectionCommand.cpp @@ -28,20 +28,21 @@ #include "ApplyStyleCommand.h" #include "BeforeTextInsertedEvent.h" -#include "BreakBlockquoteCommand.h" +#include "BreakBlockquoteCommand.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSProperty.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" #include "Document.h" #include "DocumentFragment.h" #include "EditingText.h" -#include "EventNames.h" #include "Element.h" +#include "EventNames.h" #include "Frame.h" #include "HTMLElement.h" -#include "HTMLInterchange.h" #include "HTMLInputElement.h" +#include "HTMLInterchange.h" #include "HTMLNames.h" #include "SelectionController.h" #include "SmartReplace.h" @@ -124,7 +125,7 @@ ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* f Node* shadowAncestorNode = editableRoot->shadowAncestorNode(); - if (!editableRoot->inlineEventListenerForType(eventNames().webkitBeforeTextInsertedEvent) && + if (!editableRoot->getAttributeEventListener(eventNames().webkitBeforeTextInsertedEvent) && // FIXME: Remove these checks once textareas and textfields actually register an event handler. !(shadowAncestorNode && shadowAncestorNode->renderer() && shadowAncestorNode->renderer()->isTextControl()) && editableRoot->isContentRichlyEditable()) { @@ -343,14 +344,22 @@ static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisibleP return isInsideMailBlockquote && (numEnclosingMailBlockquotes(existing) == numEnclosingMailBlockquotes(inserted)); } -bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfParagraph, bool fragmentHasInterchangeNewlineAtStart) +bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfParagraph, bool fragmentHasInterchangeNewlineAtStart, bool selectionStartWasInsideMailBlockquote) { + if (m_movingParagraph) + return false; + VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); VisiblePosition prev = startOfInsertedContent.previous(true); if (prev.isNull()) return false; - if (!m_movingParagraph && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) + // When we have matching quote levels, its ok to merge more frequently. + // For a successful merge, we still need to make sure that the inserted content starts with the beginning of a paragraph. + // And we should only merge here if the selection start was inside a mail blockquote. This prevents against removing a + // blockquote from newly pasted quoted content that was pasted into an unquoted position. If that unquoted position happens + // to be right after another blockquote, we don't want to merge and risk stripping a valid block (and newline) from the pasted content. + if (isStartOfParagraph(startOfInsertedContent) && selectionStartWasInsideMailBlockquote && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) return true; return !selectionStartWasStartOfParagraph && @@ -690,7 +699,8 @@ void ReplaceSelectionCommand::mergeEndIfNeeded() moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove), destination); // Merging forward will remove m_lastLeafInserted from the document. // FIXME: Maintain positions for the start and end of inserted content instead of keeping nodes. The nodes are - // only ever used to create positions where inserted content starts/ends. + // only ever used to create positions where inserted content starts/ends. Also, we sometimes insert content + // directly into text nodes already in the document, in which case tracking inserted nodes is inadequate. if (mergeForward) { m_lastLeafInserted = destination.previous().deepEquivalent().node(); if (!m_firstNodeInserted->inDocument()) @@ -711,6 +721,9 @@ void ReplaceSelectionCommand::doApply() Element* currentRoot = selection.rootEditableElement(); ReplacementFragment fragment(document(), m_documentFragment.get(), m_matchStyle, selection); + if (performTrivialReplace(fragment)) + return; + if (m_matchStyle) m_insertionStyle = styleAtPosition(selection.start()); @@ -772,9 +785,10 @@ void ReplaceSelectionCommand::doApply() insertionPos = endingSelection().start(); } - if (startIsInsideMailBlockquote && m_preventNesting) { - // We don't want any of the pasted content to end up nested in a Mail blockquote, so first break - // out of any surrounding Mail blockquotes. + // We don't want any of the pasted content to end up nested in a Mail blockquote, so first break + // out of any surrounding Mail blockquotes. Unless we're inserting in a table, in which case + // breaking the blockquote will prevent the content from actually being inserted in the table. + if (startIsInsideMailBlockquote && m_preventNesting && !(enclosingNodeOfType(insertionPos, &isTableStructureNode))) { applyCommandToComposite(BreakBlockquoteCommand::create(document())); // This will leave a br between the split. Node* br = endingSelection().start().node(); @@ -821,6 +835,9 @@ void ReplaceSelectionCommand::doApply() bool handledStyleSpans = handleStyleSpansBeforeInsertion(fragment, insertionPos); + // FIXME: When pasting rich content we're often prevented from heading down the fast path by style spans. Try + // again here if they've been removed. + // We're finished if there is nothing to add. if (fragment.isEmpty() || !fragment.firstChild()) return; @@ -876,7 +893,7 @@ void ReplaceSelectionCommand::doApply() // We inserted before the startBlock to prevent nesting, and the content before the startBlock wasn't in its own block and // didn't have a br after it, so the inserted content ended up in the same paragraph. - if (startBlock && insertionPos.node() == startBlock->parentNode() && (unsigned)insertionPos.m_offset < startBlock->nodeIndex() && !isStartOfParagraph(startOfInsertedContent)) + if (startBlock && insertionPos.node() == startBlock->parentNode() && (unsigned)insertionPos.deprecatedEditingOffset() < startBlock->nodeIndex() && !isStartOfParagraph(startOfInsertedContent)) insertNodeAt(createBreakElement(document()).get(), startOfInsertedContent.deepEquivalent()); Position lastPositionToSelect; @@ -890,13 +907,7 @@ void ReplaceSelectionCommand::doApply() // the start merge so that the start merge doesn't effect our decision. m_shouldMergeEnd = shouldMergeEnd(selectionEndWasEndOfParagraph); - if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasInterchangeNewlineAtStart())) { - // Bail to avoid infinite recursion. - if (m_movingParagraph) { - // setting display:inline does not work for td elements in quirks mode - ASSERT(m_firstNodeInserted->hasTagName(tdTag)); - return; - } + if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasInterchangeNewlineAtStart(), startIsInsideMailBlockquote)) { VisiblePosition destination = startOfInsertedContent.previous(); VisiblePosition startOfParagraphToMove = startOfInsertedContent; @@ -1092,4 +1103,38 @@ void ReplaceSelectionCommand::updateNodesInserted(Node *node) m_lastLeafInserted = node->lastDescendant(); } +// During simple pastes, where we're just pasting a text node into a run of text, we insert the text node +// directly into the text node that holds the selection. This is much faster than the generalized code in +// ReplaceSelectionCommand, and works around <https://bugs.webkit.org/show_bug.cgi?id=6148> since we don't +// split text nodes. +bool ReplaceSelectionCommand::performTrivialReplace(const ReplacementFragment& fragment) +{ + if (!fragment.firstChild() || fragment.firstChild() != fragment.lastChild() || !fragment.firstChild()->isTextNode()) + return false; + + // FIXME: Would be nice to handle smart replace in the fast path. + if (m_smartReplace || fragment.hasInterchangeNewlineAtStart() || fragment.hasInterchangeNewlineAtEnd()) + return false; + + Text* textNode = static_cast<Text*>(fragment.firstChild()); + // Our fragment creation code handles tabs, spaces, and newlines, so we don't have to worry about those here. + String text(textNode->data()); + + Position start = endingSelection().start(); + Position end = endingSelection().end(); + + if (start.anchorNode() != end.anchorNode() || !start.anchorNode()->isTextNode()) + return false; + + replaceTextInNode(static_cast<Text*>(start.anchorNode()), start.offsetInContainerNode(), end.offsetInContainerNode() - start.offsetInContainerNode(), text); + + end = Position(start.anchorNode(), start.offsetInContainerNode() + text.length()); + + VisibleSelection selectionAfterReplace(m_selectReplacement ? start : end, end); + + setEndingSelection(selectionAfterReplace); + + return true; +} + } // namespace WebCore diff --git a/WebCore/editing/ReplaceSelectionCommand.h b/WebCore/editing/ReplaceSelectionCommand.h index 18dffa5..1cb93c3 100644 --- a/WebCore/editing/ReplaceSelectionCommand.h +++ b/WebCore/editing/ReplaceSelectionCommand.h @@ -31,6 +31,7 @@ namespace WebCore { class DocumentFragment; +class ReplacementFragment; class ReplaceSelectionCommand : public CompositeEditCommand { public: @@ -57,7 +58,7 @@ private: void updateNodesInserted(Node*); bool shouldRemoveEndBR(Node*, const VisiblePosition&); - bool shouldMergeStart(bool, bool); + bool shouldMergeStart(bool, bool, bool); bool shouldMergeEnd(bool selectEndWasEndOfParagraph); bool shouldMerge(const VisiblePosition&, const VisiblePosition&); @@ -74,6 +75,8 @@ private: VisiblePosition positionAtStartOfInsertedContent(); VisiblePosition positionAtEndOfInsertedContent(); + + bool performTrivialReplace(const ReplacementFragment&); RefPtr<Node> m_firstNodeInserted; RefPtr<Node> m_lastLeafInserted; diff --git a/WebCore/editing/SelectionController.cpp b/WebCore/editing/SelectionController.cpp index d606891..d0427c0 100644 --- a/WebCore/editing/SelectionController.cpp +++ b/WebCore/editing/SelectionController.cpp @@ -32,7 +32,6 @@ #include "Editor.h" #include "Element.h" #include "EventHandler.h" -#include "EventNames.h" #include "ExceptionCode.h" #include "FocusController.h" #include "FloatQuad.h" @@ -70,7 +69,7 @@ SelectionController::SelectionController(Frame* frame, bool isDragCaretControlle , m_lastChangeWasHorizontalExtension(false) , m_isDragCaretController(isDragCaretController) , m_isCaretBlinkingSuspended(false) - , m_focused(false) + , m_focused(frame && frame->page() && frame->page()->focusController()->focusedFrame() == frame) { } @@ -203,8 +202,7 @@ void SelectionController::nodeWillBeRemoved(Node *node) else m_sel.setWithoutValidation(m_sel.end(), m_sel.start()); // FIXME: This could be more efficient if we had an isNodeInRange function on Ranges. - } else if (Range::compareBoundaryPoints(m_sel.start(), Position(node, 0)) == -1 && - Range::compareBoundaryPoints(m_sel.end(), Position(node, 0)) == 1) { + } else if (comparePositions(m_sel.start(), Position(node, 0)) == -1 && comparePositions(m_sel.end(), Position(node, 0)) == 1) { // 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 // the removal wouldn't be invalidated. @@ -214,7 +212,7 @@ void SelectionController::nodeWillBeRemoved(Node *node) if (clearRenderTreeSelection) { RefPtr<Document> document = m_sel.start().node()->document(); - document->updateRendering(); + document->updateStyleIfNeeded(); if (RenderView* view = toRenderView(document->renderer())) view->clearSelection(); } @@ -252,7 +250,53 @@ void SelectionController::willBeModified(EAlteration alter, EDirection direction } } -VisiblePosition SelectionController::modifyExtendingRightForward(TextGranularity granularity) +TextDirection SelectionController::directionOfEnclosingBlock() { + Node* n = m_sel.extent().node(); + Node* enclosingBlockNode = enclosingBlock(n); + if (!enclosingBlockNode) + return LTR; + RenderObject* renderer = enclosingBlockNode->renderer(); + if (renderer) + return renderer->style()->direction(); + return LTR; +} + +VisiblePosition SelectionController::modifyExtendingRight(TextGranularity granularity) +{ + VisiblePosition pos(m_sel.extent(), m_sel.affinity()); + + // The difference between modifyExtendingRight and modifyExtendingForward is: + // modifyExtendingForward always extends forward logically. + // modifyExtendingRight behaves the same as modifyExtendingForward except for extending character or word, + // it extends forward logically if the enclosing block is LTR direction, + // but it extends backward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.next(true); + else + pos = pos.previous(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = nextWordPosition(pos); + else + pos = previousWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: implement all of the above? + pos = modifyExtendingForward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingForward(TextGranularity granularity) { VisiblePosition pos(m_sel.extent(), m_sel.affinity()); switch (granularity) { @@ -275,7 +319,7 @@ VisiblePosition SelectionController::modifyExtendingRightForward(TextGranularity pos = endOfSentence(VisiblePosition(m_sel.end(), m_sel.affinity())); break; case LineBoundary: - pos = endOfLine(VisiblePosition(m_sel.end(), m_sel.affinity())); + pos = logicalEndOfLine(VisiblePosition(m_sel.end(), m_sel.affinity())); break; case ParagraphBoundary: pos = endOfParagraph(VisiblePosition(m_sel.end(), m_sel.affinity())); @@ -349,7 +393,7 @@ VisiblePosition SelectionController::modifyMovingForward(TextGranularity granula pos = endOfSentence(VisiblePosition(m_sel.end(), m_sel.affinity())); break; case LineBoundary: - pos = endOfLine(VisiblePosition(m_sel.end(), m_sel.affinity())); + pos = logicalEndOfLine(VisiblePosition(m_sel.end(), m_sel.affinity())); break; case ParagraphBoundary: pos = endOfParagraph(VisiblePosition(m_sel.end(), m_sel.affinity())); @@ -366,10 +410,44 @@ VisiblePosition SelectionController::modifyMovingForward(TextGranularity granula return pos; } -VisiblePosition SelectionController::modifyExtendingLeftBackward(TextGranularity granularity) +VisiblePosition SelectionController::modifyExtendingLeft(TextGranularity granularity) { VisiblePosition pos(m_sel.extent(), m_sel.affinity()); - + + // The difference between modifyExtendingLeft and modifyExtendingBackward is: + // modifyExtendingBackward always extends backward logically. + // modifyExtendingLeft behaves the same as modifyExtendingBackward except for extending character or word, + // it extends backward logically if the enclosing block is LTR direction, + // but it extends forward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.previous(true); + else + pos = pos.next(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = previousWordPosition(pos); + else + pos = nextWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + pos = modifyExtendingBackward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingBackward(TextGranularity granularity) +{ + VisiblePosition pos(m_sel.extent(), m_sel.affinity()); + // Extending a selection backward by word or character from just after a table selects // the table. This "makes sense" from the user perspective, esp. when deleting. // It was done here instead of in VisiblePosition because we want VPs to iterate @@ -394,7 +472,7 @@ VisiblePosition SelectionController::modifyExtendingLeftBackward(TextGranularity pos = startOfSentence(VisiblePosition(m_sel.start(), m_sel.affinity())); break; case LineBoundary: - pos = startOfLine(VisiblePosition(m_sel.start(), m_sel.affinity())); + pos = logicalStartOfLine(VisiblePosition(m_sel.start(), m_sel.affinity())); break; case ParagraphBoundary: pos = startOfParagraph(VisiblePosition(m_sel.start(), m_sel.affinity())); @@ -461,7 +539,7 @@ VisiblePosition SelectionController::modifyMovingBackward(TextGranularity granul pos = startOfSentence(VisiblePosition(m_sel.start(), m_sel.affinity())); break; case LineBoundary: - pos = startOfLine(VisiblePosition(m_sel.start(), m_sel.affinity())); + pos = logicalStartOfLine(VisiblePosition(m_sel.start(), m_sel.affinity())); break; case ParagraphBoundary: pos = startOfParagraph(VisiblePosition(m_sel.start(), m_sel.affinity())); @@ -501,11 +579,11 @@ bool SelectionController::modify(EAlteration alter, EDirection dir, TextGranular if (alter == MOVE) pos = modifyMovingRight(granularity); else - pos = modifyExtendingRightForward(granularity); + pos = modifyExtendingRight(granularity); break; case FORWARD: if (alter == EXTEND) - pos = modifyExtendingRightForward(granularity); + pos = modifyExtendingForward(granularity); else pos = modifyMovingForward(granularity); break; @@ -513,11 +591,11 @@ bool SelectionController::modify(EAlteration alter, EDirection dir, TextGranular if (alter == MOVE) pos = modifyMovingLeft(granularity); else - pos = modifyExtendingLeftBackward(granularity); + pos = modifyExtendingLeft(granularity); break; case BACKWARD: if (alter == EXTEND) - pos = modifyExtendingLeftBackward(granularity); + pos = modifyExtendingBackward(granularity); else pos = modifyMovingBackward(granularity); break; @@ -735,7 +813,7 @@ void SelectionController::layout() return; } - m_sel.start().node()->document()->updateRendering(); + m_sel.start().node()->document()->updateStyleIfNeeded(); m_caretRect = IntRect(); @@ -796,11 +874,20 @@ RenderObject* SelectionController::caretRenderer() const IntRect SelectionController::localCaretRect() const { if (m_needsLayout) - const_cast<SelectionController *>(this)->layout(); + const_cast<SelectionController*>(this)->layout(); return m_caretRect; } +IntRect SelectionController::absoluteBoundsForLocalRect(const IntRect& rect) const +{ + RenderObject* caretPainter = caretRenderer(); + if (!caretPainter) + return IntRect(); + + return caretPainter->localToAbsoluteQuad(FloatRect(rect)).enclosingBoundingBox(); +} + IntRect SelectionController::absoluteCaretBounds() { recomputeCaretRect(); @@ -819,13 +906,7 @@ static IntRect repaintRectForCaret(IntRect caret) IntRect SelectionController::caretRepaintRect() const { - IntRect localRect = repaintRectForCaret(localCaretRect()); - - RenderObject* caretPainter = caretRenderer(); - if (caretPainter) - return caretPainter->localToAbsoluteQuad(FloatRect(localRect)).enclosingBoundingBox(); - - return IntRect(); + return absoluteBoundsForLocalRect(repaintRectForCaret(localCaretRect())); } bool SelectionController::recomputeCaretRect() @@ -841,22 +922,26 @@ bool SelectionController::recomputeCaretRect() return false; IntRect oldRect = m_caretRect; - m_needsLayout = true; IntRect newRect = localCaretRect(); if (oldRect == newRect && !m_absCaretBoundsDirty) return false; - IntRect oldAbsRepaintRect = m_absCaretBounds; - m_absCaretBounds = caretRepaintRect(); + IntRect oldAbsCaretBounds = m_absCaretBounds; + // FIXME: Rename m_caretRect to m_localCaretRect. + m_absCaretBounds = absoluteBoundsForLocalRect(m_caretRect); m_absCaretBoundsDirty = false; - if (oldAbsRepaintRect == m_absCaretBounds) + if (oldAbsCaretBounds == m_absCaretBounds) return false; + + IntRect oldAbsoluteCaretRepaintBounds = m_absoluteCaretRepaintBounds; + // We believe that we need to inflate the local rect before transforming it to obtain the repaint bounds. + m_absoluteCaretRepaintBounds = caretRepaintRect(); if (RenderView* view = toRenderView(m_frame->document()->renderer())) { // FIXME: make caret repainting container-aware. - view->repaintRectangleInViewAndCompositedLayers(oldAbsRepaintRect, false); - view->repaintRectangleInViewAndCompositedLayers(m_absCaretBounds, false); + view->repaintRectangleInViewAndCompositedLayers(oldAbsoluteCaretRepaintBounds, false); + view->repaintRectangleInViewAndCompositedLayers(m_absoluteCaretRepaintBounds, false); } return true; @@ -932,9 +1017,9 @@ void SelectionController::debugRenderer(RenderObject *r, bool selected) const if (selected) { int offset = 0; if (r->node() == m_sel.start().node()) - offset = m_sel.start().m_offset; + offset = m_sel.start().deprecatedEditingOffset(); else if (r->node() == m_sel.end().node()) - offset = m_sel.end().m_offset; + offset = m_sel.end().deprecatedEditingOffset(); int pos; InlineTextBox *box = textRenderer->findNextInlineTextBox(offset, pos); @@ -1179,7 +1264,7 @@ void SelectionController::focusedOrActiveStateChanged() // RenderTheme::isFocused() check if the frame is active, we have to // update style and theme state that depended on those. if (Node* node = m_frame->document()->focusedNode()) { - node->setChanged(); + node->setNeedsStyleRecalc(); if (RenderObject* renderer = node->renderer()) if (renderer && renderer->style()->hasAppearance()) theme()->stateChanged(renderer, FocusState); @@ -1202,8 +1287,6 @@ void SelectionController::setFocused(bool flag) m_focused = flag; focusedOrActiveStateChanged(); - - m_frame->document()->dispatchWindowEvent(flag ? eventNames().focusEvent : eventNames().blurEvent, false, false); } bool SelectionController::isFocusedAndActive() const diff --git a/WebCore/editing/SelectionController.h b/WebCore/editing/SelectionController.h index 21e849d..bbd343c 100644 --- a/WebCore/editing/SelectionController.h +++ b/WebCore/editing/SelectionController.h @@ -102,6 +102,7 @@ public: bool isRange() const { return m_sel.isRange(); } bool isCaretOrRange() const { return m_sel.isCaretOrRange(); } bool isInPasswordField() const; + bool isAll(StayInEditableContent stayInEditableContent = MustStayInEditableContent) const { return m_sel.isAll(stayInEditableContent); } PassRefPtr<Range> toNormalizedRange() const { return m_sel.toNormalizedRange(); } @@ -119,6 +120,7 @@ public: // Focus void setFocused(bool); + bool isFocused() const { return m_focused; } bool isFocusedAndActive() const; void pageActivationChanged(); @@ -130,10 +132,14 @@ public: private: enum EPositionType { START, END, BASE, EXTENT }; - VisiblePosition modifyExtendingRightForward(TextGranularity); + TextDirection directionOfEnclosingBlock(); + + VisiblePosition modifyExtendingRight(TextGranularity); + VisiblePosition modifyExtendingForward(TextGranularity); VisiblePosition modifyMovingRight(TextGranularity); VisiblePosition modifyMovingForward(TextGranularity); - VisiblePosition modifyExtendingLeftBackward(TextGranularity); + VisiblePosition modifyExtendingLeft(TextGranularity); + VisiblePosition modifyExtendingBackward(TextGranularity); VisiblePosition modifyMovingLeft(TextGranularity); VisiblePosition modifyMovingBackward(TextGranularity); @@ -142,7 +148,7 @@ private: int xPosForVerticalArrowNavigation(EPositionType); -#if PLATFORM(MAC) +#if PLATFORM(MAC) || PLATFORM(GTK) void notifyAccessibilityForSelectionChange(); #else void notifyAccessibilityForSelectionChange() {}; @@ -150,6 +156,8 @@ private: void focusedOrActiveStateChanged(); bool caretRendersInsideNode(Node*) const; + + IntRect absoluteBoundsForLocalRect(const IntRect&) const; Frame* m_frame; int m_xPosForVerticalArrowNavigation; @@ -158,6 +166,7 @@ private: IntRect m_caretRect; // caret rect in coords local to the renderer responsible for painting the caret IntRect m_absCaretBounds; // absolute bounding rect for the caret + IntRect m_absoluteCaretRepaintBounds; bool m_needsLayout : 1; // true if the caret and expectedVisible rectangles need to be calculated bool m_absCaretBoundsDirty: 1; @@ -187,3 +196,4 @@ void showTree(const WebCore::SelectionController*); #endif #endif // SelectionController_h + diff --git a/WebCore/editing/TextAffinity.h b/WebCore/editing/TextAffinity.h index 5562cc4..a5565c7 100644 --- a/WebCore/editing/TextAffinity.h +++ b/WebCore/editing/TextAffinity.h @@ -38,20 +38,22 @@ namespace WebCore { // From NSTextView.h: // NSSelectionAffinityUpstream = 0 // NSSelectionAffinityDownstream = 1 -typedef enum { UPSTREAM = 0, DOWNSTREAM = 1 } EAffinity; +enum EAffinity { UPSTREAM = 0, DOWNSTREAM = 1 }; + +} // namespace WebCore #ifdef __OBJC__ -inline NSSelectionAffinity kit(EAffinity affinity) + +inline NSSelectionAffinity kit(WebCore::EAffinity affinity) { return static_cast<NSSelectionAffinity>(affinity); } -inline EAffinity core(NSSelectionAffinity affinity) +inline WebCore::EAffinity core(NSSelectionAffinity affinity) { - return static_cast<EAffinity>(affinity); + return static_cast<WebCore::EAffinity>(affinity); } -#endif -} // namespace WebCore +#endif #endif // TextAffinity_h diff --git a/WebCore/editing/TextIterator.cpp b/WebCore/editing/TextIterator.cpp index 2d7e641..b311853 100644 --- a/WebCore/editing/TextIterator.cpp +++ b/WebCore/editing/TextIterator.cpp @@ -29,7 +29,7 @@ #include "CharacterNames.h" #include "Document.h" -#include "Element.h" +#include "HTMLElement.h" #include "HTMLNames.h" #include "htmlediting.h" #include "InlineTextBox.h" @@ -103,6 +103,8 @@ TextIterator::TextIterator() , m_endContainer(0) , m_endOffset(0) , m_positionNode(0) + , m_textCharacters(0) + , m_textLength(0) , m_lastCharacter(0) , m_emitCharactersBetweenAllVisiblePositions(false) , m_enterTextControls(false) @@ -116,6 +118,8 @@ TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisibleP , m_endContainer(0) , m_endOffset(0) , m_positionNode(0) + , m_textCharacters(0) + , m_textLength(0) , m_emitCharactersBetweenAllVisiblePositions(emitCharactersBetweenAllVisiblePositions) , m_enterTextControls(enterTextControls) { @@ -609,11 +613,13 @@ bool TextIterator::shouldRepresentNodeOffsetZero() if (!m_node->renderer() || m_node->renderer()->style()->visibility() != VISIBLE) return false; - // The currPos.isNotNull() check is needed because positions in non-html content - // (like svg) do not have visible positions, and we don't want to emit for them either. + // The startPos.isNotNull() check is needed because the start could be before the body, + // and in that case we'll get null. We don't want to put in newlines at the start in that case. + // The currPos.isNotNull() check is needed because positions in non-HTML content + // (like SVG) do not have visible positions, and we don't want to emit for them either. VisiblePosition startPos = VisiblePosition(m_startContainer, m_startOffset, DOWNSTREAM); VisiblePosition currPos = VisiblePosition(m_node, 0, DOWNSTREAM); - return currPos.isNotNull() && !inSameLine(startPos, currPos); + return startPos.isNotNull() && currPos.isNotNull() && !inSameLine(startPos, currPos); } bool TextIterator::shouldEmitSpaceBeforeAndAfterNode(Node* node) @@ -1558,7 +1564,7 @@ PassRefPtr<Range> TextIterator::rangeFromLocationAndLength(Element *scope, int r Position runEnd = VisiblePosition(runStart).next().deepEquivalent(); if (runEnd.isNotNull()) { ExceptionCode ec = 0; - textRunRange->setEnd(runEnd.node(), runEnd.m_offset, ec); + textRunRange->setEnd(runEnd.node(), runEnd.deprecatedEditingOffset(), ec); ASSERT(!ec); } } diff --git a/WebCore/editing/TypingCommand.cpp b/WebCore/editing/TypingCommand.cpp index 6235f7a..5ce4e56 100644 --- a/WebCore/editing/TypingCommand.cpp +++ b/WebCore/editing/TypingCommand.cpp @@ -33,6 +33,7 @@ #include "Editor.h" #include "Element.h" #include "Frame.h" +#include "HTMLNames.h" #include "InsertLineBreakCommand.h" #include "InsertParagraphSeparatorCommand.h" #include "InsertTextCommand.h" @@ -44,6 +45,8 @@ namespace WebCore { +using namespace HTMLNames; + TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, bool selectInsertedText, TextGranularity granularity, bool killRing) : CompositeEditCommand(document), m_commandType(commandType), @@ -281,8 +284,17 @@ EditAction TypingCommand::editingAction() const void TypingCommand::markMisspellingsAfterTyping() { +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled() + && !document()->frame()->editor()->isAutomaticQuoteSubstitutionEnabled() + && !document()->frame()->editor()->isAutomaticLinkDetectionEnabled() + && !document()->frame()->editor()->isAutomaticDashSubstitutionEnabled() + && !document()->frame()->editor()->isAutomaticTextReplacementEnabled()) + return; +#else if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled()) return; +#endif // Take a look at the selection that results after typing and determine whether we need to spellcheck. // Since the word containing the current selection is never marked, this does a check to // see if typing made a new word that is not in the current selection. Basically, you @@ -299,8 +311,15 @@ void TypingCommand::markMisspellingsAfterTyping() void TypingCommand::typingAddedToOpenCommand() { +#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(); +#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(); document()->frame()->editor()->appliedEditing(this); +#endif } void TypingCommand::insertText(const String &text, bool selectInsertedText) @@ -358,10 +377,38 @@ void TypingCommand::insertParagraphSeparator() void TypingCommand::insertParagraphSeparatorInQuotedContent() { + // If the selection starts inside a table, just insert the paragraph separator normally + // Breaking the blockquote would also break apart the table, which is unecessary when inserting a newline + if (enclosingNodeOfType(endingSelection().start(), &isTableStructureNode)) { + insertParagraphSeparator(); + return; + } + applyCommandToComposite(BreakBlockquoteCommand::create(document())); typingAddedToOpenCommand(); } +bool TypingCommand::makeEditableRootEmpty() +{ + Element* root = endingSelection().rootEditableElement(); + if (!root->firstChild()) + return false; + + if (root->firstChild() == root->lastChild() && root->firstElementChild() && root->firstElementChild()->hasTagName(brTag)) { + // If there is a single child and it could be a placeholder, leave it alone. + if (root->renderer() && root->renderer()->isBlockFlow()) + return false; + } + + while (Node* child = root->firstChild()) + removeNode(child); + + addBlockPlaceholderIfNeeded(root); + setEndingSelection(VisibleSelection(Position(root, 0), DOWNSTREAM)); + + return true; +} + void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) { VisibleSelection selectionToDelete; @@ -386,12 +433,17 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) if (killRing && selection.isCaret() && granularity != CharacterGranularity) selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, CharacterGranularity); - // When the caret is at the start of the editable area in an empty list item, break out of the list item. if (endingSelection().visibleStart().previous(true).isNull()) { + // When the caret is at the start of the editable area in an empty list item, break out of the list item. if (breakOutOfEmptyListItem()) { typingAddedToOpenCommand(); return; } + // When there are no visible positions in the editing root, delete its entire contents. + if (endingSelection().visibleStart().next(true).isNull() && makeEditableRootEmpty()) { + typingAddedToOpenCommand(); + return; + } } VisiblePosition visibleStart(endingSelection().visibleStart()); @@ -411,7 +463,7 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) selectionToDelete = selection.selection(); - if (granularity == CharacterGranularity && selectionToDelete.end().node() == selectionToDelete.start().node() && selectionToDelete.end().m_offset - selectionToDelete.start().m_offset > 1) { + if (granularity == CharacterGranularity && selectionToDelete.end().node() == selectionToDelete.start().node() && selectionToDelete.end().deprecatedEditingOffset() - selectionToDelete.start().deprecatedEditingOffset() > 1) { // If there are multiple Unicode code points to be deleted, adjust the range to match platform conventions. selectionToDelete.setWithoutValidation(selectionToDelete.end(), selectionToDelete.end().previous(BackwardDeletion)); } @@ -476,7 +528,7 @@ void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool ki if (visibleEnd == endOfParagraph(visibleEnd)) downstreamEnd = visibleEnd.next(true).deepEquivalent().downstream(); // When deleting tables: Select the table first, then perform the deletion - if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.m_offset == 0) { + if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.deprecatedEditingOffset() == 0) { setEndingSelection(VisibleSelection(endingSelection().end(), lastDeepEditingPositionForNode(downstreamEnd.node()), DOWNSTREAM)); typingAddedToOpenCommand(); return; @@ -499,10 +551,10 @@ void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool ki else { int extraCharacters; if (selectionToDelete.start().node() == selectionToDelete.end().node()) - extraCharacters = selectionToDelete.end().m_offset - selectionToDelete.start().m_offset; + extraCharacters = selectionToDelete.end().deprecatedEditingOffset() - selectionToDelete.start().deprecatedEditingOffset(); else - extraCharacters = selectionToDelete.end().m_offset; - extent = Position(extent.node(), extent.m_offset + extraCharacters); + extraCharacters = selectionToDelete.end().deprecatedEditingOffset(); + extent = Position(extent.node(), extent.deprecatedEditingOffset() + extraCharacters); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } diff --git a/WebCore/editing/TypingCommand.h b/WebCore/editing/TypingCommand.h index bf588be..2c52447 100644 --- a/WebCore/editing/TypingCommand.h +++ b/WebCore/editing/TypingCommand.h @@ -83,6 +83,7 @@ private: void markMisspellingsAfterTyping(); void typingAddedToOpenCommand(); + bool makeEditableRootEmpty(); ETypingCommand m_commandType; String m_textToInsert; diff --git a/WebCore/editing/VisiblePosition.cpp b/WebCore/editing/VisiblePosition.cpp index 27ee146..2db6d31 100644 --- a/WebCore/editing/VisiblePosition.cpp +++ b/WebCore/editing/VisiblePosition.cpp @@ -28,8 +28,8 @@ #include "CString.h" #include "Document.h" -#include "Element.h" #include "FloatQuad.h" +#include "HTMLElement.h" #include "HTMLNames.h" #include "InlineTextBox.h" #include "Logging.h" @@ -511,7 +511,7 @@ Position VisiblePosition::canonicalPosition(const Position& position) return next; } -UChar VisiblePosition::characterAfter() const +UChar32 VisiblePosition::characterAfter() const { // We canonicalize to the first of two equivalent candidates, but the second of the two candidates // is the one that will be inside the text node containing the character after this visible position. @@ -520,10 +520,15 @@ UChar VisiblePosition::characterAfter() const if (!node || !node->isTextNode()) return 0; Text* textNode = static_cast<Text*>(pos.node()); - int offset = pos.m_offset; - if ((unsigned)offset >= textNode->length()) + unsigned offset = pos.deprecatedEditingOffset(); + unsigned length = textNode->length(); + if (offset >= length) return 0; - return textNode->data()[offset]; + + UChar32 ch; + const UChar* characters = textNode->data().characters(); + U16_NEXT(characters, offset, length, ch); + return ch; } IntRect VisiblePosition::localCaretRect(RenderObject*& renderer) const @@ -576,7 +581,7 @@ void VisiblePosition::debugPosition(const char* msg) const if (isNull()) fprintf(stderr, "Position [%s]: null\n", msg); else - fprintf(stderr, "Position [%s]: %s [%p] at %d\n", msg, m_deepPosition.node()->nodeName().utf8().data(), m_deepPosition.node(), m_deepPosition.m_offset); + fprintf(stderr, "Position [%s]: %s [%p] at %d\n", msg, m_deepPosition.node()->nodeName().utf8().data(), m_deepPosition.node(), m_deepPosition.deprecatedEditingOffset()); } #ifndef NDEBUG @@ -600,7 +605,7 @@ PassRefPtr<Range> makeRange(const VisiblePosition &start, const VisiblePosition Position s = rangeCompliantEquivalent(start); Position e = rangeCompliantEquivalent(end); - return Range::create(s.node()->document(), s.node(), s.m_offset, e.node(), e.m_offset); + return Range::create(s.node()->document(), s.node(), s.deprecatedEditingOffset(), e.node(), e.deprecatedEditingOffset()); } VisiblePosition startVisiblePosition(const Range *r, EAffinity affinity) @@ -621,7 +626,7 @@ bool setStart(Range *r, const VisiblePosition &visiblePosition) return false; Position p = rangeCompliantEquivalent(visiblePosition); int code = 0; - r->setStart(p.node(), p.m_offset, code); + r->setStart(p.node(), p.deprecatedEditingOffset(), code); return code == 0; } @@ -631,7 +636,7 @@ bool setEnd(Range *r, const VisiblePosition &visiblePosition) return false; Position p = rangeCompliantEquivalent(visiblePosition); int code = 0; - r->setEnd(p.node(), p.m_offset, code); + r->setEnd(p.node(), p.deprecatedEditingOffset(), code); return code == 0; } diff --git a/WebCore/editing/VisiblePosition.h b/WebCore/editing/VisiblePosition.h index 403c816..d888806 100644 --- a/WebCore/editing/VisiblePosition.h +++ b/WebCore/editing/VisiblePosition.h @@ -47,6 +47,8 @@ namespace WebCore { class InlineBox; +enum StayInEditableContent { MayLeaveEditableContent, MustStayInEditableContent }; + class VisiblePosition { public: // NOTE: UPSTREAM affinity will be used only if pos is at end of a wrapped line, @@ -64,6 +66,8 @@ public: EAffinity affinity() const { ASSERT(m_affinity == UPSTREAM || m_affinity == DOWNSTREAM); return m_affinity; } void setAffinity(EAffinity affinity) { m_affinity = affinity; } + // FIXME: Change the following functions' parameter from a boolean to StayInEditableContent. + // next() and previous() will increment/decrement by a character cluster. VisiblePosition next(bool stayInEditableContent = false) const; VisiblePosition previous(bool stayInEditableContent = false) const; @@ -73,8 +77,8 @@ public: VisiblePosition left(bool stayInEditableContent = false) const; VisiblePosition right(bool stayInEditableContent = false) const; - UChar characterAfter() const; - UChar characterBefore() const { return previous().characterAfter(); } + UChar32 characterAfter() const; + UChar32 characterBefore() const { return previous().characterAfter(); } void debugPosition(const char* msg = "") const; diff --git a/WebCore/editing/VisibleSelection.cpp b/WebCore/editing/VisibleSelection.cpp index 279adf2..56ad6b3 100644 --- a/WebCore/editing/VisibleSelection.cpp +++ b/WebCore/editing/VisibleSelection.cpp @@ -169,7 +169,7 @@ PassRefPtr<Range> VisibleSelection::toNormalizedRange() const ASSERT(isRange()); s = m_start.downstream(); e = m_end.upstream(); - if (Range::compareBoundaryPoints(s.node(), s.m_offset, e.node(), e.m_offset) > 0) { + if (comparePositions(s, e) > 0) { // Make sure the start is before the end. // The end can wind up before the start if collapsed whitespace is the only thing selected. Position tmp = s; @@ -213,7 +213,7 @@ static PassRefPtr<Range> makeSearchRange(const Position& pos) Position start(rangeCompliantEquivalent(pos)); searchRange->selectNodeContents(boundary, ec); - searchRange->setStart(start.node(), start.m_offset, ec); + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); ASSERT(!ec); if (ec) @@ -222,6 +222,11 @@ static PassRefPtr<Range> makeSearchRange(const Position& pos) return searchRange.release(); } +bool VisibleSelection::isAll(StayInEditableContent stayInEditableContent) const +{ + return !shadowTreeRootNode() && visibleStart().previous(stayInEditableContent).isNull() && visibleEnd().next(stayInEditableContent).isNull(); +} + void VisibleSelection::appendTrailingWhitespace() { RefPtr<Range> searchRange = makeSearchRange(m_end); @@ -588,13 +593,13 @@ void VisibleSelection::debugPosition() const if (m_start == m_end) { Position pos = m_start; - fprintf(stderr, "pos: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.m_offset); + fprintf(stderr, "pos: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); } else { Position pos = m_start; - fprintf(stderr, "start: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.m_offset); + fprintf(stderr, "start: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); fprintf(stderr, "-----------------------------------\n"); pos = m_end; - fprintf(stderr, "end: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.m_offset); + fprintf(stderr, "end: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); fprintf(stderr, "-----------------------------------\n"); } @@ -628,7 +633,7 @@ void VisibleSelection::showTreeForThis() const { if (start().node()) { start().node()->showTreeAndMark(start().node(), "S", end().node(), "E"); - fprintf(stderr, "start offset: %d, end offset: %d\n", start().m_offset, end().m_offset); + fprintf(stderr, "start offset: %d, end offset: %d\n", start().deprecatedEditingOffset(), end().deprecatedEditingOffset()); } } diff --git a/WebCore/editing/VisibleSelection.h b/WebCore/editing/VisibleSelection.h index ae2142d..e346b27 100644 --- a/WebCore/editing/VisibleSelection.h +++ b/WebCore/editing/VisibleSelection.h @@ -42,7 +42,7 @@ public: VisibleSelection(); VisibleSelection(const Position&, EAffinity); - VisibleSelection(const Position&, const Position&, EAffinity); + VisibleSelection(const Position&, const Position&, EAffinity = SEL_DEFAULT_AFFINITY); VisibleSelection(const Range*, EAffinity = SEL_DEFAULT_AFFINITY); @@ -76,6 +76,8 @@ public: bool isBaseFirst() const { return m_baseIsFirst; } + bool isAll(StayInEditableContent) const; + void appendTrailingWhitespace(); bool expandUsingGranularity(TextGranularity granularity); diff --git a/WebCore/editing/gtk/SelectionControllerGtk.cpp b/WebCore/editing/gtk/SelectionControllerGtk.cpp new file mode 100644 index 0000000..52fbbab --- /dev/null +++ b/WebCore/editing/gtk/SelectionControllerGtk.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2009 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "config.h" +#include "SelectionController.h" + +#include "AXObjectCache.h" +#include "Frame.h" + +#include <gtk/gtk.h> + +namespace WebCore { + +void SelectionController::notifyAccessibilityForSelectionChange() +{ + if (AXObjectCache::accessibilityEnabled() && m_sel.start().isNotNull() && m_sel.end().isNotNull()) { + RenderObject* focusedNode = m_sel.start().node()->renderer(); + AccessibilityObject* accessibilityObject = m_frame->document()->axObjectCache()->getOrCreate(focusedNode); + AtkObject* wrapper = accessibilityObject->wrapper(); + if (ATK_IS_TEXT(wrapper)) { + g_signal_emit_by_name(wrapper, "text-caret-moved", m_sel.start().offsetInContainerNode()); + + if (m_sel.isRange()) + g_signal_emit_by_name(wrapper, "text-selection-changed"); + } + } +} + +} // namespace WebCore diff --git a/WebCore/editing/htmlediting.cpp b/WebCore/editing/htmlediting.cpp index 055c3a7..7b51295 100644 --- a/WebCore/editing/htmlediting.cpp +++ b/WebCore/editing/htmlediting.cpp @@ -98,8 +98,8 @@ int comparePositions(const Position& a, const Position& b) ASSERT(nodeA); Node* nodeB = b.node(); ASSERT(nodeB); - int offsetA = a.m_offset; - int offsetB = b.m_offset; + int offsetA = a.deprecatedEditingOffset(); + int offsetB = b.deprecatedEditingOffset(); Node* shadowAncestorA = nodeA->shadowAncestorNode(); if (shadowAncestorA == nodeA) @@ -126,6 +126,11 @@ int comparePositions(const Position& a, const Position& b) return result ? result : bias; } +int comparePositions(const VisiblePosition& a, const VisiblePosition& b) +{ + return comparePositions(a.deepEquivalent(), b.deepEquivalent()); +} + Node* highestEditableRoot(const Position& position) { Node* node = position.node(); @@ -322,17 +327,17 @@ Position rangeCompliantEquivalent(const Position& pos) Node* node = pos.node(); - if (pos.m_offset <= 0) { + if (pos.deprecatedEditingOffset() <= 0) { if (node->parentNode() && (editingIgnoresContent(node) || isTableElement(node))) return positionBeforeNode(node); return Position(node, 0); } if (node->offsetInCharacters()) - return Position(node, min(node->maxCharacterOffset(), pos.m_offset)); + return Position(node, min(node->maxCharacterOffset(), pos.deprecatedEditingOffset())); int maxCompliantOffset = node->childNodeCount(); - if (pos.m_offset > maxCompliantOffset) { + if (pos.deprecatedEditingOffset() > maxCompliantOffset) { if (node->parentNode()) return positionAfterNode(node); @@ -342,12 +347,12 @@ Position rangeCompliantEquivalent(const Position& pos) } // Editing should never generate positions like this. - if ((pos.m_offset < maxCompliantOffset) && editingIgnoresContent(node)) { + if ((pos.deprecatedEditingOffset() < maxCompliantOffset) && editingIgnoresContent(node)) { ASSERT_NOT_REACHED(); return node->parentNode() ? positionBeforeNode(node) : Position(node, 0); } - if (pos.m_offset == maxCompliantOffset && (editingIgnoresContent(node) || isTableElement(node))) + if (pos.deprecatedEditingOffset() == maxCompliantOffset && (editingIgnoresContent(node) || isTableElement(node))) return positionAfterNode(node); return Position(pos); @@ -906,14 +911,25 @@ int caretMaxOffset(const Node* n) return lastOffsetForEditing(n); } -bool lineBreakExistsAtPosition(const VisiblePosition& visiblePosition) +bool lineBreakExistsAtVisiblePosition(const VisiblePosition& visiblePosition) { - if (visiblePosition.isNull()) + return lineBreakExistsAtPosition(visiblePosition.deepEquivalent().downstream()); +} + +bool lineBreakExistsAtPosition(const Position& position) +{ + if (position.isNull()) return false; - - Position downstream(visiblePosition.deepEquivalent().downstream()); - return downstream.node()->hasTagName(brTag) || - (downstream.node()->isTextNode() && downstream.node()->renderer()->style()->preserveNewline() && visiblePosition.characterAfter() == '\n'); + + if (position.anchorNode()->hasTagName(brTag) && position.atFirstEditingPositionForNode()) + return true; + + if (!position.anchorNode()->isTextNode() || !position.anchorNode()->renderer()->style()->preserveNewline()) + return false; + + Text* textNode = static_cast<Text*>(position.anchorNode()); + unsigned offset = position.offsetInContainerNode(); + return offset < textNode->length() && textNode->data()[offset] == '\n'; } // Modifies selections that have an end point at the edge of a table diff --git a/WebCore/editing/htmlediting.h b/WebCore/editing/htmlediting.h index ece5e29..374b512 100644 --- a/WebCore/editing/htmlediting.h +++ b/WebCore/editing/htmlediting.h @@ -37,9 +37,9 @@ class HTMLElement; class Node; class Position; class Range; -class VisibleSelection; class String; class VisiblePosition; +class VisibleSelection; Position rangeCompliantEquivalent(const Position&); Position rangeCompliantEquivalent(const VisiblePosition&); @@ -51,6 +51,7 @@ Node* highestEditableRoot(const Position&); VisiblePosition firstEditablePositionAfterPositionInRoot(const Position&, Node*); VisiblePosition lastEditablePositionBeforePositionInRoot(const Position&, Node*); int comparePositions(const Position&, const Position&); +int comparePositions(const VisiblePosition&, const VisiblePosition&); Node* lowestEditableAncestor(Node*); bool isContentEditable(const Node*); Position nextCandidate(const Position&); @@ -127,7 +128,8 @@ Node* highestAncestor(Node*); bool isTableElement(Node*); bool isTableCell(const Node*); -bool lineBreakExistsAtPosition(const VisiblePosition&); +bool lineBreakExistsAtPosition(const Position&); +bool lineBreakExistsAtVisiblePosition(const VisiblePosition&); VisibleSelection selectionForParagraphIteration(const VisibleSelection&); diff --git a/WebCore/editing/mac/SelectionControllerMac.mm b/WebCore/editing/mac/SelectionControllerMac.mm index 03e1051..5970f99 100644 --- a/WebCore/editing/mac/SelectionControllerMac.mm +++ b/WebCore/editing/mac/SelectionControllerMac.mm @@ -38,7 +38,7 @@ void SelectionController::notifyAccessibilityForSelectionChange() Document* document = m_frame->document(); if (AXObjectCache::accessibilityEnabled() && m_sel.start().isNotNull() && m_sel.end().isNotNull()) - document->axObjectCache()->postNotification(m_sel.start().node()->renderer(), "AXSelectedTextChanged"); + document->axObjectCache()->postNotification(m_sel.start().node()->renderer(), "AXSelectedTextChanged", false); // if zoom feature is enabled, insertion point changes should update the zoom if (!UAZoomEnabled() || !m_sel.isCaret()) diff --git a/WebCore/editing/markup.cpp b/WebCore/editing/markup.cpp index 7de5287..d6fe1ce 100644 --- a/WebCore/editing/markup.cpp +++ b/WebCore/editing/markup.cpp @@ -30,6 +30,7 @@ #include "CharacterNames.h" #include "Comment.h" #include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" #include "CSSPrimitiveValue.h" #include "CSSProperty.h" #include "CSSPropertyNames.h" @@ -190,7 +191,7 @@ static void appendQuotedURLAttributeValue(Vector<UChar>& result, const String& u { UChar quoteChar = '\"'; String strippedURLString = urlString.stripWhiteSpace(); - if (protocolIs(strippedURLString, "javascript")) { + if (protocolIsJavaScript(strippedURLString)) { // minimal escaping for javascript urls if (strippedURLString.contains('"')) { if (strippedURLString.contains('\'')) @@ -387,7 +388,14 @@ static void appendDocumentType(Vector<UChar>& result, const DocumentType* n) append(result, ">"); } -static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0) +static void removeExteriorStyles(CSSMutableStyleDeclaration* style) +{ + style->removeProperty(CSSPropertyFloat); +} + +enum RangeFullySelectsNode { DoesFullySelectNode, DoesNotFullySelectNode }; + +static void appendStartMarkup(Vector<UChar>& result, const Node* node, const Range* range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0, RangeFullySelectsNode rangeFullySelectsNode = DoesFullySelectNode) { bool documentIsHTML = node->document()->isHTMLDocument(); switch (node->nodeType()) { @@ -439,7 +447,7 @@ static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Ran const Element* el = static_cast<const Element*>(node); bool convert = convertBlocksToInlines && isBlock(const_cast<Node*>(node)); append(result, el->nodeNamePreservingCase()); - NamedAttrMap *attrs = el->attributes(); + NamedNodeMap *attrs = el->attributes(); unsigned length = attrs->length(); if (!documentIsHTML && namespaces && shouldAddNamespaceElem(el)) appendNamespace(result, el->prefix(), el->namespaceURI(), *namespaces); @@ -502,6 +510,10 @@ static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Ran } if (convert) style->setProperty(CSSPropertyDisplay, CSSValueInline, true); + // If the node is not fully selected by the range, then we don't want to keep styles that affect its relationship to the nodes around it + // only the ones that affect it and the nodes within it. + if (rangeFullySelectsNode == DoesNotFullySelectNode) + removeExteriorStyles(style.get()); if (style->length() > 0) { DEFINE_STATIC_LOCAL(const String, stylePrefix, (" style=\"")); append(result, stylePrefix); @@ -536,10 +548,10 @@ static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Ran } } -static String getStartMarkup(const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0) +static String getStartMarkup(const Node* node, const Range* range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0, RangeFullySelectsNode rangeFullySelectsNode = DoesFullySelectNode) { Vector<UChar> result; - appendStartMarkup(result, node, range, annotate, convertBlocksToInlines, namespaces); + appendStartMarkup(result, node, range, annotate, convertBlocksToInlines, namespaces, rangeFullySelectsNode); return String::adopt(result); } @@ -642,7 +654,7 @@ static void completeURLs(Node* node, const String& baseURL) for (Node* n = node; n != end; n = n->traverseNextNode()) { if (n->isElementNode()) { Element* e = static_cast<Element*>(n); - NamedAttrMap* attrs = e->attributes(); + NamedNodeMap* attrs = e->attributes(); unsigned length = attrs->length(); for (unsigned i = 0; i < length; i++) { Attribute* attr = attrs->attributeItem(i); @@ -742,6 +754,15 @@ static bool isSpecialAncestorBlock(Node* node) node->hasTagName(h5Tag); } +static bool shouldIncludeWrapperForFullySelectedRoot(Node* fullySelectedRoot, CSSMutableStyleDeclaration* style) +{ + if (fullySelectedRoot->isElementNode() && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) + return true; + + return style->getPropertyCSSValue(CSSPropertyBackgroundImage) || + style->getPropertyCSSValue(CSSPropertyBackgroundColor); +} + // FIXME: Shouldn't we omit style info when annotate == DoNotAnnotateForInterchange? // FIXME: At least, annotation and style info should probably not be included in range.markupString() String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterchange annotate, bool convertBlocksToInlines) @@ -798,14 +819,20 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc markups.append(interchangeNewlineString); startNode = visibleStart.next().deepEquivalent().node(); + + if (pastEnd && Range::compareBoundaryPoints(startNode, 0, pastEnd, 0) >= 0) { + if (deleteButton) + deleteButton->enable(); + return interchangeNewlineString; + } } Node* next; for (Node* n = startNode; n != pastEnd; n = next) { - - // According to <rdar://problem/5730668>, it is possible for n to blow past pastEnd and become null here. This - // shouldn't be possible. This null check will prevent crashes (but create too much markup) and the ASSERT will - // hopefully lead us to understanding the problem. + // According to <rdar://problem/5730668>, it is possible for n to blow + // past pastEnd and become null here. This shouldn't be possible. + // This null check will prevent crashes (but create too much markup) + // and the ASSERT will hopefully lead us to understanding the problem. ASSERT(n); if (!n) break; @@ -878,7 +905,7 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc // We added markup for this node, and we're descending into it. Set it to close eventually. ancestorsToClose.append(n); } - + // Include ancestors that aren't completely inside the range but are required to retain // the structure and appearance of the copied markup. Node* specialCommonAncestor = 0; @@ -921,29 +948,29 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc specialCommonAncestor = enclosingAnchor; Node* body = enclosingNodeWithTag(Position(commonAncestor, 0), bodyTag); - // FIXME: Only include markup for a fully selected root (and ancestors of lastClosed up to that root) if - // there are styles/attributes on those nodes that need to be included to preserve the appearance of the copied markup. // FIXME: Do this for all fully selected blocks, not just the body. Node* fullySelectedRoot = body && *VisibleSelection::selectionFromContentsOfNode(body).toNormalizedRange() == *updatedRange ? body : 0; - if (annotate && fullySelectedRoot) - specialCommonAncestor = fullySelectedRoot; + RefPtr<CSSMutableStyleDeclaration> fullySelectedRootStyle = fullySelectedRoot ? styleFromMatchedRulesAndInlineDecl(fullySelectedRoot) : 0; + if (annotate && fullySelectedRoot) { + if (shouldIncludeWrapperForFullySelectedRoot(fullySelectedRoot, fullySelectedRootStyle.get())) + specialCommonAncestor = fullySelectedRoot; + } if (specialCommonAncestor && lastClosed) { // Also include all of the ancestors of lastClosed up to this special ancestor. for (Node* ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) { if (ancestor == fullySelectedRoot && !convertBlocksToInlines) { - RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(fullySelectedRoot); // Bring the background attribute over, but not as an attribute because a background attribute on a div // appears to have no effect. - if (!style->getPropertyCSSValue(CSSPropertyBackgroundImage) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) - style->setProperty(CSSPropertyBackgroundImage, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')"); + if (!fullySelectedRootStyle->getPropertyCSSValue(CSSPropertyBackgroundImage) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) + fullySelectedRootStyle->setProperty(CSSPropertyBackgroundImage, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')"); - if (style->length()) { + if (fullySelectedRootStyle->length()) { Vector<UChar> openTag; DEFINE_STATIC_LOCAL(const String, divStyle, ("<div style=\"")); append(openTag, divStyle); - appendAttributeValue(openTag, style->cssText(), documentIsHTML); + appendAttributeValue(openTag, fullySelectedRootStyle->cssText(), documentIsHTML); openTag.append('\"'); openTag.append('>'); preMarkups.append(String::adopt(openTag)); @@ -952,7 +979,9 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc markups.append(divCloseTag); } } else { - preMarkups.append(getStartMarkup(ancestor, updatedRange.get(), annotate, convertBlocksToInlines)); + // Since this node and all the other ancestors are not in the selection we want to set RangeFullySelectsNode to DoesNotFullySelectNode + // so that styles that affect the exterior of the node are not included. + preMarkups.append(getStartMarkup(ancestor, updatedRange.get(), annotate, convertBlocksToInlines, 0, DoesNotFullySelectNode)); markups.append(getEndMarkup(ancestor)); } if (nodes) diff --git a/WebCore/editing/visible_units.cpp b/WebCore/editing/visible_units.cpp index 1e8b05b..02e9fb8 100644 --- a/WebCore/editing/visible_units.cpp +++ b/WebCore/editing/visible_units.cpp @@ -43,25 +43,35 @@ namespace WebCore { using namespace HTMLNames; using namespace WTF::Unicode; -static int firstNonComplexContextLineBreak(const UChar* characters, int length) +static int endOfFirstWordBoundaryContext(const UChar* characters, int length) { - for (int i = 0; i < length; ++i) { - if (!hasLineBreakingPropertyComplexContext(characters[i])) - return i; + for (int i = 0; i < length; ) { + int first = i; + UChar32 ch; + U16_NEXT(characters, i, length, ch); + if (!requiresContextForWordBoundary(ch)) + return first; } return length; } -static int lastNonComplexContextLineBreak(const UChar* characters, int length) +static int startOfLastWordBoundaryContext(const UChar* characters, int length) { - for (int i = length - 1; i >= 0; --i) { - if (!hasLineBreakingPropertyComplexContext(characters[i])) - return i; + for (int i = length; i > 0; ) { + int last = i; + UChar32 ch; + U16_PREV(characters, 0, i, ch); + if (!requiresContextForWordBoundary(ch)) + return last; } - return -1; + return 0; } -static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*searchFunction)(const UChar *, unsigned, unsigned)) +enum BoundarySearchContextAvailability { DontHaveMoreContext, MayHaveMoreContext }; + +typedef unsigned (*BoundarySearchFunction)(const UChar*, unsigned length, unsigned offset, BoundarySearchContextAvailability, bool& needMoreContext); + +static VisiblePosition previousBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) { Position pos = c.deepEquivalent(); Node *n = pos.node(); @@ -86,15 +96,15 @@ static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*sea unsigned suffixLength = 0; ExceptionCode ec = 0; - if (hasLineBreakingPropertyComplexContext(c.characterBefore())) { + if (requiresContextForWordBoundary(c.characterBefore())) { RefPtr<Range> forwardsScanRange(d->createRange()); forwardsScanRange->setEndAfter(boundary, ec); - forwardsScanRange->setStart(end.node(), end.m_offset, ec); + forwardsScanRange->setStart(end.node(), end.deprecatedEditingOffset(), ec); TextIterator forwardsIterator(forwardsScanRange.get()); while (!forwardsIterator.atEnd()) { const UChar* characters = forwardsIterator.characters(); int length = forwardsIterator.length(); - int i = firstNonComplexContextLineBreak(characters, length); + int i = endOfFirstWordBoundaryContext(characters, length); string.append(characters, i); suffixLength += i; if (i < length) @@ -103,8 +113,8 @@ static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*sea } } - searchRange->setStart(start.node(), start.m_offset, ec); - searchRange->setEnd(end.node(), end.m_offset, ec); + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); + searchRange->setEnd(end.node(), end.deprecatedEditingOffset(), ec); ASSERT(!ec); if (ec) @@ -113,6 +123,7 @@ static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*sea SimplifiedBackwardsTextIterator it(searchRange.get()); unsigned next = 0; bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE; + bool needMoreContext = false; while (!it.atEnd()) { // iterate to get chunks until the searchFunction returns a non-zero value. if (!inTextSecurityMode) @@ -123,13 +134,18 @@ static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*sea iteratorString = iteratorString.impl()->secure('x'); string.prepend(iteratorString.characters(), iteratorString.length()); } - - next = searchFunction(string.data(), string.size(), string.size() - suffixLength); + next = searchFunction(string.data(), string.size(), string.size() - suffixLength, MayHaveMoreContext, needMoreContext); if (next != 0) break; it.advance(); } - + if (needMoreContext) { + // The last search returned the beginning of the buffer and asked for more context, + // but there is no earlier text. Force a search with what's available. + next = searchFunction(string.data(), string.size(), string.size() - suffixLength, DontHaveMoreContext, needMoreContext); + ASSERT(!needMoreContext); + } + if (it.atEnd() && next == 0) { pos = it.range()->startPosition(); } else if (next != 0) { @@ -148,7 +164,7 @@ static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*sea return VisiblePosition(pos, DOWNSTREAM); } -static VisiblePosition nextBoundary(const VisiblePosition &c, unsigned (*searchFunction)(const UChar *, unsigned, unsigned)) +static VisiblePosition nextBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) { Position pos = c.deepEquivalent(); Node *n = pos.node(); @@ -172,27 +188,28 @@ static VisiblePosition nextBoundary(const VisiblePosition &c, unsigned (*searchF unsigned prefixLength = 0; ExceptionCode ec = 0; - if (hasLineBreakingPropertyComplexContext(c.characterAfter())) { + if (requiresContextForWordBoundary(c.characterAfter())) { RefPtr<Range> backwardsScanRange(d->createRange()); - backwardsScanRange->setEnd(start.node(), start.m_offset, ec); + backwardsScanRange->setEnd(start.node(), start.deprecatedEditingOffset(), ec); SimplifiedBackwardsTextIterator backwardsIterator(backwardsScanRange.get()); while (!backwardsIterator.atEnd()) { const UChar* characters = backwardsIterator.characters(); int length = backwardsIterator.length(); - int i = lastNonComplexContextLineBreak(characters, length); - string.prepend(characters + i + 1, length - i - 1); - prefixLength += length - i - 1; - if (i > -1) + int i = startOfLastWordBoundaryContext(characters, length); + string.prepend(characters + i, length - i); + prefixLength += length - i; + if (i > 0) break; backwardsIterator.advance(); } } searchRange->selectNodeContents(boundary, ec); - searchRange->setStart(start.node(), start.m_offset, ec); + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); TextIterator it(searchRange.get(), true); unsigned next = 0; bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE; + bool needMoreContext = false; while (!it.atEnd()) { // Keep asking the iterator for chunks until the search function // returns an end value not equal to the length of the string passed to it. @@ -204,12 +221,17 @@ static VisiblePosition nextBoundary(const VisiblePosition &c, unsigned (*searchF iteratorString = iteratorString.impl()->secure('x'); string.append(iteratorString.characters(), iteratorString.length()); } - - next = searchFunction(string.data(), string.size(), prefixLength); + next = searchFunction(string.data(), string.size(), prefixLength, MayHaveMoreContext, needMoreContext); if (next != string.size()) break; it.advance(); } + if (needMoreContext) { + // The last search returned the end of the buffer and asked for more context, + // but there is no further text. Force a search with what's available. + next = searchFunction(string.data(), string.size(), prefixLength, DontHaveMoreContext, needMoreContext); + ASSERT(!needMoreContext); + } if (it.atEnd() && next == string.size()) { pos = it.range()->startPosition(); @@ -233,11 +255,14 @@ static VisiblePosition nextBoundary(const VisiblePosition &c, unsigned (*searchF // --------- -static unsigned startWordBoundary(const UChar* characters, unsigned length, unsigned offset) +static unsigned startWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { ASSERT(offset); - if (lastNonComplexContextLineBreak(characters, offset) == -1) + if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { + needMoreContext = true; return 0; + } + needMoreContext = false; int start, end; findWordBoundary(characters, length, offset - 1, &start, &end); return start; @@ -260,11 +285,14 @@ VisiblePosition startOfWord(const VisiblePosition &c, EWordSide side) return previousBoundary(p, startWordBoundary); } -static unsigned endWordBoundary(const UChar* characters, unsigned length, unsigned offset) +static unsigned endWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { ASSERT(offset <= length); - if (firstNonComplexContextLineBreak(characters + offset, length - offset) == static_cast<int>(length - offset)) + if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast<int>(length - offset)) { + needMoreContext = true; return length; + } + needMoreContext = false; int start, end; findWordBoundary(characters, length, offset, &start, &end); return end; @@ -286,10 +314,13 @@ VisiblePosition endOfWord(const VisiblePosition &c, EWordSide side) return nextBoundary(p, endWordBoundary); } -static unsigned previousWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset) +static unsigned previousWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { - if (lastNonComplexContextLineBreak(characters, offset) == -1) + if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { + needMoreContext = true; return 0; + } + needMoreContext = false; return findNextWordFromIndex(characters, length, offset, false); } @@ -299,10 +330,13 @@ VisiblePosition previousWordPosition(const VisiblePosition &c) return c.honorEditableBoundaryAtOrAfter(prev); } -static unsigned nextWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset) +static unsigned nextWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { - if (firstNonComplexContextLineBreak(characters + offset, length - offset) == static_cast<int>(length - offset)) + if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast<int>(length - offset)) { + needMoreContext = true; return length; + } + needMoreContext = false; return findNextWordFromIndex(characters, length, offset, true); } @@ -352,7 +386,7 @@ static VisiblePosition startPositionForLine(const VisiblePosition& c) // There are VisiblePositions at offset 0 in blocks without // RootInlineBoxes, like empty editable blocks and bordered blocks. Position p = c.deepEquivalent(); - if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.m_offset == 0) + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.deprecatedEditingOffset() == 0) return positionAvoidingFirstPositionInTable(c); return VisiblePosition(); @@ -399,7 +433,7 @@ VisiblePosition startOfLine(const VisiblePosition& c) // greater than the input position. This fix is to account for the discrepancy between lines with webkit-line-break:after-white-space // style versus lines without that style, which would break before a space by default. Position p = visPos.deepEquivalent(); - if (p.m_offset > c.deepEquivalent().m_offset && p.node()->isSameNode(c.deepEquivalent().node())) { + if (p.deprecatedEditingOffset() > c.deepEquivalent().deprecatedEditingOffset() && p.node()->isSameNode(c.deepEquivalent().node())) { visPos = c.previous(); if (visPos.isNull()) return VisiblePosition(); @@ -420,7 +454,7 @@ static VisiblePosition endPositionForLine(const VisiblePosition& c) // There are VisiblePositions at offset 0 in blocks without // RootInlineBoxes, like empty editable blocks and bordered blocks. Position p = c.deepEquivalent(); - if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.m_offset == 0) + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.deprecatedEditingOffset() == 0) return c; return VisiblePosition(); } @@ -505,6 +539,15 @@ static Node* previousLeafWithSameEditability(Node* node) return 0; } +static Node* enclosingNodeWithNonInlineRenderer(Node* n) +{ + for (Node* p = n; p; p = p->parentNode()) { + if (p->renderer() && !p->renderer()->isInline()) + return p; + } + return 0; +} + VisiblePosition previousLinePosition(const VisiblePosition &visiblePosition, int x) { Position p = visiblePosition.deepEquivalent(); @@ -534,9 +577,9 @@ VisiblePosition previousLinePosition(const VisiblePosition &visiblePosition, int // This containing editable block does not have a previous line. // Need to move back to previous containing editable block in this root editable // block and find the last root line box in that block. - Node* startBlock = enclosingBlock(node); + Node* startBlock = enclosingNodeWithNonInlineRenderer(node); Node* n = previousLeafWithSameEditability(node); - while (n && startBlock == enclosingBlock(n)) + while (n && startBlock == enclosingNodeWithNonInlineRenderer(n)) n = previousLeafWithSameEditability(n); while (n) { if (highestEditableRoot(Position(n, 0)) != highestRoot) @@ -566,15 +609,15 @@ VisiblePosition previousLinePosition(const VisiblePosition &visiblePosition, int absPos -= containingBlock->layer()->scrolledContentOffset(); RenderObject* renderer = root->closestLeafChildForXPos(x - absPos.x(), isEditablePosition(p))->renderer(); Node* node = renderer->node(); - if (editingIgnoresContent(node)) + if (node && editingIgnoresContent(node)) return Position(node->parent(), node->nodeIndex()); - return renderer->positionForCoordinates(x - absPos.x(), root->topOverflow()); + return renderer->positionForPoint(IntPoint(x - absPos.x(), root->topOverflow())); } // Could not find a previous line. This means we must already be on the first line. // Move to the start of the content in this block, which effectively moves us // to the start of the line we're on. - Node* rootElement = node->isContentEditable() ? node->rootEditableElement() : node->document()->documentElement(); + Element* rootElement = node->isContentEditable() ? node->rootEditableElement() : node->document()->documentElement(); return VisiblePosition(rootElement, 0, DOWNSTREAM); } @@ -636,9 +679,9 @@ VisiblePosition nextLinePosition(const VisiblePosition &visiblePosition, int x) // This containing editable block does not have a next line. // Need to move forward to next containing editable block in this root editable // block and find the first root line box in that block. - Node* startBlock = enclosingBlock(node); - Node* n = nextLeafWithSameEditability(node, p.m_offset); - while (n && startBlock == enclosingBlock(n)) + Node* startBlock = enclosingNodeWithNonInlineRenderer(node); + Node* n = nextLeafWithSameEditability(node, p.deprecatedEditingOffset()); + while (n && startBlock == enclosingNodeWithNonInlineRenderer(n)) n = nextLeafWithSameEditability(n); while (n) { if (highestEditableRoot(Position(n, 0)) != highestRoot) @@ -667,9 +710,9 @@ VisiblePosition nextLinePosition(const VisiblePosition &visiblePosition, int x) absPos -= containingBlock->layer()->scrolledContentOffset(); RenderObject* renderer = root->closestLeafChildForXPos(x - absPos.x(), isEditablePosition(p))->renderer(); Node* node = renderer->node(); - if (editingIgnoresContent(node)) + if (node && editingIgnoresContent(node)) return Position(node->parent(), node->nodeIndex()); - return renderer->positionForCoordinates(x - absPos.x(), root->topOverflow()); + return renderer->positionForPoint(IntPoint(x - absPos.x(), root->topOverflow())); } // Could not find a next line. This means we must already be on the last line. @@ -681,7 +724,7 @@ VisiblePosition nextLinePosition(const VisiblePosition &visiblePosition, int x) // --------- -static unsigned startSentenceBoundary(const UChar* characters, unsigned length, unsigned) +static unsigned startSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { TextBreakIterator* iterator = sentenceBreakIterator(characters, length); // FIXME: The following function can return -1; we don't handle that. @@ -693,7 +736,7 @@ VisiblePosition startOfSentence(const VisiblePosition &c) return previousBoundary(c, startSentenceBoundary); } -static unsigned endSentenceBoundary(const UChar* characters, unsigned length, unsigned) +static unsigned endSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { TextBreakIterator* iterator = sentenceBreakIterator(characters, length); return textBreakNext(iterator); @@ -705,7 +748,7 @@ VisiblePosition endOfSentence(const VisiblePosition &c) return nextBoundary(c, endSentenceBoundary); } -static unsigned previousSentencePositionBoundary(const UChar* characters, unsigned length, unsigned) +static unsigned previousSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { // FIXME: This is identical to startSentenceBoundary. I'm pretty sure that's not right. TextBreakIterator* iterator = sentenceBreakIterator(characters, length); @@ -719,7 +762,7 @@ VisiblePosition previousSentencePosition(const VisiblePosition &c) return c.honorEditableBoundaryAtOrAfter(prev); } -static unsigned nextSentencePositionBoundary(const UChar* characters, unsigned length, unsigned) +static unsigned nextSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { // FIXME: This is identical to endSentenceBoundary. This isn't right, it needs to // move to the equivlant position in the following sentence. @@ -753,7 +796,7 @@ VisiblePosition startOfParagraph(const VisiblePosition& c) Node* startBlock = enclosingBlock(startNode); Node *node = startNode; - int offset = p.m_offset; + int offset = p.deprecatedEditingOffset(); Node *n = startNode; while (n) { @@ -814,7 +857,7 @@ VisiblePosition endOfParagraph(const VisiblePosition &c) Node *stayInsideBlock = startBlock; Node *node = startNode; - int offset = p.m_offset; + int offset = p.deprecatedEditingOffset(); Node *n = startNode; while (n) { @@ -1021,4 +1064,178 @@ VisiblePosition endOfEditableContent(const VisiblePosition& visiblePosition) return lastDeepEditingPositionForNode(highestRoot); } +static void getLeafBoxesInLogicalOrder(RootInlineBox* rootBox, Vector<InlineBox*>& leafBoxesInLogicalOrder) +{ + unsigned char minLevel = 128; + unsigned char maxLevel = 0; + unsigned count = 0; + InlineBox* r = rootBox->firstLeafChild(); + // First find highest and lowest levels, + // and initialize leafBoxesInLogicalOrder with the leaf boxes in visual order. + while (r) { + if (r->bidiLevel() > maxLevel) + maxLevel = r->bidiLevel(); + if (r->bidiLevel() < minLevel) + minLevel = r->bidiLevel(); + leafBoxesInLogicalOrder.append(r); + r = r->nextLeafChild(); + ++count; + } + + if (rootBox->renderer()->style()->visuallyOrdered()) + return; + // Reverse of reordering of the line (L2 according to Bidi spec): + // L2. From the highest level found in the text to the lowest odd level on each line, + // reverse any contiguous sequence of characters that are at that level or higher. + + // Reversing the reordering of the line is only done up to the lowest odd level. + if (!(minLevel % 2)) + minLevel++; + + InlineBox** end = leafBoxesInLogicalOrder.end(); + while (minLevel <= maxLevel) { + InlineBox** iter = leafBoxesInLogicalOrder.begin(); + while (iter != end) { + while (iter != end) { + if ((*iter)->bidiLevel() >= minLevel) + break; + ++iter; + } + InlineBox** first = iter; + while (iter != end) { + if ((*iter)->bidiLevel() < minLevel) + break; + ++iter; + } + InlineBox** last = iter; + std::reverse(first, last); + } + ++minLevel; + } +} + +static void getLogicalStartBoxAndNode(RootInlineBox* rootBox, InlineBox*& startBox, Node*& startNode) +{ + Vector<InlineBox*> leafBoxesInLogicalOrder; + getLeafBoxesInLogicalOrder(rootBox, leafBoxesInLogicalOrder); + startBox = 0; + startNode = 0; + for (size_t i = 0; i < leafBoxesInLogicalOrder.size(); ++i) { + startBox = leafBoxesInLogicalOrder[i]; + startNode = startBox->renderer()->node(); + if (startNode) + return; + } +} + +static void getLogicalEndBoxAndNode(RootInlineBox* rootBox, InlineBox*& endBox, Node*& endNode) +{ + Vector<InlineBox*> leafBoxesInLogicalOrder; + getLeafBoxesInLogicalOrder(rootBox, leafBoxesInLogicalOrder); + endBox = 0; + endNode = 0; + // Generated content (e.g. list markers and CSS :before and :after + // pseudoelements) have no corresponding DOM element, and so cannot be + // represented by a VisiblePosition. Use whatever precedes instead. + for (size_t i = leafBoxesInLogicalOrder.size(); i > 0; --i) { + endBox = leafBoxesInLogicalOrder[i - 1]; + endNode = endBox->renderer()->node(); + if (endNode) + return; + } +} + +static VisiblePosition logicalStartPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox* rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) + return positionAvoidingFirstPositionInTable(c); + + return VisiblePosition(); + } + + InlineBox* logicalStartBox; + Node* logicalStartNode; + getLogicalStartBoxAndNode(rootBox, logicalStartBox, logicalStartNode); + + if (!logicalStartNode) + return VisiblePosition(); + + int startOffset = logicalStartBox->caretMinOffset(); + + VisiblePosition visPos = VisiblePosition(logicalStartNode, startOffset, DOWNSTREAM); + return positionAvoidingFirstPositionInTable(visPos); +} + +VisiblePosition logicalStartOfLine(const VisiblePosition& c) +{ + VisiblePosition visPos = logicalStartPositionForLine(c); + + if (visPos.isNull()) + return c.honorEditableBoundaryAtOrAfter(visPos); + + return c.honorEditableBoundaryAtOrAfter(visPos); +} + +static VisiblePosition logicalEndPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox* rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) + return c; + return VisiblePosition(); + } + + InlineBox* logicalEndBox; + Node* logicalEndNode; + getLogicalEndBoxAndNode(rootBox, logicalEndBox, logicalEndNode); + if (!logicalEndNode) + return VisiblePosition(); + + int endOffset = 1; + if (logicalEndNode->hasTagName(brTag)) + endOffset = 0; + else if (logicalEndBox->isInlineTextBox()) { + InlineTextBox* endTextBox = static_cast<InlineTextBox*>(logicalEndBox); + endOffset = endTextBox->start(); + if (!endTextBox->isLineBreak()) + endOffset += endTextBox->len(); + } + + return VisiblePosition(logicalEndNode, endOffset, VP_UPSTREAM_IF_POSSIBLE); +} + +bool inSameLogicalLine(const VisiblePosition& a, const VisiblePosition& b) +{ + return a.isNotNull() && logicalStartOfLine(a) == logicalStartOfLine(b); +} + +VisiblePosition logicalEndOfLine(const VisiblePosition& c) +{ + VisiblePosition visPos = logicalEndPositionForLine(c); + + // Make sure the end of line is at the same line as the given input position. For a wrapping line, the logical end + // position for the not-last-2-lines might incorrectly hand back the logical beginning of the next line. + // For example, <div contenteditable dir="rtl" style="line-break:before-white-space">abcdefg abcdefg abcdefg + // a abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg </div> + // In this case, use the previous position of the computed logical end position. + if (!inSameLogicalLine(c, visPos)) + visPos = visPos.previous(); + + return c.honorEditableBoundaryAtOrBefore(visPos); +} + } diff --git a/WebCore/editing/visible_units.h b/WebCore/editing/visible_units.h index 2663888..a20b588 100644 --- a/WebCore/editing/visible_units.h +++ b/WebCore/editing/visible_units.h @@ -53,8 +53,11 @@ VisiblePosition endOfLine(const VisiblePosition &); VisiblePosition previousLinePosition(const VisiblePosition &, int x); VisiblePosition nextLinePosition(const VisiblePosition &, int x); bool inSameLine(const VisiblePosition &, const VisiblePosition &); +bool inSameLogicalLine(const VisiblePosition &, const VisiblePosition &); bool isStartOfLine(const VisiblePosition &); bool isEndOfLine(const VisiblePosition &); +VisiblePosition logicalStartOfLine(const VisiblePosition &); +VisiblePosition logicalEndOfLine(const VisiblePosition &); // paragraphs (perhaps a misnomer, can be divided by line break elements) VisiblePosition startOfParagraph(const VisiblePosition&); |