From 166beb485a5bdb2cfc5b32b6777655b40df34e21 Mon Sep 17 00:00:00 2001 From: "aaronleventhal@moonset.net" Date: Tue, 18 Sep 2007 15:28:26 -0700 Subject: [PATCH] bug 387273. Incorrect starting offset exposed when selection spans multiple objects. r=surkov, r=david.bolter, a=dsicore --- accessible/src/html/nsHyperTextAccessible.cpp | 157 +++++++++++++----- accessible/src/html/nsHyperTextAccessible.h | 21 ++- 2 files changed, 131 insertions(+), 47 deletions(-) diff --git a/accessible/src/html/nsHyperTextAccessible.cpp b/accessible/src/html/nsHyperTextAccessible.cpp index e2fb6138fd05..9d939a97e98f 100644 --- a/accessible/src/html/nsHyperTextAccessible.cpp +++ b/accessible/src/html/nsHyperTextAccessible.cpp @@ -58,6 +58,7 @@ #include "nsIFrame.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIPlaintextEditor.h" +#include "nsISelection2.h" #include "nsIServiceManager.h" #include "nsTextFragment.h" #include "gfxSkipChars.h" @@ -105,7 +106,12 @@ nsresult nsHyperTextAccessible::QueryInterface(REFNSIID aIID, void** aInstancePt if (aIID.Equals(NS_GET_IID(nsIAccessibleHyperText))) { if (role == nsIAccessibleRole::ROLE_ENTRY || role == nsIAccessibleRole::ROLE_PASSWORD_TEXT) { - return NS_ERROR_NO_INTERFACE; + nsCOMPtr editor; + GetAssociatedEditor(getter_AddRefs(editor)); + nsCOMPtr peditor(do_QueryInterface(editor)); + if (peditor) { + return NS_ERROR_NO_INTERFACE; // No embedded objects ever in plain text + } } *aInstancePtr = static_cast(this); NS_ADDREF_THIS(); @@ -533,7 +539,8 @@ NS_IMETHODIMP nsHyperTextAccessible::GetCharacterAtOffset(PRInt32 aOffset, PRUni nsresult nsHyperTextAccessible::DOMPointToHypertextOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset, PRInt32* aHyperTextOffset, - nsIAccessible **aFinalAccessible) + nsIAccessible **aFinalAccessible, + PRBool aIsEndOffset) { // Turn a DOM Node and offset into an offset into this hypertext. // On failure, return null. On success, return the DOM node which contains the offset. @@ -609,10 +616,19 @@ nsresult nsHyperTextAccessible::DOMPointToHypertextOffset(nsIDOMNode* aNode, PRI //
abc

def

ghi
// If the passed-in DOM point was not on a direct child of the hypertext, we will // return the offset for that entire hypertext - // If the offset was after the first character of the passed in object, we will now use 1 for + if (aIsEndOffset) { + // Not inclusive, the indicated char comes at index before this offset + // If the end offset is after the first character of the passed in object, use 1 for // addTextOffset, to put us after the embedded object char. We'll only treat the offset as // before the embedded object char if we end at the very beginning of the child. addTextOffset = addTextOffset > 0; + } + else { + // Start offset, inclusive + // Make sure the offset lands on the embedded object character in order to indicate + // the true inner offset is inside the subtree for that link + addTextOffset = (TextLength(descendantAccessible) == addTextOffset) ? 1 : 0; + } descendantAccessible = parentAccessible; } @@ -699,7 +715,9 @@ nsHyperTextAccessible::GetRelativeOffset(nsIPresShell *aPresShell, NS_ENSURE_TRUE(resultNode, -1); nsCOMPtr finalAccessible; - rv = DOMPointToHypertextOffset(resultNode, pos.mContentOffset, &hyperTextOffset, getter_AddRefs(finalAccessible)); + rv = DOMPointToHypertextOffset(resultNode, pos.mContentOffset, &hyperTextOffset, + getter_AddRefs(finalAccessible), + aDirection == eDirNext); // If finalAccessible == nsnull, then DOMPointToHypertextOffset() searched through the hypertext // children without finding the node/offset position NS_ENSURE_SUCCESS(rv, -1); @@ -1371,49 +1389,89 @@ NS_IMETHODIMP nsHyperTextAccessible::GetCaretOffset(PRInt32 *aCaretOffset) return DOMPointToHypertextOffset(caretNode, caretOffset, aCaretOffset); } -nsresult nsHyperTextAccessible::GetSelections(nsISelectionController **aSelCon, nsISelection **aDomSel) +nsresult nsHyperTextAccessible::GetSelections(nsISelectionController **aSelCon, + nsISelection **aDomSel, + nsCOMArray* aRanges) { + if (!mDOMNode) { + return NS_ERROR_FAILURE; + } if (aSelCon) { *aSelCon = nsnull; } if (aDomSel) { *aDomSel = nsnull; } + if (aRanges) { + aRanges->Clear(); + } + nsCOMPtr domSel; + nsCOMPtr selCon; + nsCOMPtr editor; GetAssociatedEditor(getter_AddRefs(editor)); - if (editor) { + nsCOMPtr peditor(do_QueryInterface(editor)); + if (peditor) { + // Case 1: plain text editor + // This is for form controls which have their own + // selection controller separate from the document, for example + // HTML:input, HTML:textarea, XUL:textbox, etc. if (aSelCon) { - editor->GetSelectionController(aSelCon); + editor->GetSelectionController(getter_AddRefs(selCon)); NS_ENSURE_TRUE(*aSelCon, NS_ERROR_FAILURE); } - if (aDomSel) { - editor->GetSelection(aDomSel); - NS_ENSURE_TRUE(*aDomSel, NS_ERROR_FAILURE); + editor->GetSelection(getter_AddRefs(domSel)); + NS_ENSURE_TRUE(domSel, NS_ERROR_FAILURE); } + else { + // Case 2: rich content subtree (can be rich editor) + // This uses the selection controller from the entire document + nsIFrame *frame = GetFrame(); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); - return NS_OK; + // Get the selection and selection controller + frame->GetSelectionController(GetPresContext(), + getter_AddRefs(selCon)); + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(domSel)); + NS_ENSURE_TRUE(domSel, NS_ERROR_FAILURE); } - nsIFrame *frame = GetFrame(); - NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); - - // Get the selection and selection controller - nsCOMPtr selCon; - frame->GetSelectionController(GetPresContext(), - getter_AddRefs(selCon)); - NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); if (aSelCon) { NS_ADDREF(*aSelCon = selCon); } - if (aDomSel) { - nsCOMPtr domSel; - selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(domSel)); - NS_ENSURE_TRUE(domSel, NS_ERROR_FAILURE); NS_ADDREF(*aDomSel = domSel); } + if (aRanges) { + nsCOMPtr selection2(do_QueryInterface(domSel)); + NS_ENSURE_TRUE(selection2, NS_ERROR_FAILURE); + + nsCOMPtr childNodes; + nsresult rv = mDOMNode->GetChildNodes(getter_AddRefs(childNodes)); + NS_ENSURE_SUCCESS(rv, rv); + PRUint32 numChildren; + rv = childNodes->GetLength(&numChildren); + NS_ENSURE_SUCCESS(rv, rv); + rv = selection2->GetRangesForIntervalCOMArray(mDOMNode, 0, + mDOMNode, numChildren, + PR_TRUE, aRanges); + NS_ENSURE_SUCCESS(rv, rv); + // Remove collapsed ranges + PRInt32 numRanges = aRanges->Count(); + for (PRInt32 count = 0; count < numRanges; count ++) { + PRBool isCollapsed; + (*aRanges)[count]->GetCollapsed(&isCollapsed); + if (isCollapsed) { + aRanges->RemoveObjectAt(count); + -- numRanges; + -- count; + } + } + } return NS_OK; } @@ -1424,18 +1482,13 @@ nsresult nsHyperTextAccessible::GetSelections(nsISelectionController **aSelCon, NS_IMETHODIMP nsHyperTextAccessible::GetSelectionCount(PRInt32 *aSelectionCount) { nsCOMPtr domSel; - nsresult rv = GetSelections(nsnull, getter_AddRefs(domSel)); + nsCOMArray ranges; + nsresult rv = GetSelections(nsnull, nsnull, &ranges); NS_ENSURE_SUCCESS(rv, rv); - PRBool isSelectionCollapsed; - rv = domSel->GetIsCollapsed(&isSelectionCollapsed); - NS_ENSURE_SUCCESS(rv, rv); + *aSelectionCount = ranges.Count(); - if (isSelectionCollapsed) { - *aSelectionCount = 0; - return NS_OK; - } - return domSel->GetRangeCount(aSelectionCount); + return NS_OK; } /* @@ -1446,35 +1499,49 @@ NS_IMETHODIMP nsHyperTextAccessible::GetSelectionBounds(PRInt32 aSelectionNum, P *aStartOffset = *aEndOffset = 0; nsCOMPtr domSel; - nsresult rv = GetSelections(nsnull, getter_AddRefs(domSel)); + nsCOMArray ranges; + nsresult rv = GetSelections(nsnull, getter_AddRefs(domSel), &ranges); NS_ENSURE_SUCCESS(rv, rv); - PRInt32 rangeCount; - domSel->GetRangeCount(&rangeCount); + PRInt32 rangeCount = ranges.Count(); if (aSelectionNum < 0 || aSelectionNum >= rangeCount) return NS_ERROR_INVALID_ARG; - nsCOMPtr range; - rv = domSel->GetRangeAt(aSelectionNum, getter_AddRefs(range)); - NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr range = ranges[aSelectionNum]; + // Get start point nsCOMPtr startNode; range->GetStartContainer(getter_AddRefs(startNode)); PRInt32 startOffset; range->GetStartOffset(&startOffset); - rv = DOMPointToHypertextOffset(startNode, startOffset, aStartOffset); - NS_ENSURE_SUCCESS(rv, rv); + // Get end point nsCOMPtr endNode; range->GetEndContainer(getter_AddRefs(endNode)); PRInt32 endOffset; range->GetEndOffset(&endOffset); - if (startNode == endNode && startOffset == endOffset) { - // Shortcut for collapsed selection case (caret) - *aEndOffset = *aStartOffset; - return NS_OK; + + PRInt16 rangeCompareResult; + rv = range->CompareBoundaryPoints(nsIDOMRange::START_TO_END, range, &rangeCompareResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (rangeCompareResult < 0) { + // Make sure start is before end, by swapping offsets + // This occurs when the user selects backwards in the text + startNode.swap(endNode); + PRInt32 tempOffset = startOffset; + startOffset = endOffset; + endOffset = tempOffset; } - return DOMPointToHypertextOffset(endNode, endOffset, aEndOffset); + + nsCOMPtr startAccessible; + rv = DOMPointToHypertextOffset(startNode, startOffset, aStartOffset, getter_AddRefs(startAccessible)); + NS_ENSURE_SUCCESS(rv, rv); + if (!startAccessible) { + *aStartOffset = 0; // Could not find start point within this hypertext, so starts before + } + + return DOMPointToHypertextOffset(endNode, endOffset, aEndOffset, nsnull, PR_TRUE); } /* diff --git a/accessible/src/html/nsHyperTextAccessible.h b/accessible/src/html/nsHyperTextAccessible.h index 4a7a247a89c6..8d09a34c0e97 100644 --- a/accessible/src/html/nsHyperTextAccessible.h +++ b/accessible/src/html/nsHyperTextAccessible.h @@ -110,10 +110,18 @@ public: * contained the offset, if it is within * the current nsHyperTextAccessible, * otherwise it is set to nsnull. + * @param aIsEndOffset - if PR_TRUE, then then this offset is not inclusive. The character + * indicated by the offset returned is at [offset - 1]. This means + * if the passed-in offset is really in a descendant, then the offset returned + * will come just after the relevant embedded object characer. + * If PR_FALSE, then the offset is inclusive. The character indicated + * by the offset returned is at [offset]. If the passed-in offset in inside a + * descendant, then the returned offset will be on the relevant embedded object char. */ nsresult DOMPointToHypertextOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset, PRInt32 *aHypertextOffset, - nsIAccessible **aFinalAccessible = nsnull); + nsIAccessible **aFinalAccessible = nsnull, + PRBool aIsEndOffset = PR_FALSE); protected: /* @@ -184,7 +192,16 @@ protected: nsIntRect GetBoundsForString(nsIFrame *aFrame, PRUint32 aStartRenderedOffset, PRUint32 aEndRenderedOffset); // Selection helpers - nsresult GetSelections(nsISelectionController **aSelCon, nsISelection **aDomSel); + + /** + * Get the relevant selection interfaces and ranges for the current hyper text + * @param aSelCon The selection controller for the current hyper text, or nsnull if not needed + * @param aDomSel The selection interface for the current hyper text, or nsnull if not needed + * @param aRanges The selected ranges within the current subtree, or nsnull if not needed + */ + nsresult GetSelections(nsISelectionController **aSelCon, + nsISelection **aDomSel = nsnull, + nsCOMArray* aRanges = nsnull); nsresult SetSelectionRange(PRInt32 aStartPos, PRInt32 aEndPos); };