static void expandSelectionToGranularity(Frame* frame, int x, int y, TextGranularity granularity, bool isInputMode) { ASSERT(frame); ASSERT(frame->selection()); VisibleSelection selection; if (x < 0 || y < 0) { if (!isInputMode) return; // Invalid request // Input mode based selection, use the current selection as the selection point. ASSERT(frame->selection()->selectionType() != VisibleSelection::NoSelection); selection = frame->selection()->selection(); } else { VisiblePosition pointLocation(frame->visiblePositionForPoint(WebCore::IntPoint(x, y))); selection = VisibleSelection(pointLocation, pointLocation); } if (!(selection.start().anchorNode() && selection.start().anchorNode()->isTextNode())) return; selection.expandUsingGranularity(granularity); RefPtr<Range> newRange = selection.toNormalizedRange(); if (!newRange) return; ExceptionCode ec = 0; if (newRange->collapsed(ec)) return; RefPtr<Range> oldRange = frame->selection()->selection().toNormalizedRange(); EAffinity affinity = frame->selection()->affinity(); if (isInputMode && !frame->editor()->client()->shouldChangeSelectedRange(oldRange.get(), newRange.get(), affinity, false)) return; frame->selection()->setSelectedRange(newRange.get(), affinity, true); }
TEST_F(SurroundingTextTest, TreeCaretSelection) { setHTML(String("<div>This is outside of <p id='selection'>foo bar</p> the selected node</div>")); { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 1); EXPECT_EQ("f", surroundingText.content()); EXPECT_EQ(0u, surroundingText.startOffsetInContent()); EXPECT_EQ(0u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 5); EXPECT_EQ("foo", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(1u, surroundingText.startOffsetInContent()); EXPECT_EQ(1u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 1337); EXPECT_EQ("This is outside of foo bar the selected node", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(20u, surroundingText.startOffsetInContent()); EXPECT_EQ(20u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(6); SurroundingText surroundingText(selection.start(), 2); EXPECT_EQ("ar", surroundingText.content()); EXPECT_EQ(1u, surroundingText.startOffsetInContent()); EXPECT_EQ(1u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(6); SurroundingText surroundingText(selection.start(), 1337); EXPECT_EQ("This is outside of foo bar the selected node", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(26u, surroundingText.startOffsetInContent()); EXPECT_EQ(26u, surroundingText.endOffsetInContent()); } }
static char* webkitAccessibleTextGetWordForBoundary(AtkText* text, int offset, AtkTextBoundary boundaryType, GetTextRelativePosition textPosition, int* startOffset, int* endOffset) { AccessibilityObject* coreObject = core(text); Document* document = coreObject->document(); if (!document) return emptyTextSelectionAtOffset(0, startOffset, endOffset); Node* node = getNodeForAccessibilityObject(coreObject); if (!node) return emptyTextSelectionAtOffset(0, startOffset, endOffset); int actualOffset = atkOffsetToWebCoreOffset(text, offset); // Besides of the usual conversion from ATK offsets to WebCore offsets, // we need to consider the potential embedded objects that might have been // inserted in the text exposed through AtkText when calculating the offset. actualOffset -= numberOfReplacedElementsBeforeOffset(text, actualOffset); VisiblePosition caretPosition = coreObject->visiblePositionForIndex(actualOffset); VisibleSelection currentWord = wordAtPositionForAtkBoundary(coreObject, caretPosition, boundaryType); // Take into account other relative positions, if needed, by // calculating the new position that we would need to consider. VisiblePosition newPosition = caretPosition; switch (textPosition) { case GetTextPositionAt: break; case GetTextPositionBefore: // Early return if asking for the previous word while already at the beginning. if (isFirstVisiblePositionInNode(currentWord.visibleStart(), node)) return emptyTextSelectionAtOffset(0, startOffset, endOffset); if (isStartOfLine(currentWord.end())) newPosition = currentWord.visibleStart().previous(); else newPosition = startOfWord(currentWord.start(), LeftWordIfOnBoundary); break; case GetTextPositionAfter: // Early return if asking for the following word while already at the end. if (isLastVisiblePositionInNode(currentWord.visibleEnd(), node)) return emptyTextSelectionAtOffset(accessibilityObjectLength(coreObject), startOffset, endOffset); if (isEndOfLine(currentWord.end())) newPosition = currentWord.visibleEnd().next(); else newPosition = endOfWord(currentWord.end(), RightWordIfOnBoundary); break; default: ASSERT_NOT_REACHED(); } // Determine the relevant word we are actually interested in // and calculate the ATK offsets for it, then return everything. VisibleSelection selectedWord = newPosition != caretPosition ? wordAtPositionForAtkBoundary(coreObject, newPosition, boundaryType) : currentWord; getSelectionOffsetsForObject(coreObject, selectedWord, *startOffset, *endOffset); return webkitAccessibleTextGetText(text, *startOffset, *endOffset); }
// Updates |selectionInFlatTree| to match with |selection|. void SelectionAdjuster::adjustSelectionInFlatTree( VisibleSelectionInFlatTree* selectionInFlatTree, const VisibleSelection& selection) { if (selection.isNone()) { *selectionInFlatTree = VisibleSelectionInFlatTree(); return; } const PositionInFlatTree& base = toPositionInFlatTree(selection.base()); const PositionInFlatTree& extent = toPositionInFlatTree(selection.extent()); const PositionInFlatTree& position1 = toPositionInFlatTree(selection.start()); const PositionInFlatTree& position2 = toPositionInFlatTree(selection.end()); position1.anchorNode()->updateDistribution(); position2.anchorNode()->updateDistribution(); selectionInFlatTree->m_base = base; selectionInFlatTree->m_extent = extent; selectionInFlatTree->m_affinity = selection.m_affinity; selectionInFlatTree->m_isDirectional = selection.m_isDirectional; selectionInFlatTree->m_granularity = selection.m_granularity; selectionInFlatTree->m_hasTrailingWhitespace = selection.m_hasTrailingWhitespace; selectionInFlatTree->m_baseIsFirst = base.isNull() || base.compareTo(extent) <= 0; if (position1.compareTo(position2) <= 0) { selectionInFlatTree->m_start = position1; selectionInFlatTree->m_end = position2; } else { selectionInFlatTree->m_start = position2; selectionInFlatTree->m_end = position1; } selectionInFlatTree->updateSelectionType(); }
bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectInsertedText, TextEvent* triggeringEvent) { if (text.isEmpty()) return false; VisibleSelection selection = selectionForCommand(triggeringEvent); if (!selection.isContentEditable()) return false; spellChecker().updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0])); // Get the selection to use for the event that triggered this insertText. // If the event handler changed the selection, we may want to use a different selection // that is contained in the event target. selection = selectionForCommand(triggeringEvent); if (selection.isContentEditable()) { if (Node* selectionStart = selection.start().deprecatedNode()) { RefPtr<Document> document(selectionStart->document()); // Insert the text TypingCommand::Options options = 0; if (selectInsertedText) options |= TypingCommand::SelectInsertedText; TypingCommand::insertText(*document.get(), text, selection, options, triggeringEvent && triggeringEvent->isComposition() ? TypingCommand::TextCompositionConfirm : TypingCommand::TextCompositionNone); // Reveal the current selection if (LocalFrame* editedFrame = document->frame()) { if (Page* page = editedFrame->page()) page->focusController().focusedOrMainFrame()->selection().revealSelection(ScrollAlignment::alignCenterIfNeeded); } } } return true; }
bool static selectionIsContainedByAnchorNode(const VisibleSelection& selection) { // Check whether the start or end of the selection is outside of the selections // anchor node. return (selection.start().anchorType() == WebCore::Position::PositionIsOffsetInAnchor && selection.end().anchorType() == WebCore::Position::PositionIsOffsetInAnchor); }
void TypingCommand::postTextStateChangeNotificationForDeletion(const VisibleSelection& selection) { if (!AXObjectCache::accessibilityEnabled()) return; postTextStateChangeNotification(AXTextEditTypeDelete, AccessibilityObject::stringForVisiblePositionRange(selection), selection.start()); VisiblePositionIndexRange range; range.startIndex.value = indexForVisiblePosition(selection.start(), range.startIndex.scope); range.endIndex.value = indexForVisiblePosition(selection.end(), range.endIndex.scope); composition()->setTextInsertedByUnapplyRange(range); }
// FIXME: We shouldn't need to take selectionForInsertion. It should be identical to SelectionController's current selection. void TypingCommand::insertText(Document* document, const String& text, const VisibleSelection& selectionForInsertion, bool selectInsertedText, TextCompositionType compositionType) { #if REMOVE_MARKERS_UPON_EDITING if (!text.isEmpty()) document->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(isSpaceOrNewline(text.characters()[0])); #endif ASSERT(document); RefPtr<Frame> frame = document->frame(); ASSERT(frame); VisibleSelection currentSelection = frame->selection()->selection(); bool changeSelection = currentSelection != selectionForInsertion; String newText = text; Node* startNode = selectionForInsertion.start().node(); if (startNode && startNode->rootEditableElement() && compositionType != TextCompositionUpdate) { // Send BeforeTextInsertedEvent. The event handler will update text if necessary. ExceptionCode ec = 0; RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text); startNode->rootEditableElement()->dispatchEvent(evt, ec); newText = evt->text(); } if (newText.isEmpty()) return; // Set the starting and ending selection appropriately if we are using a selection // that is different from the current selection. In the future, we should change EditCommand // to deal with custom selections in a general way that can be used by all of the commands. RefPtr<EditCommand> lastEditCommand = frame->editor()->lastEditCommand(); if (isOpenForMoreTypingCommand(lastEditCommand.get())) { TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand.get()); if (lastTypingCommand->endingSelection() != selectionForInsertion) { lastTypingCommand->setStartingSelection(selectionForInsertion); lastTypingCommand->setEndingSelection(selectionForInsertion); } lastTypingCommand->setCompositionType(compositionType); lastTypingCommand->insertText(newText, selectInsertedText); return; } RefPtr<TypingCommand> cmd = TypingCommand::create(document, InsertText, newText, selectInsertedText, compositionType); if (changeSelection) { cmd->setStartingSelection(selectionForInsertion); cmd->setEndingSelection(selectionForInsertion); } applyCommand(cmd); if (changeSelection) { cmd->setEndingSelection(currentSelection); frame->selection()->setSelection(currentSelection); } }
VisibleSelection visibleSelectionForClosestActualWordStart(const VisibleSelection& selection) { // VisibleSelection validation has a special case when the caret is at the end of a paragraph where // it selects the paragraph marker. As well, if the position is at the end of a word, it will select // only the space between words. We want to select an actual word so we move the selection to // the start of the leftmost word if the character after the selection point is whitespace. if (selection.selectionType() != VisibleSelection::RangeSelection) { int leftDistance = 0; int rightDistance = 0; VisibleSelection leftSelection(previousWordPosition(selection.start())); bool leftSelectionIsOnWord = !isWhitespace(leftSelection.visibleStart().characterAfter()) && leftSelection.start().containerNode() == selection.start().containerNode(); if (leftSelectionIsOnWord) { VisibleSelection rangeSelection(endOfWord(leftSelection.start()), selection.visibleStart()); leftDistance = TextIterator::rangeLength(rangeSelection.toNormalizedRange().get()); } VisibleSelection rightSelection = previousWordPosition(nextWordPosition(selection.start())); bool rightSelectionIsOnWord = !isWhitespace(rightSelection.visibleStart().characterAfter()) && rightSelection.start().containerNode() == selection.start().containerNode(); if (rightSelectionIsOnWord) { VisibleSelection rangeSelection = VisibleSelection(rightSelection.visibleStart(), selection.visibleStart()); rightDistance = TextIterator::rangeLength(rangeSelection.toNormalizedRange().get()); } // Make sure we found an actual word. If not, return the original selection. if (!leftSelectionIsOnWord && !rightSelectionIsOnWord) return selection; if (!rightSelectionIsOnWord || (leftSelectionIsOnWord && leftDistance <= rightDistance)) { // Left is closer or right is invalid. return leftSelection; } // Right is closer or equal, or left was invalid. return rightSelection; } // No adjustment required. return selection; }
static void writeSelection(TextStream& ts, const RenderObject* o) { Node* n = o->node(); if (!n || !n->isDocumentNode()) return; Document* doc = static_cast<Document*>(n); Frame* frame = doc->frame(); if (!frame) return; VisibleSelection selection = frame->selection()->selection(); if (selection.isCaret()) { ts << "caret: position " << selection.start().deprecatedEditingOffset() << " of " << nodePosition(selection.start().node()); if (selection.affinity() == UPSTREAM) ts << " (upstream affinity)"; ts << "\n"; } else if (selection.isRange()) ts << "selection start: position " << selection.start().deprecatedEditingOffset() << " of " << nodePosition(selection.start().node()) << "\n" << "selection end: position " << selection.end().deprecatedEditingOffset() << " of " << nodePosition(selection.end().node()) << "\n"; }
void visibleTextQuads(const VisibleSelection& selection, Vector<FloatQuad>& quads) { if (!selection.isRange()) return; // Make sure that both start and end have valid nodes associated otherwise // this can crash. See PR 220628. if (!selection.start().anchorNode() || !selection.end().anchorNode()) return; visibleTextQuads(*(selection.firstRange()), quads, true /* useSelectionHeight */); }
// FIXME: We shouldn't need to take selectionForInsertion. It should be identical to SelectionController's current selection. void TypingCommand::insertText(Document* document, const String& text, const VisibleSelection& selectionForInsertion, Options options, TextCompositionType compositionType) { ASSERT(document); RefPtr<Frame> frame = document->frame(); ASSERT(frame); VisibleSelection currentSelection = frame->selection()->selection(); bool changeSelection = currentSelection != selectionForInsertion; String newText = text; Node* startNode = selectionForInsertion.start().deprecatedNode(); if (startNode && startNode->rootEditableElement() && compositionType != TextCompositionUpdate) { // Send BeforeTextInsertedEvent. The event handler will update text if necessary. ExceptionCode ec = 0; RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text); startNode->rootEditableElement()->dispatchEvent(evt, ec); newText = evt->text(); } if (newText.isEmpty()) return; // Set the starting and ending selection appropriately if we are using a selection // that is different from the current selection. In the future, we should change EditCommand // to deal with custom selections in a general way that can be used by all of the commands. RefPtr<EditCommand> lastEditCommand = frame->editor()->lastEditCommand(); if (isOpenForMoreTypingCommand(lastEditCommand.get())) { TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand.get()); if (lastTypingCommand->endingSelection() != selectionForInsertion) { lastTypingCommand->setStartingSelection(selectionForInsertion); lastTypingCommand->setEndingSelection(selectionForInsertion); } lastTypingCommand->setCompositionType(compositionType); lastTypingCommand->setShouldRetainAutocorrectionIndicator(options & RetainAutocorrectionIndicator); lastTypingCommand->setShouldPreventSpellChecking(options & PreventSpellChecking); lastTypingCommand->insertText(newText, options & SelectInsertedText); return; } RefPtr<TypingCommand> cmd = TypingCommand::create(document, InsertText, newText, options, compositionType); if (changeSelection) { cmd->setStartingSelection(selectionForInsertion); cmd->setEndingSelection(selectionForInsertion); } applyCommand(cmd); if (changeSelection) { cmd->setEndingSelection(currentSelection); frame->selection()->setSelection(currentSelection); } }
void SpellChecker::respondToChangedSelection(const VisibleSelection& oldSelection, FrameSelection::SetSelectionOptions options) { bool closeTyping = options & FrameSelection::CloseTyping; bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); if (isContinuousSpellCheckingEnabled) { VisibleSelection newAdjacentWords; VisibleSelection newSelectedSentence; const VisibleSelection newSelection = m_frame.selection().selection(); VisiblePosition newStart(newSelection.visibleStart()); newAdjacentWords = VisibleSelection(startOfWord(newStart, LeftWordIfOnBoundary), endOfWord(newStart, RightWordIfOnBoundary)); if (isContinuousGrammarCheckingEnabled) newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. bool shouldCheckSpellingAndGrammar = !(options & FrameSelection::SpellCorrectionTriggered); // When typing we check spelling elsewhere, so don't redo it here. // If this is a change in selection resulting from a delete operation, // oldSelection may no longer be in the document. // FIXME(http://crbug.com/382809): if oldSelection is on a textarea // element, we cause synchronous layout. if (shouldCheckSpellingAndGrammar && closeTyping && !isSelectionInTextField(oldSelection) && (isSelectionInTextArea(oldSelection) || oldSelection.isContentEditable()) && oldSelection.start().inDocument()) { spellCheckOldSelection(oldSelection, newAdjacentWords); } // FIXME(http://crbug.com/382809): // shouldEraseMarkersAfterChangeSelection is true, we cause synchronous // layout. if (textChecker().shouldEraseMarkersAfterChangeSelection(TextCheckingTypeSpelling)) { if (RefPtr<Range> wordRange = newAdjacentWords.toNormalizedRange()) m_frame.document()->markers().removeMarkers(wordRange.get(), DocumentMarker::Spelling); } if (textChecker().shouldEraseMarkersAfterChangeSelection(TextCheckingTypeGrammar)) { if (RefPtr<Range> sentenceRange = newSelectedSentence.toNormalizedRange()) m_frame.document()->markers().removeMarkers(sentenceRange.get(), DocumentMarker::Grammar); } } // When continuous spell checking is off, existing markers disappear after the selection changes. if (!isContinuousSpellCheckingEnabled) m_frame.document()->markers().removeMarkers(DocumentMarker::Spelling); if (!isContinuousGrammarCheckingEnabled) m_frame.document()->markers().removeMarkers(DocumentMarker::Grammar); }
String dispatchBeforeTextInsertedEvent(const String& text, const VisibleSelection& selectionForInsertion, bool insertionIsForUpdatingComposition) { if (insertionIsForUpdatingComposition) return text; String newText = text; if (Node* startNode = selectionForInsertion.start().containerNode()) { if (startNode->rootEditableElement()) { // Send BeforeTextInsertedEvent. The event handler will update text if necessary. RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text); startNode->rootEditableElement()->dispatchEvent(evt, IGNORE_EXCEPTION); newText = evt->text(); } } return newText; }
DeleteSelectionCommand::DeleteSelectionCommand(const VisibleSelection& selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements) : CompositeEditCommand(selection.start().anchorNode()->document()) , m_hasSelectionToDelete(true) , m_smartDelete(smartDelete) , m_mergeBlocksAfterDelete(mergeBlocksAfterDelete) , m_needPlaceholder(false) , m_replace(replace) , m_expandForSpecialElements(expandForSpecialElements) , m_pruneStartBlockIfNecessary(false) , m_startsAtEmptyLine(false) , m_selectionToDelete(selection) , m_startBlock(0) , m_endBlock(0) , m_typingStyle(0) , m_deleteIntoBlockquoteStyle(0) { }
TEST_F(SelectionAdjusterTest, adjustSelectionInDOMTree) { setBodyContent("<div id=sample>foo</div>"); MockVisibleSelectionChangeObserver selectionObserver; VisibleSelection selection; selection.setChangeObserver(selectionObserver); Node* const sample = document().getElementById("sample"); Node* const foo = sample->firstChild(); // Select "foo" VisibleSelectionInFlatTree selectionInFlatTree( PositionInFlatTree(foo, 0), PositionInFlatTree(foo, 3)); SelectionAdjuster::adjustSelectionInDOMTree(&selection, selectionInFlatTree); EXPECT_EQ(Position(foo, 0), selection.start()); EXPECT_EQ(Position(foo, 3), selection.end()); EXPECT_EQ(1, selectionObserver.callCounter()) << "adjustSelectionInDOMTree() should call didChangeVisibleSelection()"; }
void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, FrameSelection::SetSelectionOptions options) { // If the new selection is orphaned, then don't update the selection. if (newSelection.start().isOrphan() || newSelection.end().isOrphan()) return; // See <rdar://problem/5729315> Some shouldChangeSelectedDOMRange contain Ranges for selections that are no longer valid bool selectionDidNotChangeDOMPosition = newSelection == m_frame.selection().selection(); m_frame.selection().setSelection(newSelection, options); // Some editing operations change the selection visually without affecting its position within the DOM. // For example when you press return in the following (the caret is marked by ^): // <div contentEditable="true"><div>^Hello</div></div> // WebCore inserts <div><br></div> *before* the current block, which correctly moves the paragraph down but which doesn't // change the caret's DOM position (["hello", 0]). In these situations the above FrameSelection::setSelection call // does not call EditorClient::respondToChangedSelection(), which, on the Mac, sends selection change notifications and // starts a new kill ring sequence, but we want to do these things (matches AppKit). if (selectionDidNotChangeDOMPosition) client().respondToChangedSelection(&m_frame, m_frame.selection().selectionType()); }
static gchar* webkit_accessible_text_get_selection(AtkText* text, gint selection_num, gint* start_offset, gint* end_offset) { AccessibilityObject* coreObject = core(text); VisibleSelection selection = coreObject->selection(); // WebCore does not support multiple selection, so anything but 0 does not make sense for now. // Also, we don't want to do anything if the selection does not // belong to the currently selected object. We have to check since // there's no way to get the selection for a given object, only // the global one (the API is a bit confusing) if (selection_num != 0 || !selectionBelongsToObject(coreObject, selection)) { *start_offset = *end_offset = 0; return NULL; } *start_offset = selection.start().offsetInContainerNode(); *end_offset = selection.end().offsetInContainerNode(); return webkit_accessible_text_get_text(text, *start_offset, *end_offset); }
// This needs to be static so it can be called by canIncreaseSelectionListLevel and canDecreaseSelectionListLevel static bool getStartEndListChildren(const VisibleSelection& selection, Node*& start, Node*& end) { if (selection.isNone()) return false; // start must be in a list child Node* startListChild = enclosingListChild(selection.start().anchorNode()); if (!startListChild) return false; // end must be in a list child Node* endListChild = selection.isRange() ? enclosingListChild(selection.end().anchorNode()) : startListChild; if (!endListChild) return false; // For a range selection we want the following behavior: // - the start and end must be within the same overall list // - the start must be at or above the level of the rest of the range // - if the end is anywhere in a sublist lower than start, the whole sublist gets moved // In terms of this function, this means: // - endListChild must start out being be a sibling of startListChild, or be in a // sublist of startListChild or a sibling // - if endListChild is in a sublist of startListChild or a sibling, it must be adjusted // to be the ancestor that is startListChild or its sibling while (startListChild->parentNode() != endListChild->parentNode()) { endListChild = endListChild->parentNode(); if (!endListChild) return false; } // if the selection ends on a list item with a sublist, include the entire sublist if (endListChild->renderer()->isListItem()) { RenderObject* r = endListChild->renderer()->nextSibling(); if (r && isListHTMLElement(r->node())) endListChild = r->node(); } start = startListChild; end = endListChild; return true; }
Position adjustedSelectionStartForStyleComputation(const VisibleSelection& selection) { // This function is used by range style computations to avoid bugs like: // <rdar://problem/4017641> REGRESSION (Mail): you can only bold/unbold a selection starting from end of line once // It is important to skip certain irrelevant content at the start of the selection, so we do not wind up // with a spurious "mixed" style. VisiblePosition visiblePosition = selection.start(); if (visiblePosition.isNull()) return Position(); // if the selection is a caret, just return the position, since the style // behind us is relevant if (selection.isCaret()) return visiblePosition.deepEquivalent(); // if the selection starts just before a paragraph break, skip over it if (isEndOfParagraph(visiblePosition)) return visiblePosition.next().deepEquivalent().downstream(); // otherwise, make sure to be at the start of the first selected node, // instead of possibly at the end of the last node before the selection return visiblePosition.deepEquivalent().downstream(); }
static bool isSpellCheckingEnabledFor(const VisibleSelection& selection) { if (selection.isNone()) return false; return isSpellCheckingEnabledFor(selection.start()); }
static Position focusPosition(const VisibleSelection& selection) { Position focus = selection.isBaseFirst() ? selection.end() : selection.start(); return focus.parentAnchoredEquivalent(); }
TEST_F(SurroundingTextTest, BasicCaretSelection) { setHTML(String("<p id='selection'>foo bar</p>")); { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 1); EXPECT_EQ("f", surroundingText.content()); EXPECT_EQ(0u, surroundingText.startOffsetInContent()); EXPECT_EQ(0u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 5); // maxlength/2 is used on the left and right. EXPECT_EQ("foo", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(1u, surroundingText.startOffsetInContent()); EXPECT_EQ(1u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(0); SurroundingText surroundingText(selection.start(), 42); EXPECT_EQ("foo bar", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(1u, surroundingText.startOffsetInContent()); EXPECT_EQ(1u, surroundingText.endOffsetInContent()); } { // FIXME: if the selection is at the end of the text, SurroundingText // will return nothing. VisibleSelection selection = select(7); SurroundingText surroundingText(selection.start(), 42); EXPECT_EQ(0u, surroundingText.content().length()); EXPECT_EQ(0u, surroundingText.startOffsetInContent()); EXPECT_EQ(0u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(6); SurroundingText surroundingText(selection.start(), 2); EXPECT_EQ("ar", surroundingText.content()); EXPECT_EQ(1u, surroundingText.startOffsetInContent()); EXPECT_EQ(1u, surroundingText.endOffsetInContent()); } { VisibleSelection selection = select(6); SurroundingText surroundingText(selection.start(), 42); EXPECT_EQ("foo bar", surroundingText.content().simplifyWhiteSpace()); EXPECT_EQ(7u, surroundingText.startOffsetInContent()); EXPECT_EQ(7u, surroundingText.endOffsetInContent()); } }
bool isSelectionInTextFormControl(const VisibleSelection& selection) { return !!enclosingTextFormControl(selection.start()); }
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) { Frame* frame = document()->frame(); if (!frame) return; frame->editor()->updateMarkersForWordsAffectedByEditing(false); VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; switch (endingSelection().selectionType()) { case VisibleSelection::RangeSelection: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case VisibleSelection::CaretSelection: { m_smartDelete = false; // Handle delete at beginning-of-block case. // Do nothing in the case that the caret is at the start of a // root editable element or at the start of a document. FrameSelection selection; selection.setSelection(endingSelection()); selection.modify(FrameSelection::AlterationExtend, DirectionForward, granularity); if (killRing && selection.isCaret() && granularity != CharacterGranularity) selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); Position downstreamEnd = endingSelection().end().downstream(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); Node* enclosingTableCell = enclosingNodeOfType(visibleEnd.deepEquivalent(), &isTableCell); if (enclosingTableCell && visibleEnd == lastPositionInNode(enclosingTableCell)) return; if (visibleEnd == endOfParagraph(visibleEnd)) downstreamEnd = visibleEnd.next(CannotCrossEditingBoundary).deepEquivalent().downstream(); // When deleting tables: Select the table first, then perform the deletion if (downstreamEnd.containerNode() && downstreamEnd.containerNode()->renderer() && downstreamEnd.containerNode()->renderer()->isTable() && downstreamEnd.computeOffsetInContainerNode() <= caretMinOffset(downstreamEnd.containerNode())) { setEndingSelection(VisibleSelection(endingSelection().end(), positionAfterNode(downstreamEnd.containerNode()), DOWNSTREAM, endingSelection().isDirectional())); typingAddedToOpenCommand(ForwardDeleteKey); return; } // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any) if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd())) selection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); selectionToDelete = selection.selection(); if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) selectionAfterUndo = selectionToDelete; else { // It's a little tricky to compute what the starting selection would have been in the original document. // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on // the current state of the document and we'll get the wrong result. Position extent = startingSelection().end(); if (extent.containerNode() != selectionToDelete.end().containerNode()) extent = selectionToDelete.extent(); else { int extraCharacters; if (selectionToDelete.start().containerNode() == selectionToDelete.end().containerNode()) extraCharacters = selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode(); else extraCharacters = selectionToDelete.end().computeOffsetInContainerNode(); extent = Position(extent.containerNode(), extent.computeOffsetInContainerNode() + extraCharacters, Position::PositionIsOffsetInAnchor); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } break; } case VisibleSelection::NoSelection: ASSERT_NOT_REACHED(); break; } ASSERT(!selectionToDelete.isNone()); if (selectionToDelete.isNone()) return; if (selectionToDelete.isCaret() || !frame->selection()->shouldDeleteSelection(selectionToDelete)) return; if (killRing) frame->editor()->addToKillRing(selectionToDelete.toNormalizedRange().get(), false); // make undo select what was deleted setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(ForwardDeleteKey); }
void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) { Frame* frame = document()->frame(); if (!frame) return; frame->editor()->updateMarkersForWordsAffectedByEditing(false); VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; switch (endingSelection().selectionType()) { case VisibleSelection::RangeSelection: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case VisibleSelection::CaretSelection: { // After breaking out of an empty mail blockquote, we still want continue with the deletion // so actual content will get deleted, and not just the quote style. if (breakOutOfEmptyMailBlockquotedParagraph()) typingAddedToOpenCommand(DeleteKey); m_smartDelete = false; FrameSelection selection; selection.setSelection(endingSelection()); selection.modify(FrameSelection::AlterationExtend, DirectionBackward, granularity); if (killRing && selection.isCaret() && granularity != CharacterGranularity) selection.modify(FrameSelection::AlterationExtend, DirectionBackward, CharacterGranularity); if (endingSelection().visibleStart().previous(CannotCrossEditingBoundary).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(DeleteKey); return; } // When there are no visible positions in the editing root, delete its entire contents. if (endingSelection().visibleStart().next(CannotCrossEditingBoundary).isNull() && makeEditableRootEmpty()) { typingAddedToOpenCommand(DeleteKey); return; } } VisiblePosition visibleStart(endingSelection().visibleStart()); // If we have a caret selection at the beginning of a cell, we have nothing to do. Node* enclosingTableCell = enclosingNodeOfType(visibleStart.deepEquivalent(), &isTableCell); if (enclosingTableCell && visibleStart == firstPositionInNode(enclosingTableCell)) return; // If the caret is at the start of a paragraph after a table, move content into the last table cell. if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(CannotCrossEditingBoundary))) { // Unless the caret is just before a table. We don't want to move a table into the last table cell. if (isLastPositionBeforeTable(visibleStart)) return; // Extend the selection backward into the last cell, then deletion will handle the move. selection.modify(FrameSelection::AlterationExtend, DirectionBackward, granularity); // If the caret is just after a table, select the table and don't delete anything. } else if (Node* table = isFirstPositionAfterTable(visibleStart)) { setEndingSelection(VisibleSelection(positionBeforeNode(table), endingSelection().start(), DOWNSTREAM, endingSelection().isDirectional())); typingAddedToOpenCommand(DeleteKey); return; } selectionToDelete = selection.selection(); if (granularity == CharacterGranularity && selectionToDelete.end().containerNode() == selectionToDelete.start().containerNode() && selectionToDelete.end().computeOffsetInContainerNode() - selectionToDelete.start().computeOffsetInContainerNode() > 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)); } if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) selectionAfterUndo = selectionToDelete; else // It's a little tricky to compute what the starting selection would have been in the original document. // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on // the current state of the document and we'll get the wrong result. selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent()); break; } case VisibleSelection::NoSelection: ASSERT_NOT_REACHED(); break; } ASSERT(!selectionToDelete.isNone()); if (selectionToDelete.isNone()) return; if (selectionToDelete.isCaret() || !frame->selection()->shouldDeleteSelection(selectionToDelete)) return; if (killRing) frame->editor()->addToKillRing(selectionToDelete.toNormalizedRange().get(), false); // Make undo select everything that has been deleted, unless an undo will undo more than just this deletion. // FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete // more text than you insert. In that case all of the text that was around originally should be selected. if (m_openedByBackwardDelete) setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(DeleteKey); }
static Position focusPosition(const VisibleSelection& selection) { Position focus = selection.isBaseFirst() ? selection.end() : selection.start(); return rangeCompliantEquivalent(focus); }
static Position anchorPosition(const VisibleSelection& selection) { Position anchor = selection.isBaseFirst() ? selection.start() : selection.end(); return rangeCompliantEquivalent(anchor); }
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) { VisibleSelection selectionToDelete; VisibleSelection selectionAfterUndo; switch (endingSelection().selectionType()) { case VisibleSelection::RangeSelection: selectionToDelete = endingSelection(); selectionAfterUndo = selectionToDelete; break; case VisibleSelection::CaretSelection: { m_smartDelete = false; // Handle delete at beginning-of-block case. // Do nothing in the case that the caret is at the start of a // root editable element or at the start of a document. SelectionController selection; selection.setSelection(endingSelection()); selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, granularity); if (killRing && selection.isCaret() && granularity != CharacterGranularity) selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity); Position downstreamEnd = endingSelection().end().downstream(); VisiblePosition visibleEnd = endingSelection().visibleEnd(); 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.deprecatedEditingOffset() == 0) { setEndingSelection(VisibleSelection(endingSelection().end(), lastDeepEditingPositionForNode(downstreamEnd.node()), DOWNSTREAM)); typingAddedToOpenCommand(ForwardDeleteKey); return; } // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any) if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd())) selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity); selectionToDelete = selection.selection(); if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) selectionAfterUndo = selectionToDelete; else { // It's a little tricky to compute what the starting selection would have been in the original document. // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on // the current state of the document and we'll get the wrong result. Position extent = startingSelection().end(); if (extent.node() != selectionToDelete.end().node()) extent = selectionToDelete.extent(); else { int extraCharacters; if (selectionToDelete.start().node() == selectionToDelete.end().node()) extraCharacters = selectionToDelete.end().deprecatedEditingOffset() - selectionToDelete.start().deprecatedEditingOffset(); else extraCharacters = selectionToDelete.end().deprecatedEditingOffset(); extent = Position(extent.node(), extent.deprecatedEditingOffset() + extraCharacters); } selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); } break; } case VisibleSelection::NoSelection: ASSERT_NOT_REACHED(); break; } ASSERT(!selectionToDelete.isNone()); if (selectionToDelete.isNone()) return; if (selectionToDelete.isCaret() || !document()->frame()->shouldDeleteSelection(selectionToDelete)) return; if (killRing) document()->frame()->editor()->addToKillRing(selectionToDelete.toNormalizedRange().get(), false); // make undo select what was deleted setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); typingAddedToOpenCommand(ForwardDeleteKey); }
static void getSelectionOffsetsForObject(AccessibilityObject* coreObject, VisibleSelection& selection, gint& startOffset, gint& endOffset) { // Default values, unless the contrary is proved. startOffset = 0; endOffset = 0; Node* node = getNodeForAccessibilityObject(coreObject); if (!node) return; if (selection.isNone()) return; // We need to limit our search to positions that fall inside the domain of the current object. Position firstValidPosition = firstPositionInOrBeforeNode(node->firstDescendant()); Position lastValidPosition = lastPositionInOrAfterNode(node->lastDescendant()); // Early return with proper values if the selection falls entirely out of the object. if (!selectionBelongsToObject(coreObject, selection)) { startOffset = comparePositions(selection.start(), firstValidPosition) < 0 ? 0 : accessibilityObjectLength(coreObject); endOffset = startOffset; return; } // Find the proper range for the selection that falls inside the object. Position nodeRangeStart = selection.start(); if (comparePositions(nodeRangeStart, firstValidPosition) < 0) nodeRangeStart = firstValidPosition; Position nodeRangeEnd = selection.end(); if (comparePositions(nodeRangeEnd, lastValidPosition) > 0) nodeRangeEnd = lastValidPosition; // Calculate position of the selected range inside the object. Position parentFirstPosition = firstPositionInOrBeforeNode(node); RefPtr<Range> rangeInParent = Range::create(&node->document(), parentFirstPosition, nodeRangeStart); // Set values for start offsets and calculate initial range length. // These values might be adjusted later to cover special cases. startOffset = webCoreOffsetToAtkOffset(coreObject, TextIterator::rangeLength(rangeInParent.get(), true)); RefPtr<Range> nodeRange = Range::create(&node->document(), nodeRangeStart, nodeRangeEnd); int rangeLength = TextIterator::rangeLength(nodeRange.get(), true); // Special cases that are only relevant when working with *_END boundaries. if (selection.affinity() == UPSTREAM) { VisiblePosition visibleStart(nodeRangeStart, UPSTREAM); VisiblePosition visibleEnd(nodeRangeEnd, UPSTREAM); // We need to adjust offsets when finding wrapped lines so the position at the end // of the line is properly taking into account when calculating the offsets. if (isEndOfLine(visibleStart) && !lineBreakExistsAtVisiblePosition(visibleStart)) { if (isStartOfLine(visibleStart.next())) rangeLength++; if (!isEndOfBlock(visibleStart)) startOffset = std::max(startOffset - 1, 0); } if (isEndOfLine(visibleEnd) && !lineBreakExistsAtVisiblePosition(visibleEnd) && !isEndOfBlock(visibleEnd)) rangeLength--; } endOffset = std::min(startOffset + rangeLength, static_cast<int>(accessibilityObjectLength(coreObject))); }