diff --git a/content/base/src/nsGenericElement.cpp b/content/base/src/nsGenericElement.cpp index 471108ed33f9..5fa68a145b32 100644 --- a/content/base/src/nsGenericElement.cpp +++ b/content/base/src/nsGenericElement.cpp @@ -118,7 +118,7 @@ #include "nsIDOMNSFeatureFactory.h" #include "nsIDOMDocumentType.h" #include "nsIDOMUserDataHandler.h" -#include "nsIDOMNSEditableElement.h" +#include "nsGenericHTMLElement.h" #include "nsIEditor.h" #include "nsIEditorDocShell.h" #include "nsEventDispatcher.h" @@ -343,13 +343,15 @@ nsINode::GetTextEditorRootContent(nsIEditor** aEditor) if (aEditor) *aEditor = nsnull; for (nsINode* node = this; node; node = node->GetNodeParent()) { - nsCOMPtr editableElement(do_QueryInterface(node)); - if (!editableElement) + if (!node->IsNodeOfType(eHTML)) continue; nsCOMPtr editor; - editableElement->GetEditor(getter_AddRefs(editor)); - NS_ENSURE_TRUE(editor, nsnull); + static_cast(node)-> + GetEditorInternal(getter_AddRefs(editor)); + if (!editor) + continue; + nsIContent* rootContent = GetEditorRootContent(editor); if (aEditor) editor.swap(*aEditor); diff --git a/content/base/src/nsGkAtomList.h b/content/base/src/nsGkAtomList.h index ad16a6b1162a..c856ad8bb66a 100644 --- a/content/base/src/nsGkAtomList.h +++ b/content/base/src/nsGkAtomList.h @@ -62,7 +62,9 @@ //--------------------------------------------------------------------------- GK_ATOM(_empty, "") +GK_ATOM(moz, "_moz") GK_ATOM(mozdirty, "_moz_dirty") +GK_ATOM(mozeditorbogusnode, "_moz_editor_bogus_node") GK_ATOM(mozgeneratedcontentbefore, "_moz_generated_content_before") GK_ATOM(mozgeneratedcontentafter, "_moz_generated_content_after") GK_ATOM(mozgeneratedcontentimage, "_moz_generated_content_image") diff --git a/content/events/src/Makefile.in b/content/events/src/Makefile.in index 7c5e73c1cf3c..6d69bddef2a7 100644 --- a/content/events/src/Makefile.in +++ b/content/events/src/Makefile.in @@ -91,7 +91,7 @@ CPPSRCS = \ nsPLDOMEvent.cpp \ nsEventDispatcher.cpp \ nsIMEStateManager.cpp \ - nsQueryContentEventHandler.cpp \ + nsContentEventHandler.cpp \ nsDOMProgressEvent.cpp \ nsDOMDataTransfer.cpp \ nsDOMNotifyPaintEvent.cpp \ diff --git a/content/events/src/nsQueryContentEventHandler.cpp b/content/events/src/nsContentEventHandler.cpp similarity index 58% rename from content/events/src/nsQueryContentEventHandler.cpp rename to content/events/src/nsContentEventHandler.cpp index 1e4641a00f5e..078bb7dbbeb0 100644 --- a/content/events/src/nsQueryContentEventHandler.cpp +++ b/content/events/src/nsContentEventHandler.cpp @@ -22,6 +22,7 @@ * * Contributor(s): * Masayuki Nakano + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -37,7 +38,7 @@ * * ***** END LICENSE BLOCK ***** */ -#include "nsQueryContentEventHandler.h" +#include "nsContentEventHandler.h" #include "nsCOMPtr.h" #include "nsPresContext.h" #include "nsIPresShell.h" @@ -53,14 +54,19 @@ #include "nsIContentIterator.h" #include "nsTextFragment.h" #include "nsTextFrame.h" +#include "nsISelectionController.h" +#include "nsISelectionPrivate.h" +#include "nsContentUtils.h" +#include "nsISelection2.h" +#include "nsIMEStateManager.h" nsresult NS_NewContentIterator(nsIContentIterator** aInstancePtrResult); /******************************************************************/ -/* nsQueryContentEventHandler */ +/* nsContentEventHandler */ /******************************************************************/ -nsQueryContentEventHandler::nsQueryContentEventHandler( +nsContentEventHandler::nsContentEventHandler( nsPresContext* aPresContext) : mPresContext(aPresContext), mPresShell(aPresContext->GetPresShell()), mSelection(nsnull), @@ -69,7 +75,7 @@ nsQueryContentEventHandler::nsQueryContentEventHandler( } nsresult -nsQueryContentEventHandler::Init(nsQueryContentEvent* aEvent) +nsContentEventHandler::Init(nsQueryContentEvent* aEvent) { NS_ASSERTION(aEvent, "aEvent must not be null"); @@ -117,6 +123,24 @@ nsQueryContentEventHandler::Init(nsQueryContentEvent* aEvent) return NS_OK; } +// Editor places a bogus BR node under its root content if the editor doesn't +// have any text. This happens even for single line editors. +// When we get text content and when we change the selection, +// we don't want to include the bogus BRs at the end. +static PRBool IsContentBR(nsIContent* aContent) +{ + return aContent->IsNodeOfType(nsINode::eHTML) && + aContent->Tag() == nsGkAtoms::br && + !aContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::type, + nsGkAtoms::moz, + eIgnoreCase) && + !aContent->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::mozeditorbogusnode, + nsGkAtoms::_true, + eIgnoreCase); +} + static void ConvertToNativeNewlines(nsAFlatString& aString) { #if defined(XP_MACOSX) @@ -161,8 +185,7 @@ static PRUint32 GetNativeTextLength(nsIContent* aContent) nsAutoString str; if (aContent->IsNodeOfType(nsINode::eTEXT)) AppendString(str, aContent); - else if (aContent->IsNodeOfType(nsINode::eHTML) && - aContent->Tag() == nsGkAtoms::br) + else if (IsContentBR(aContent)) str.Assign(PRUnichar('\n')); ConvertToNativeNewlines(str); return str.Length(); @@ -181,9 +204,8 @@ static PRUint32 ConvertToXPOffset(nsIContent* aContent, PRUint32 aNativeOffset) return str.Length(); } -nsresult -nsQueryContentEventHandler::GenerateFlatTextContent(nsIRange* aRange, - nsAFlatString& aString) +static nsresult GenerateFlatTextContent(nsIRange* aRange, + nsAFlatString& aString) { nsCOMPtr iter; nsresult rv = NS_NewContentIterator(getter_AddRefs(iter)); @@ -221,8 +243,7 @@ nsQueryContentEventHandler::GenerateFlatTextContent(nsIRange* aRange, AppendSubString(aString, content, 0, aRange->EndOffset()); else AppendString(aString, content); - } else if (content->IsNodeOfType(nsINode::eHTML) && - content->Tag() == nsGkAtoms::br) + } else if (IsContentBR(content)) aString.Append(PRUnichar('\n')); } ConvertToNativeNewlines(aString); @@ -230,18 +251,19 @@ nsQueryContentEventHandler::GenerateFlatTextContent(nsIRange* aRange, } nsresult -nsQueryContentEventHandler::ExpandToClusterBoundary(nsIContent* aContent, +nsContentEventHandler::ExpandToClusterBoundary(nsIContent* aContent, PRBool aForward, PRUint32* aXPOffset) { - NS_ASSERTION(*aXPOffset >= 0 && *aXPOffset <= aContent->TextLength(), - "offset is out of range."); - // XXX This method assumes that the frame boundaries must be cluster // boundaries. It's false, but no problem now, maybe. if (!aContent->IsNodeOfType(nsINode::eTEXT) || *aXPOffset == 0 || *aXPOffset == aContent->TextLength()) return NS_OK; + + NS_ASSERTION(*aXPOffset >= 0 && *aXPOffset <= aContent->TextLength(), + "offset is out of range."); + nsCOMPtr fs = mPresShell->FrameSelection(); PRInt32 offsetInFrame; nsFrameSelection::HINT hint = @@ -265,7 +287,7 @@ nsQueryContentEventHandler::ExpandToClusterBoundary(nsIContent* aContent, if (frame->GetType() != nsGkAtoms::textFrame) return NS_ERROR_FAILURE; nsTextFrame* textFrame = static_cast(frame); - PRInt32 newOffsetInFrame = offsetInFrame; + PRInt32 newOffsetInFrame = *aXPOffset - startOffset; newOffsetInFrame += aForward ? -1 : 1; textFrame->PeekOffsetCharacter(aForward, &newOffsetInFrame); *aXPOffset = startOffset + newOffsetInFrame; @@ -273,7 +295,7 @@ nsQueryContentEventHandler::ExpandToClusterBoundary(nsIContent* aContent, } nsresult -nsQueryContentEventHandler::SetRangeFromFlatTextOffset( +nsContentEventHandler::SetRangeFromFlatTextOffset( nsIRange* aRange, PRUint32 aNativeOffset, PRUint32 aNativeLength, @@ -369,7 +391,7 @@ nsQueryContentEventHandler::SetRangeFromFlatTextOffset( } nsresult -nsQueryContentEventHandler::OnQuerySelectedText(nsQueryContentEvent* aEvent) +nsContentEventHandler::OnQuerySelectedText(nsQueryContentEvent* aEvent) { nsresult rv = Init(aEvent); if (NS_FAILED(rv)) @@ -378,21 +400,32 @@ nsQueryContentEventHandler::OnQuerySelectedText(nsQueryContentEvent* aEvent) NS_ASSERTION(aEvent->mReply.mString.IsEmpty(), "The reply string must be empty"); - rv = GetFlatTextOffsetOfRange(mFirstSelectedRange, &aEvent->mReply.mOffset); + rv = GetFlatTextOffsetOfRange(mRootContent, + mFirstSelectedRange, &aEvent->mReply.mOffset); NS_ENSURE_SUCCESS(rv, rv); - PRBool isCollapsed; - rv = mSelection->GetIsCollapsed(&isCollapsed); + nsCOMPtr anchorDomNode, focusDomNode; + rv = mSelection->GetAnchorNode(getter_AddRefs(anchorDomNode)); + NS_ENSURE_TRUE(anchorDomNode, NS_ERROR_FAILURE); + rv = mSelection->GetFocusNode(getter_AddRefs(focusDomNode)); + NS_ENSURE_TRUE(focusDomNode, NS_ERROR_FAILURE); + + PRInt32 anchorOffset, focusOffset; + rv = mSelection->GetAnchorOffset(&anchorOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = mSelection->GetFocusOffset(&focusOffset); NS_ENSURE_SUCCESS(rv, rv); - if (!isCollapsed) { - nsCOMPtr domRange; - rv = mSelection->GetRangeAt(0, getter_AddRefs(domRange)); - NS_ENSURE_SUCCESS(rv, rv); - NS_ASSERTION(domRange, "GetRangeAt succeeded, but the result is null"); + nsCOMPtr anchorNode(do_QueryInterface(anchorDomNode)); + nsCOMPtr focusNode(do_QueryInterface(focusDomNode)); + NS_ENSURE_TRUE(anchorNode && focusNode, NS_ERROR_UNEXPECTED); - nsCOMPtr range(do_QueryInterface(domRange)); - NS_ENSURE_TRUE(range, NS_ERROR_FAILURE); + PRInt16 compare = nsContentUtils::ComparePoints(anchorNode, anchorOffset, + focusNode, focusOffset); + aEvent->mReply.mReversed = compare > 0; + + if (compare) { + nsCOMPtr range = mFirstSelectedRange; rv = GenerateFlatTextContent(range, aEvent->mReply.mString); NS_ENSURE_SUCCESS(rv, rv); } @@ -402,7 +435,7 @@ nsQueryContentEventHandler::OnQuerySelectedText(nsQueryContentEvent* aEvent) } nsresult -nsQueryContentEventHandler::OnQueryTextContent(nsQueryContentEvent* aEvent) +nsContentEventHandler::OnQueryTextContent(nsQueryContentEvent* aEvent) { nsresult rv = Init(aEvent); if (NS_FAILED(rv)) @@ -425,65 +458,168 @@ nsQueryContentEventHandler::OnQueryTextContent(nsQueryContentEvent* aEvent) return NS_OK; } -nsresult -nsQueryContentEventHandler::QueryRectFor(nsQueryContentEvent* aEvent, - nsIRange* aRange, - nsCaret* aCaret) +// Adjust to use a child node if possible +// to make the returned rect more accurate +static nsINode* AdjustTextRectNode(nsINode* aNode, + PRInt32& aOffset) { - PRInt32 offsetInFrame; - nsIFrame* frame; - nsresult rv = GetStartFrameAndOffset(aRange, &frame, &offsetInFrame); - NS_ENSURE_SUCCESS(rv, rv); - - nsPoint posInFrame; - rv = frame->GetPointFromOffset(aRange->StartOffset(), &posInFrame); - NS_ENSURE_SUCCESS(rv, rv); - - nsRect rect; - rect.y = posInFrame.y; - rect.height = frame->GetSize().height; - - if (aEvent->message == NS_QUERY_CHARACTER_RECT) { - nsPoint nextPos; - rv = frame->GetPointFromOffset(aRange->EndOffset(), &nextPos); - NS_ENSURE_SUCCESS(rv, rv); - rect.x = PR_MIN(posInFrame.x, nextPos.x); - rect.width = PR_ABS(posInFrame.x - nextPos.x); - } else { - rect.x = posInFrame.x; - rect.width = aCaret->GetCaretRect().width; + PRInt32 childCount = PRInt32(aNode->GetChildCount()); + nsINode* node = aNode; + if (childCount) { + if (aOffset < childCount) { + node = aNode->GetChildAt(aOffset); + aOffset = 0; + } else if (aOffset == childCount) { + node = aNode->GetChildAt(childCount - 1); + aOffset = node->IsNodeOfType(nsINode::eTEXT) ? + static_cast(node)->TextLength() : 1; + } } + return node; +} - rv = ConvertToRootViewRelativeOffset(frame, rect); - NS_ENSURE_SUCCESS(rv, rv); - - aEvent->mReply.mRect = nsRect::ToOutsidePixels(rect, mPresContext->AppUnitsPerDevPixel()); - aEvent->mSucceeded = PR_TRUE; - return NS_OK; +// Similar to nsFrameSelection::GetFrameForNodeOffset, +// but this is more flexible for OnQueryTextRect to use +static nsresult GetFrameForTextRect(nsIPresShell* aPresShell, + nsINode* aNode, + PRInt32 aOffset, + PRBool aHint, + nsIFrame** aReturnFrame) +{ + NS_ENSURE_TRUE(aNode && aNode->IsNodeOfType(nsINode::eCONTENT), + NS_ERROR_UNEXPECTED); + nsIContent* content = static_cast(aNode); + nsIFrame* frame = aPresShell->GetPrimaryFrameFor(content); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + PRInt32 childOffset = 0; + return frame->GetChildFrameContainingOffset(aOffset, aHint, &childOffset, + aReturnFrame); } nsresult -nsQueryContentEventHandler::OnQueryCharacterRect(nsQueryContentEvent* aEvent) +nsContentEventHandler::OnQueryTextRect(nsQueryContentEvent* aEvent) { nsresult rv = Init(aEvent); if (NS_FAILED(rv)) return rv; - nsCOMPtr range = new nsRange(); - NS_ENSURE_TRUE(range, NS_ERROR_OUT_OF_MEMORY); - rv = SetRangeFromFlatTextOffset(range, aEvent->mInput.mOffset, 1, PR_TRUE); + nsRefPtr range = new nsRange(); + if (!range) { + return NS_ERROR_OUT_OF_MEMORY; + } + rv = SetRangeFromFlatTextOffset(range, aEvent->mInput.mOffset, + aEvent->mInput.mLength, PR_TRUE); NS_ENSURE_SUCCESS(rv, rv); - if (range->Collapsed()) { - // There is no character at the offset. - return NS_OK; + // used to iterate over all contents and their frames + nsCOMPtr iter; + rv = NS_NewContentIterator(getter_AddRefs(iter)); + NS_ENSURE_SUCCESS(rv, rv); + iter->Init(range); + NS_ENSURE_SUCCESS(rv, rv); + + // get the starting frame + PRInt32 offset = range->StartOffset(); + nsINode* node = iter->GetCurrentNode(); + if (!node) { + node = AdjustTextRectNode(range->GetStartParent(), offset); + } + nsIFrame* firstFrame = nsnull; + rv = GetFrameForTextRect(mPresShell, node, offset, + PR_TRUE, &firstFrame); + NS_ENSURE_SUCCESS(rv, rv); + + // get the starting frame rect + nsRect rect(nsPoint(0, 0), firstFrame->GetRect().Size()); + rv = ConvertToRootViewRelativeOffset(firstFrame, rect); + NS_ENSURE_SUCCESS(rv, rv); + nsRect frameRect = rect; + nsPoint ptOffset; + firstFrame->GetPointFromOffset(offset, &ptOffset); + // minus 1 to avoid creating an empty rect + rect.x += ptOffset.x - 1; + rect.width -= ptOffset.x - 1; + + // get the ending frame + offset = range->EndOffset(); + node = AdjustTextRectNode(range->GetEndParent(), offset); + nsIFrame* lastFrame = nsnull; + rv = GetFrameForTextRect(mPresShell, node, offset, + range->Collapsed(), &lastFrame); + NS_ENSURE_SUCCESS(rv, rv); + + // iterate over all covered frames + for (nsIFrame* frame = firstFrame; frame != lastFrame;) { + frame = frame->GetNextContinuation(); + if (!frame) { + do { + iter->Next(); + node = iter->GetCurrentNode(); + if (!node || !node->IsNodeOfType(nsINode::eCONTENT)) + continue; + frame = mPresShell->GetPrimaryFrameFor(static_cast(node)); + } while (!frame && !iter->IsDone()); + if (!frame) { + // this can happen when the end offset of the range is 0. + frame = lastFrame; + } + } + frameRect.SetRect(nsPoint(0, 0), frame->GetRect().Size()); + rv = ConvertToRootViewRelativeOffset(frame, frameRect); + NS_ENSURE_SUCCESS(rv, rv); + if (frame != lastFrame) { + // not last frame, so just add rect to previous result + rect.UnionRect(rect, frameRect); + } } - return QueryRectFor(aEvent, range, nsnull); + // get the ending frame rect + lastFrame->GetPointFromOffset(offset, &ptOffset); + // minus 1 to avoid creating an empty rect + frameRect.width -= lastFrame->GetRect().width - ptOffset.x - 1; + + if (firstFrame == lastFrame) { + rect.IntersectRect(rect, frameRect); + } else { + rect.UnionRect(rect, frameRect); + } + aEvent->mReply.mRect = + nsRect::ToOutsidePixels(rect, mPresContext->AppUnitsPerDevPixel()); + aEvent->mSucceeded = PR_TRUE; + return NS_OK; } nsresult -nsQueryContentEventHandler::OnQueryCaretRect(nsQueryContentEvent* aEvent) +nsContentEventHandler::OnQueryEditorRect(nsQueryContentEvent* aEvent) +{ + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) + return rv; + + nsIFrame* frame = mPresShell->GetPrimaryFrameFor(mRootContent); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + // get rect for first frame + nsRect resultRect(nsPoint(0, 0), frame->GetRect().Size()); + rv = ConvertToRootViewRelativeOffset(frame, resultRect); + NS_ENSURE_SUCCESS(rv, rv); + + // account for any additional frames + while ((frame = frame->GetNextContinuation()) != nsnull) { + nsRect frameRect(nsPoint(0, 0), frame->GetRect().Size()); + rv = ConvertToRootViewRelativeOffset(frame, frameRect); + NS_ENSURE_SUCCESS(rv, rv); + resultRect.UnionRect(resultRect, frameRect); + } + + aEvent->mReply.mRect = + nsRect::ToOutsidePixels(resultRect, mPresContext->AppUnitsPerDevPixel()); + aEvent->mSucceeded = PR_TRUE; + return NS_OK; +} + +nsresult +nsContentEventHandler::OnQueryCaretRect(nsQueryContentEvent* aEvent) { nsresult rv = Init(aEvent); if (NS_FAILED(rv)) @@ -502,7 +638,7 @@ nsQueryContentEventHandler::OnQueryCaretRect(nsQueryContentEvent* aEvent) if (selectionIsCollapsed) { PRUint32 offset; - rv = GetFlatTextOffsetOfRange(mFirstSelectedRange, &offset); + rv = GetFlatTextOffsetOfRange(mRootContent, mFirstSelectedRange, &offset); NS_ENSURE_SUCCESS(rv, rv); if (offset == aEvent->mInput.mOffset) { PRBool isCollapsed; @@ -510,7 +646,8 @@ nsQueryContentEventHandler::OnQueryCaretRect(nsQueryContentEvent* aEvent) rv = caret->GetCaretCoordinates(nsCaret::eTopLevelWindowCoordinates, mSelection, &rect, &isCollapsed, nsnull); - aEvent->mReply.mRect = nsRect::ToOutsidePixels(rect, mPresContext->AppUnitsPerDevPixel()); + aEvent->mReply.mRect = + nsRect::ToOutsidePixels(rect, mPresContext->AppUnitsPerDevPixel()); NS_ENSURE_SUCCESS(rv, rv); aEvent->mSucceeded = PR_TRUE; return NS_OK; @@ -523,12 +660,35 @@ nsQueryContentEventHandler::OnQueryCaretRect(nsQueryContentEvent* aEvent) rv = SetRangeFromFlatTextOffset(range, aEvent->mInput.mOffset, 0, PR_TRUE); NS_ENSURE_SUCCESS(rv, rv); - return QueryRectFor(aEvent, range, caret); + PRInt32 offsetInFrame; + nsIFrame* frame; + rv = GetStartFrameAndOffset(range, &frame, &offsetInFrame); + NS_ENSURE_SUCCESS(rv, rv); + + nsPoint posInFrame; + rv = frame->GetPointFromOffset(range->StartOffset(), &posInFrame); + NS_ENSURE_SUCCESS(rv, rv); + + nsRect rect; + rect.x = posInFrame.x; + rect.y = posInFrame.y; + rect.width = caret->GetCaretRect().width; + rect.height = frame->GetSize().height; + + rv = ConvertToRootViewRelativeOffset(frame, rect); + NS_ENSURE_SUCCESS(rv, rv); + + aEvent->mReply.mRect = + nsRect::ToOutsidePixels(rect, mPresContext->AppUnitsPerDevPixel()); + aEvent->mSucceeded = PR_TRUE; + return NS_OK; } nsresult -nsQueryContentEventHandler::GetFlatTextOffsetOfRange(nsIRange* aRange, - PRUint32* aNativeOffset) +nsContentEventHandler::GetFlatTextOffsetOfRange(nsIContent* aRootContent, + nsINode* aNode, + PRInt32 aNodeOffset, + PRUint32* aNativeOffset) { NS_ASSERTION(aNativeOffset, "param is invalid"); @@ -536,16 +696,12 @@ nsQueryContentEventHandler::GetFlatTextOffsetOfRange(nsIRange* aRange, NS_ENSURE_TRUE(prev, NS_ERROR_OUT_OF_MEMORY); nsCOMPtr domPrev(do_QueryInterface(prev)); NS_ASSERTION(domPrev, "nsRange doesn't have nsIDOMRange??"); - nsCOMPtr rootDOMNode(do_QueryInterface(mRootContent)); + nsCOMPtr rootDOMNode(do_QueryInterface(aRootContent)); domPrev->SetStart(rootDOMNode, 0); - nsINode* startNode = aRange->GetStartParent(); - NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); - - PRInt32 startOffset = aRange->StartOffset(); - nsCOMPtr startDOMNode(do_QueryInterface(startNode)); + nsCOMPtr startDOMNode(do_QueryInterface(aNode)); NS_ASSERTION(startDOMNode, "startNode doesn't have nsIDOMNode"); - domPrev->SetEnd(startDOMNode, startOffset); + domPrev->SetEnd(startDOMNode, aNodeOffset); nsAutoString prevStr; nsresult rv = GenerateFlatTextContent(prev, prevStr); @@ -555,9 +711,21 @@ nsQueryContentEventHandler::GetFlatTextOffsetOfRange(nsIRange* aRange, } nsresult -nsQueryContentEventHandler::GetStartFrameAndOffset(nsIRange* aRange, - nsIFrame** aFrame, - PRInt32* aOffsetInFrame) +nsContentEventHandler::GetFlatTextOffsetOfRange(nsIContent* aRootContent, + nsIRange* aRange, + PRUint32* aNativeOffset) +{ + nsINode* startNode = aRange->GetStartParent(); + NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); + PRInt32 startOffset = aRange->StartOffset(); + return GetFlatTextOffsetOfRange(aRootContent, startNode, startOffset, + aNativeOffset); +} + +nsresult +nsContentEventHandler::GetStartFrameAndOffset(nsIRange* aRange, + nsIFrame** aFrame, + PRInt32* aOffsetInFrame) { NS_ASSERTION(aRange && aFrame && aOffsetInFrame, "params are invalid"); @@ -577,8 +745,8 @@ nsQueryContentEventHandler::GetStartFrameAndOffset(nsIRange* aRange, } nsresult -nsQueryContentEventHandler::ConvertToRootViewRelativeOffset(nsIFrame* aFrame, - nsRect& aRect) +nsContentEventHandler::ConvertToRootViewRelativeOffset(nsIFrame* aFrame, + nsRect& aRect) { NS_ASSERTION(aFrame, "aFrame must not be null"); @@ -590,3 +758,86 @@ nsQueryContentEventHandler::ConvertToRootViewRelativeOffset(nsIFrame* aFrame, aRect += posInView + view->GetOffsetTo(nsnull); return NS_OK; } + +static void AdjustRangeForSelection(nsIContent* aRoot, + nsINode** aNode, + PRInt32* aOffset) +{ + nsINode* node = *aNode; + PRInt32 offset = *aOffset; + if (aRoot != node && node->GetParent() && + !node->IsNodeOfType(nsINode::eTEXT)) { + node = node->GetParent(); + offset = node->IndexOf(*aNode) + (offset ? 1 : 0); + } + nsINode* brNode = node->GetChildAt(offset - 1); + while (brNode && brNode->IsNodeOfType(nsINode::eHTML)) { + nsIContent* brContent = static_cast(brNode); + if (brContent->Tag() != nsGkAtoms::br || IsContentBR(brContent)) + break; + brNode = node->GetChildAt(--offset - 1); + } + *aNode = node; + *aOffset = PR_MAX(offset, 0); +} + +nsresult +nsContentEventHandler::OnSelectionEvent(nsSelectionEvent* aEvent) +{ + aEvent->mSucceeded = PR_FALSE; + + // Get selection to manipulate + nsCOMPtr sel; + nsresult rv = nsIMEStateManager:: + GetFocusSelectionAndRoot(getter_AddRefs(sel), + getter_AddRefs(mRootContent)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get range from offset and length + nsRefPtr range = new nsRange(); + NS_ENSURE_TRUE(range, NS_ERROR_OUT_OF_MEMORY); + rv = SetRangeFromFlatTextOffset(range, aEvent->mOffset, + aEvent->mLength, PR_TRUE); + NS_ENSURE_SUCCESS(rv, rv); + + nsINode* startNode = range->GetStartParent(); + nsINode* endNode = range->GetEndParent(); + PRInt32 startOffset = range->StartOffset(); + PRInt32 endOffset = range->EndOffset(); + AdjustRangeForSelection(mRootContent, &startNode, &startOffset); + AdjustRangeForSelection(mRootContent, &endNode, &endOffset); + + nsCOMPtr startDomNode(do_QueryInterface(startNode)); + nsCOMPtr endDomNode(do_QueryInterface(endNode)); + NS_ENSURE_TRUE(startDomNode && endDomNode, NS_ERROR_UNEXPECTED); + + nsCOMPtr selPrivate = do_QueryInterface(sel); + NS_ENSURE_TRUE(selPrivate, NS_ERROR_UNEXPECTED); + selPrivate->StartBatchChanges(); + + // Clear selection first before setting + rv = sel->RemoveAllRanges(); + // Need to call EndBatchChanges at the end even if call failed + if (NS_SUCCEEDED(rv)) { + if (aEvent->mReversed) { + rv = sel->Collapse(endDomNode, endOffset); + } else { + rv = sel->Collapse(startDomNode, startOffset); + } + if (NS_SUCCEEDED(rv) && + (startDomNode != endDomNode || startOffset != endOffset)) { + if (aEvent->mReversed) { + rv = sel->Extend(startDomNode, startOffset); + } else { + rv = sel->Extend(endDomNode, endOffset); + } + } + } + selPrivate->EndBatchChanges(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr(do_QueryInterface(sel))->ScrollIntoView( + nsISelectionController::SELECTION_FOCUS_REGION, PR_FALSE, -1, -1); + aEvent->mSucceeded = PR_TRUE; + return NS_OK; +} diff --git a/content/events/src/nsQueryContentEventHandler.h b/content/events/src/nsContentEventHandler.h similarity index 73% rename from content/events/src/nsQueryContentEventHandler.h rename to content/events/src/nsContentEventHandler.h index 3ecb299f61ab..b3f45b5149ca 100644 --- a/content/events/src/nsQueryContentEventHandler.h +++ b/content/events/src/nsContentEventHandler.h @@ -21,6 +21,7 @@ * * Contributor(s): * Masayuki Nakano + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -36,8 +37,8 @@ * * ***** END LICENSE BLOCK ***** */ -#ifndef nsQueryContentEventHandler_h__ -#define nsQueryContentEventHandler_h__ +#ifndef nsContentEventHandler_h__ +#define nsContentEventHandler_h__ #include "nscore.h" #include "nsCOMPtr.h" @@ -50,29 +51,36 @@ class nsPresContext; class nsIPresShell; class nsQueryContentEvent; +class nsSelectionEvent; class nsCaret; struct nsRect; /* * Query Content Event Handler - * nsQueryContentEventHandler is a helper class for nsEventStateManager. + * nsContentEventHandler is a helper class for nsEventStateManager. * The platforms request some content informations, e.g., the selected text, * the offset of the selected text and the text for specified range. * This class answers to NS_QUERY_* events from actual contents. */ -class NS_STACK_CLASS nsQueryContentEventHandler { +class NS_STACK_CLASS nsContentEventHandler { public: - nsQueryContentEventHandler(nsPresContext *aPresContext); + nsContentEventHandler(nsPresContext *aPresContext); // NS_QUERY_SELECTED_TEXT event handler nsresult OnQuerySelectedText(nsQueryContentEvent* aEvent); // NS_QUERY_TEXT_CONTENT event handler nsresult OnQueryTextContent(nsQueryContentEvent* aEvent); - // NS_QUERY_CHARACTER_RECT event handler - nsresult OnQueryCharacterRect(nsQueryContentEvent* aEvent); // NS_QUERY_CARET_RECT event handler nsresult OnQueryCaretRect(nsQueryContentEvent* aEvent); + // NS_QUERY_TEXT_RECT event handler + nsresult OnQueryTextRect(nsQueryContentEvent* aEvent); + // NS_QUERY_EDITOR_RECT event handler + nsresult OnQueryEditorRect(nsQueryContentEvent* aEvent); + + // NS_SELECTION_* event + nsresult OnSelectionEvent(nsSelectionEvent* aEvent); + protected: nsPresContext* mPresContext; nsIPresShell* mPresShell; @@ -82,11 +90,19 @@ protected: nsresult Init(nsQueryContentEvent* aEvent); +public: // FlatText means the text that is generated from DOM tree. The BR elements // are replaced to native linefeeds. Other elements are ignored. - // Generate the FlatText from DOM range. - nsresult GenerateFlatTextContent(nsIRange* aRange, nsAFlatString& aString); + // Get the offset in FlatText of the range. (also used by nsIMEStateManager) + static nsresult GetFlatTextOffsetOfRange(nsIContent* aRootContent, + nsINode* aNode, + PRInt32 aNodeOffset, + PRUint32* aOffset); + static nsresult GetFlatTextOffsetOfRange(nsIContent* aRootContent, + nsIRange* aRange, + PRUint32* aOffset); +protected: // Make the DOM range from the offset of FlatText and the text length. // If aExpandToClusterBoundaries is true, the start offset and the end one are // expanded to nearest cluster boundaries. @@ -94,22 +110,18 @@ protected: PRUint32 aNativeOffset, PRUint32 aNativeLength, PRBool aExpandToClusterBoundaries); - // Get the offset in FlatText of the range. - nsresult GetFlatTextOffsetOfRange(nsIRange* aRange, PRUint32* aOffset); // Find the first textframe for the range, and get the start offset in // the frame. nsresult GetStartFrameAndOffset(nsIRange* aRange, - nsIFrame** aFrame, PRInt32* aOffsetInFrame); + nsIFrame** aFrame, + PRInt32* aOffsetInFrame); // Convert the frame relative offset to the root view relative offset. - nsresult ConvertToRootViewRelativeOffset(nsIFrame* aFrame, nsRect& aRect); - // The helper for OnQueryCharacterRect/OnQueryCaretRect. - // Don't call for another event. - nsresult QueryRectFor(nsQueryContentEvent* aEvent, nsIRange* aRange, - nsCaret* aCaret); + nsresult ConvertToRootViewRelativeOffset(nsIFrame* aFrame, + nsRect& aRect); // Expand aXPOffset to the nearest offset in cluster boundary. aForward is // true, it is expanded to forward. nsresult ExpandToClusterBoundary(nsIContent* aContent, PRBool aForward, PRUint32* aXPOffset); }; -#endif // nsQueryContentEventHandler_h__ +#endif // nsContentEventHandler_h__ diff --git a/content/events/src/nsEventStateManager.cpp b/content/events/src/nsEventStateManager.cpp index bf82ccafdc39..fa83d6bfe0c4 100644 --- a/content/events/src/nsEventStateManager.cpp +++ b/content/events/src/nsEventStateManager.cpp @@ -28,6 +28,7 @@ * Ginn Chen * Simon Bünzli * Ehsan Akhgari + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -47,7 +48,7 @@ #include "nsEventStateManager.h" #include "nsEventListenerManager.h" #include "nsIMEStateManager.h" -#include "nsQueryContentEventHandler.h" +#include "nsContentEventHandler.h" #include "nsIContent.h" #include "nsINodeInfo.h" #include "nsIDocument.h" @@ -959,6 +960,8 @@ nsEventStateManager::PreHandleEvent(nsPresContext* aPresContext, break; if (mDocument) { + nsIMEStateManager::OnTextStateBlur(mPresContext, mCurrentFocus); + if (gLastFocusedDocument && gLastFocusedPresContextWeak) { nsCOMPtr ourWindow = gLastFocusedDocument->GetWindow(); @@ -1076,6 +1079,8 @@ nsEventStateManager::PreHandleEvent(nsPresContext* aPresContext, NS_IF_RELEASE(gLastFocusedContent); gLastFocusedContent = mCurrentFocus; NS_IF_ADDREF(gLastFocusedContent); + + nsIMEStateManager::OnTextStateFocus(mPresContext, mCurrentFocus); } // Try to keep the focus controllers and the globals in synch @@ -1147,6 +1152,8 @@ nsEventStateManager::PreHandleEvent(nsPresContext* aPresContext, NS_RELEASE(gLastFocusedContent); } + nsIMEStateManager::OnTextStateBlur(nsnull, nsnull); + // Now fire blurs. We fire a blur on the focused document, element, // and window. @@ -1320,6 +1327,8 @@ nsEventStateManager::PreHandleEvent(nsPresContext* aPresContext, if (focusController) focusController->SetSuppressFocus(PR_TRUE, "Deactivate Suppression"); + nsIMEStateManager::OnTextStateBlur(nsnull, nsnull); + // Now fire blurs. Blur the content, then the document, then the window. if (gLastFocusedDocument && gLastFocusedDocument == mDocument && @@ -1494,28 +1503,40 @@ nsEventStateManager::PreHandleEvent(nsPresContext* aPresContext, break; case NS_QUERY_SELECTED_TEXT: { - nsQueryContentEventHandler handler(mPresContext); + nsContentEventHandler handler(mPresContext); handler.OnQuerySelectedText((nsQueryContentEvent*)aEvent); } break; case NS_QUERY_TEXT_CONTENT: { - nsQueryContentEventHandler handler(mPresContext); + nsContentEventHandler handler(mPresContext); handler.OnQueryTextContent((nsQueryContentEvent*)aEvent); } break; - case NS_QUERY_CHARACTER_RECT: - { - nsQueryContentEventHandler handler(mPresContext); - handler.OnQueryCharacterRect((nsQueryContentEvent*)aEvent); - } - break; case NS_QUERY_CARET_RECT: { - nsQueryContentEventHandler handler(mPresContext); + nsContentEventHandler handler(mPresContext); handler.OnQueryCaretRect((nsQueryContentEvent*)aEvent); } break; + case NS_QUERY_TEXT_RECT: + { + nsContentEventHandler handler(mPresContext); + handler.OnQueryTextRect((nsQueryContentEvent*)aEvent); + } + break; + case NS_QUERY_EDITOR_RECT: + { + nsContentEventHandler handler(mPresContext); + handler.OnQueryEditorRect((nsQueryContentEvent*)aEvent); + } + break; + case NS_SELECTION_SET: + { + nsContentEventHandler handler(mPresContext); + handler.OnSelectionEvent((nsSelectionEvent*)aEvent); + } + break; } return NS_OK; } @@ -5031,6 +5052,8 @@ nsEventStateManager::SendFocusBlur(nsPresContext* aPresContext, // Track the old focus controller if any focus suppressions is used on it. nsFocusSuppressor oldFocusSuppressor; + nsIMEStateManager::OnTextStateBlur(aPresContext, aContent); + if (nsnull != gLastFocusedPresContextWeak) { nsCOMPtr focusAfterBlur; @@ -5261,6 +5284,8 @@ nsEventStateManager::SendFocusBlur(nsPresContext* aPresContext, if (clearFirstFocusEvent) { mFirstFocusEvent = nsnull; } + + nsIMEStateManager::OnTextStateFocus(mPresContext, mCurrentFocus); } else if (!aContent) { //fire focus on document even if the content isn't focusable (ie. text) //see bugzilla bug 93521 diff --git a/content/events/src/nsIMEStateManager.cpp b/content/events/src/nsIMEStateManager.cpp index fa08fdd203ee..8261130383c2 100644 --- a/content/events/src/nsIMEStateManager.cpp +++ b/content/events/src/nsIMEStateManager.cpp @@ -22,6 +22,7 @@ * * Contributor(s): * Masayuki Nakano + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -53,6 +54,16 @@ #include "nsIFocusController.h" #include "nsIDOMWindow.h" #include "nsContentUtils.h" +#include "nsINode.h" +#include "nsIFrame.h" +#include "nsRange.h" +#include "nsIDOMRange.h" +#include "nsISelection.h" +#include "nsISelectionPrivate.h" +#include "nsISelectionListener.h" +#include "nsISelectionController.h" +#include "nsIMutationObserver.h" +#include "nsContentEventHandler.h" /******************************************************************/ /* nsIMEStateManager */ @@ -63,6 +74,8 @@ nsPresContext* nsIMEStateManager::sPresContext = nsnull; nsPIDOMWindow* nsIMEStateManager::sActiveWindow = nsnull; PRBool nsIMEStateManager::sInstalledMenuKeyboardListener = PR_FALSE; +nsTextStateManager* nsIMEStateManager::sTextStateObserver = nsnull; + nsresult nsIMEStateManager::OnDestroyPresContext(nsPresContext* aPresContext) { @@ -71,6 +84,7 @@ nsIMEStateManager::OnDestroyPresContext(nsPresContext* aPresContext) return NS_OK; sContent = nsnull; sPresContext = nsnull; + OnTextStateBlur(nsnull, nsnull); return NS_OK; } @@ -269,3 +283,301 @@ nsIMEStateManager::GetWidget(nsPresContext* aPresContext) return widget; } + +// nsTextStateManager notifies widget of any text and selection changes +// in the currently focused editor +// sTextStateObserver points to the currently active nsTextStateManager +// sTextStateObserver is null if there is no focused editor + +class nsTextStateManager : public nsISelectionListener, + public nsStubMutationObserver +{ +public: + nsTextStateManager(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISELECTIONLISTENER + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + nsresult Init(nsIWidget* aWidget, + nsPresContext* aPresContext, + nsINode* aNode); + void Destroy(void); + + nsCOMPtr mWidget; + nsCOMPtr mSel; + nsCOMPtr mRootContent; + nsCOMPtr mEditableNode; + PRBool mDestroying; + +private: + void NotifyContentAdded(nsINode* aContainer, PRInt32 aStart, PRInt32 aEnd); +}; + +nsTextStateManager::nsTextStateManager() +{ + mDestroying = PR_FALSE; +} + +nsresult +nsTextStateManager::Init(nsIWidget* aWidget, + nsPresContext* aPresContext, + nsINode* aNode) +{ + mWidget = aWidget; + + nsIPresShell* presShell = aPresContext->PresShell(); + + // get selection and root content + nsCOMPtr selCon; + if (aNode->IsNodeOfType(nsINode::eCONTENT)) { + nsIFrame* frame = presShell->GetPrimaryFrameFor( + static_cast(aNode)); + NS_ENSURE_TRUE(frame, NS_ERROR_UNEXPECTED); + + frame->GetSelectionController(aPresContext, + getter_AddRefs(selCon)); + } else { + // aNode is a document + selCon = do_QueryInterface(presShell); + } + NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE); + + nsCOMPtr sel; + nsresult rv = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(sel)); + NS_ENSURE_TRUE(sel, NS_ERROR_UNEXPECTED); + + nsCOMPtr selDomRange; + rv = sel->GetRangeAt(0, getter_AddRefs(selDomRange)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr selRange(do_QueryInterface(selDomRange)); + NS_ENSURE_TRUE(selRange && selRange->GetStartParent(), NS_ERROR_UNEXPECTED); + + mRootContent = selRange->GetStartParent()-> + GetSelectionRootContent(presShell); + + // add text change observer + mRootContent->AddMutationObserver(this); + + // add selection change listener + nsCOMPtr selPrivate(do_QueryInterface(sel)); + NS_ENSURE_TRUE(selPrivate, NS_ERROR_UNEXPECTED); + rv = selPrivate->AddSelectionListener(this); + NS_ENSURE_SUCCESS(rv, rv); + mSel = sel; + + mEditableNode = aNode; + return NS_OK; +} + +void +nsTextStateManager::Destroy(void) +{ + if (mSel) { + nsCOMPtr selPrivate(do_QueryInterface(mSel)); + if (selPrivate) + selPrivate->RemoveSelectionListener(this); + mSel = nsnull; + } + if (mRootContent) { + mRootContent->RemoveMutationObserver(this); + mRootContent = nsnull; + } + mEditableNode = nsnull; + mWidget = nsnull; +} + +NS_IMPL_ISUPPORTS2(nsTextStateManager, + nsIMutationObserver, + nsISelectionListener) + +nsresult +nsTextStateManager::NotifySelectionChanged(nsIDOMDocument* aDoc, + nsISelection* aSel, + PRInt16 aReason) +{ + PRInt32 count = 0; + nsresult rv = aSel->GetRangeCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + if (count > 0) { + mWidget->OnIMESelectionChange(); + } + return NS_OK; +} + +void +nsTextStateManager::CharacterDataChanged(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ + NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT), + "character data changed for non-text node"); + + PRUint32 offset = 0; + // get offsets of change and fire notification + if (NS_FAILED(nsContentEventHandler::GetFlatTextOffsetOfRange( + mRootContent, aContent, aInfo->mChangeStart, &offset))) + return; + + PRUint32 oldEnd = offset + aInfo->mChangeEnd - aInfo->mChangeStart; + PRUint32 newEnd = offset + aInfo->mReplaceLength; + mWidget->OnIMETextChange(offset, oldEnd, newEnd); +} + +void +nsTextStateManager::NotifyContentAdded(nsINode* aContainer, + PRInt32 aStartIndex, + PRInt32 aEndIndex) +{ + PRUint32 offset = 0, newOffset = 0; + if (NS_FAILED(nsContentEventHandler::GetFlatTextOffsetOfRange( + mRootContent, aContainer, aStartIndex, &offset))) + return; + + // get offset at the end of the last added node + if (NS_FAILED(nsContentEventHandler::GetFlatTextOffsetOfRange( + aContainer->GetChildAt(aStartIndex), + aContainer, aEndIndex, &newOffset))) + return; + + // fire notification + if (newOffset) + mWidget->OnIMETextChange(offset, offset, offset + newOffset); +} + +void +nsTextStateManager::ContentAppended(nsIDocument* aDocument, + nsIContent* aContainer, + PRInt32 aNewIndexInContainer) +{ + NotifyContentAdded(aContainer, aNewIndexInContainer, + aContainer->GetChildCount()); +} + +void +nsTextStateManager::ContentInserted(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + PRInt32 aIndexInContainer) +{ + NotifyContentAdded(NODE_FROM(aContainer, aDocument), + aIndexInContainer, aIndexInContainer + 1); +} + +void +nsTextStateManager::ContentRemoved(nsIDocument* aDocument, + nsIContent* aContainer, + nsIContent* aChild, + PRInt32 aIndexInContainer) +{ + PRUint32 offset = 0, childOffset = 1; + if (NS_FAILED(nsContentEventHandler::GetFlatTextOffsetOfRange( + mRootContent, NODE_FROM(aContainer, aDocument), + aIndexInContainer, &offset))) + return; + + // get offset at the end of the deleted node + if (aChild->IsNodeOfType(nsINode::eTEXT)) + childOffset = aChild->TextLength(); + else if (0 < aChild->GetChildCount()) + childOffset = aChild->GetChildCount(); + + if (NS_FAILED(nsContentEventHandler::GetFlatTextOffsetOfRange( + aChild, aChild, childOffset, &childOffset))) + return; + + // fire notification + if (childOffset) + mWidget->OnIMETextChange(offset, offset + childOffset, offset); +} + +static nsINode* GetRootEditableNode(nsPresContext* aPresContext, + nsIContent* aContent) +{ + if (aContent) { + nsINode* root = nsnull; + nsINode* node = aContent; + while (node && node->IsEditable()) { + root = node; + node = node->GetParent(); + } + return root; + } + if (aPresContext) { + nsIDocument* document = aPresContext->Document(); + if (document && document->IsEditable()) + return document; + } + return nsnull; +} + +nsresult +nsIMEStateManager::OnTextStateBlur(nsPresContext* aPresContext, + nsIContent* aContent) +{ + if (!sTextStateObserver || sTextStateObserver->mDestroying || + sTextStateObserver->mEditableNode == + GetRootEditableNode(aPresContext, aContent)) + return NS_OK; + + sTextStateObserver->mDestroying = PR_TRUE; + sTextStateObserver->mWidget->OnIMEFocusChange(PR_FALSE); + sTextStateObserver->Destroy(); + NS_RELEASE(sTextStateObserver); + return NS_OK; +} + +nsresult +nsIMEStateManager::OnTextStateFocus(nsPresContext* aPresContext, + nsIContent* aContent) +{ + if (sTextStateObserver) return NS_OK; + + nsINode *editableNode = GetRootEditableNode(aPresContext, aContent); + if (!editableNode) return NS_OK; + + nsIViewManager* vm = aPresContext->GetViewManager(); + NS_ENSURE_TRUE(vm, NS_ERROR_NOT_AVAILABLE); + + nsCOMPtr widget; + nsresult rv = vm->GetWidget(getter_AddRefs(widget)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_AVAILABLE); + + rv = widget->OnIMEFocusChange(PR_TRUE); + NS_ENSURE_SUCCESS(rv, NS_OK); + + // OnIMEFocusChange may cause focus and sTextStateObserver to change + // In that case return and keep the current sTextStateObserver + NS_ENSURE_TRUE(!sTextStateObserver, NS_OK); + + sTextStateObserver = new nsTextStateManager(); + NS_ENSURE_TRUE(sTextStateObserver, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(sTextStateObserver); + rv = sTextStateObserver->Init(widget, aPresContext, editableNode); + if (NS_FAILED(rv)) { + sTextStateObserver->mDestroying = PR_TRUE; + sTextStateObserver->Destroy(); + NS_RELEASE(sTextStateObserver); + widget->OnIMEFocusChange(PR_FALSE); + return rv; + } + return NS_OK; +} + +nsresult +nsIMEStateManager::GetFocusSelectionAndRoot(nsISelection** aSel, + nsIContent** aRoot) +{ + if (!sTextStateObserver || !sTextStateObserver->mEditableNode) + return NS_ERROR_NOT_AVAILABLE; + + NS_ASSERTION(sTextStateObserver->mSel && sTextStateObserver->mRootContent, + "uninitialized text state observer"); + NS_ADDREF(*aSel = sTextStateObserver->mSel); + NS_ADDREF(*aRoot = sTextStateObserver->mRootContent); + return NS_OK; +} diff --git a/content/events/src/nsIMEStateManager.h b/content/events/src/nsIMEStateManager.h index e9d648f0f99e..9fec1c2ff2a7 100644 --- a/content/events/src/nsIMEStateManager.h +++ b/content/events/src/nsIMEStateManager.h @@ -40,12 +40,15 @@ #define nsIMEStateManager_h__ #include "nscore.h" +#include "nsGUIEvent.h" class nsIContent; class nsPIDOMWindow; class nsPresContext; class nsIWidget; class nsIFocusController; +class nsTextStateManager; +class nsISelection; /* * IME state manager @@ -62,6 +65,25 @@ public: static nsresult OnActivate(nsPresContext* aPresContext); static nsresult OnDeactivate(nsPresContext* aPresContext); static void OnInstalledMenuKeyboardListener(PRBool aInstalling); + + // These two methods manage focus and selection/text observers. + // They are separate from OnChangeFocus above because this offers finer + // control compared to having the two methods incorporated into OnChangeFocus + + // OnTextStateBlur should be called *before* NS_BLUR_CONTENT fires + // aPresContext is the nsPresContext receiving focus (not lost focus) + // aContent is the nsIContent receiving focus (not lost focus) + // aPresContext and/or aContent may be null + static nsresult OnTextStateBlur(nsPresContext* aPresContext, + nsIContent* aContent); + // OnTextStateFocus should be called *after* NS_FOCUS_CONTENT fires + // aPresContext is the nsPresContext receiving focus + // aContent is the nsIContent receiving focus + static nsresult OnTextStateFocus(nsPresContext* aPresContext, + nsIContent* aContent); + // Get the focused editor's selection and root + static nsresult GetFocusSelectionAndRoot(nsISelection** aSel, + nsIContent** aRoot); protected: static void SetIMEState(nsPresContext* aPresContext, PRUint32 aState, @@ -78,6 +100,8 @@ protected: static nsPresContext* sPresContext; static nsPIDOMWindow* sActiveWindow; static PRBool sInstalledMenuKeyboardListener; + + static nsTextStateManager* sTextStateObserver; }; #endif // nsIMEStateManager_h__ diff --git a/content/html/content/src/nsGenericHTMLElement.h b/content/html/content/src/nsGenericHTMLElement.h index 52d11c9af0ea..9b7296fcbe46 100644 --- a/content/html/content/src/nsGenericHTMLElement.h +++ b/content/html/content/src/nsGenericHTMLElement.h @@ -569,6 +569,13 @@ public: static nsresult GetHashFromHrefString(const nsAString &aHref, nsAString& aHash); + + /** + * Locate an nsIEditor rooted at this content node, if there is one. + */ + NS_HIDDEN_(nsresult) GetEditor(nsIEditor** aEditor); + NS_HIDDEN_(nsresult) GetEditorInternal(nsIEditor** aEditor); + protected: /** * Focus or blur the element. This is what you should call if you want to @@ -728,12 +735,6 @@ protected: */ NS_HIDDEN_(nsresult) GetURIListAttr(nsIAtom* aAttr, nsAString& aResult); - /** - * Locate an nsIEditor rooted at this content node, if there is one. - */ - NS_HIDDEN_(nsresult) GetEditor(nsIEditor** aEditor); - NS_HIDDEN_(nsresult) GetEditorInternal(nsIEditor** aEditor); - /** * Locates the nsIEditor associated with this node. In general this is * equivalent to GetEditorInternal(), but for designmode or contenteditable, diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index f29766e56937..eaef3c790745 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -1543,6 +1543,9 @@ pref("intl.jis0208.map", "CP932"); // Switch the keyboard layout per window pref("intl.keyboard.per_window_layout", false); +// Enable/Disable TSF support +pref("intl.enable_tsf_support", false); + // See bug 448927, on topmost panel, some IMEs are not usable on Windows. pref("ui.panel.default_level_parent", false); diff --git a/view/src/nsViewManager.cpp b/view/src/nsViewManager.cpp index 45fb56c0b0cc..efb0db41121f 100644 --- a/view/src/nsViewManager.cpp +++ b/view/src/nsViewManager.cpp @@ -1249,6 +1249,7 @@ NS_IMETHODIMP nsViewManager::DispatchEvent(nsGUIEvent *aEvent, nsEventStatus *aS if (!NS_IS_KEY_EVENT(aEvent) && !NS_IS_IME_EVENT(aEvent) && !NS_IS_CONTEXT_MENU_KEY(aEvent) && !NS_IS_FOCUS_EVENT(aEvent) && !NS_IS_QUERY_CONTENT_EVENT(aEvent) && !NS_IS_PLUGIN_EVENT(aEvent) && + !NS_IS_SELECTION_EVENT(aEvent) && aEvent->eventStructType != NS_ACCESSIBLE_EVENT) { // will dispatch using coordinates. Pretty bogus but it's consistent // with what presshell does. diff --git a/widget/Makefile.in b/widget/Makefile.in index f495accb88ef..ce8fa5c349cc 100644 --- a/widget/Makefile.in +++ b/widget/Makefile.in @@ -45,7 +45,7 @@ include $(DEPTH)/config/autoconf.mk DIRS = public src ifdef ENABLE_TESTS -DIRS += tests +TOOL_DIRS += tests endif include $(topsrcdir)/config/rules.mk diff --git a/widget/public/nsGUIEvent.h b/widget/public/nsGUIEvent.h index 8a1f5fcd0063..244a19bf63f1 100644 --- a/widget/public/nsGUIEvent.h +++ b/widget/public/nsGUIEvent.h @@ -105,6 +105,7 @@ class nsHashKey; #define NS_DRAG_EVENT 35 #define NS_NOTIFYPAINT_EVENT 36 #define NS_SIMPLE_GESTURE_EVENT 37 +#define NS_SELECTION_EVENT 38 // These flags are sort of a mess. They're sort of shared between event // listener flags and event flags, but only some of them. You've been @@ -344,14 +345,18 @@ class nsHashKey; #define NS_QUERY_SELECTED_TEXT (NS_QUERY_CONTENT_EVENT_START) // Query for the text content of specified range, it returns actual lengh (if // the specified range is too long) and the text of the specified range. +// Returns the entire text if requested length > actual length. #define NS_QUERY_TEXT_CONTENT (NS_QUERY_CONTENT_EVENT_START + 1) -// Query for the character rect of nth character. If there is no character at -// the offset, the query will be failed. The offset of the result is relative -// position from the top level widget. -#define NS_QUERY_CHARACTER_RECT (NS_QUERY_CONTENT_EVENT_START + 2) // Query for the caret rect of nth insertion point. The offset of the result is // relative position from the top level widget. #define NS_QUERY_CARET_RECT (NS_QUERY_CONTENT_EVENT_START + 3) +// Query for the bounding rect of a range of characters. This works on any +// valid character range given offset and length. Result is relative to top +// level widget coordinates +#define NS_QUERY_TEXT_RECT (NS_QUERY_CONTENT_EVENT_START + 4) +// Query for the bounding rect of the current focused frame. Result is relative +// to top level widget coordinates +#define NS_QUERY_EDITOR_RECT (NS_QUERY_CONTENT_EVENT_START + 5) // Video events #ifdef MOZ_MEDIA @@ -397,6 +402,11 @@ class nsHashKey; #define NS_PLUGIN_EVENT_START 3600 #define NS_PLUGIN_EVENT (NS_PLUGIN_EVENT_START) +// Events to manipulate selection (nsSelectionEvent) +#define NS_SELECTION_EVENT_START 3700 +// Clear any previous selection and set the given range as the selection +#define NS_SELECTION_SET (NS_SELECTION_EVENT_START) + /** * Return status for event processors, nsEventStatus, is defined in * nsEvent.h. @@ -858,11 +868,11 @@ class nsTextEvent : public nsInputEvent public: nsTextEvent(PRBool isTrusted, PRUint32 msg, nsIWidget *w) : nsInputEvent(isTrusted, msg, w, NS_TEXT_EVENT), - theText(nsnull), rangeCount(0), rangeArray(nsnull), isChar(PR_FALSE) + rangeCount(0), rangeArray(nsnull), isChar(PR_FALSE) { } - const PRUnichar* theText; + nsString theText; nsTextEventReply theReply; PRUint32 rangeCount; // Note that the range array may not specify a caret position; in that @@ -965,13 +975,6 @@ public: mInput.mLength = aLength; } - void InitForQueryCharacterRect(PRUint32 aOffset) - { - NS_ASSERTION(message == NS_QUERY_CHARACTER_RECT, - "wrong initializer is called"); - mInput.mOffset = aOffset; - } - void InitForQueryCaretRect(PRUint32 aOffset) { NS_ASSERTION(message == NS_QUERY_CARET_RECT, @@ -979,6 +982,14 @@ public: mInput.mOffset = aOffset; } + void InitForQueryTextRect(PRUint32 aOffset, PRUint32 aLength) + { + NS_ASSERTION(message == NS_QUERY_TEXT_RECT, + "wrong initializer is called"); + mInput.mOffset = aOffset; + mInput.mLength = aLength; + } + PRBool mSucceeded; struct { PRUint32 mOffset; @@ -991,9 +1002,25 @@ public: nsIntRect mRect; // Finally, the coordinates is system coordinates. // The return widget has the caret. This is set at all query events. nsIWidget* mFocusedWidget; + PRPackedBool mReversed; // true if selection is reversed (end < start) } mReply; }; +class nsSelectionEvent : public nsGUIEvent +{ +public: + nsSelectionEvent(PRBool aIsTrusted, PRUint32 aMsg, nsIWidget *aWidget) : + nsGUIEvent(aIsTrusted, aMsg, aWidget, NS_SELECTION_EVENT), + mSucceeded(PR_FALSE) + { + } + + PRUint32 mOffset; // start offset of selection + PRUint32 mLength; // length of selection + PRPackedBool mReversed; // selection "anchor" should be in front + PRPackedBool mSucceeded; +}; + /** * MenuItem event * @@ -1232,8 +1259,12 @@ enum nsDragDropEventStatus { #define NS_IS_QUERY_CONTENT_EVENT(evnt) \ (((evnt)->message == NS_QUERY_SELECTED_TEXT) || \ ((evnt)->message == NS_QUERY_TEXT_CONTENT) || \ - ((evnt)->message == NS_QUERY_CHARACTER_RECT) || \ - ((evnt)->message == NS_QUERY_CARET_RECT)) + ((evnt)->message == NS_QUERY_CARET_RECT) || \ + ((evnt)->message == NS_QUERY_TEXT_RECT) || \ + ((evnt)->message == NS_QUERY_EDITOR_RECT)) + +#define NS_IS_SELECTION_EVENT(evnt) \ + (((evnt)->message == NS_SELECTION_SET)) #define NS_IS_PLUGIN_EVENT(evnt) \ (((evnt)->message == NS_PLUGIN_EVENT)) diff --git a/widget/public/nsIWidget.h b/widget/public/nsIWidget.h index b7ec83fc3252..e335a95d3bc7 100644 --- a/widget/public/nsIWidget.h +++ b/widget/public/nsIWidget.h @@ -93,11 +93,14 @@ typedef nsEventStatus (* EVENT_CALLBACK)(nsGUIEvent *event); #define NS_NATIVE_PLUGIN_PORT_QD 100 #define NS_NATIVE_PLUGIN_PORT_CG 101 #endif +#ifdef XP_WIN +#define NS_NATIVE_TSF_POINTER 100 +#endif -// a85944af-7fce-4e45-bf04-ac12c823394b +// 075a7792-6ba9-454e-b431-25a43fdbd3f6 #define NS_IWIDGET_IID \ -{ 0xa85944af, 0x7fce, 0x4e45, \ - { 0xbf, 0x04, 0xac, 0x12, 0xc8, 0x23, 0x39, 0x4b } } +{ 0x075a7792, 0x6ba9, 0x454e, \ + { 0xb4, 0x31, 0x25, 0xa4, 0x3f, 0xdb, 0xd3, 0xf6 } } // Hide the native window systems real window type so as to avoid // including native window system types and APIs. This is necessary @@ -1242,6 +1245,29 @@ class nsIWidget : public nsISupports { */ NS_IMETHOD GetToggledKeyState(PRUint32 aKeyCode, PRBool* aLEDState) = 0; + /* + * An editable node (i.e. input/textarea/design mode document) + * is receiving or giving up focus + * aFocus is true if node is receiving focus + * aFocus is false if node is giving up focus (blur) + */ + NS_IMETHOD OnIMEFocusChange(PRBool aFocus) = 0; + + /* + * Text content of the focused node has changed + * aStart is the starting offset of the change + * aOldEnd is the ending offset of the change + * aNewEnd is the caret offset after the change + */ + NS_IMETHOD OnIMETextChange(PRUint32 aStart, + PRUint32 aOldEnd, + PRUint32 aNewEnd) = 0; + + /* + * Selection has changed in the focused node + */ + NS_IMETHOD OnIMESelectionChange(void) = 0; + protected: // keep the list of children. We also keep track of our siblings. // The ownership model is as follows: parent holds a strong ref to diff --git a/widget/src/cocoa/nsChildView.mm b/widget/src/cocoa/nsChildView.mm index c6d0f6e2959c..6cbfcd644962 100644 --- a/widget/src/cocoa/nsChildView.mm +++ b/widget/src/cocoa/nsChildView.mm @@ -5490,8 +5490,8 @@ GetUSLayoutCharFromKeyTranslate(UInt32 aKeyCode, UInt32 aModifiers) nsIntRect r; PRBool useCaretRect = theRange.length == 0; if (!useCaretRect) { - nsQueryContentEvent charRect(PR_TRUE, NS_QUERY_CHARACTER_RECT, mGeckoChild); - charRect.InitForQueryCharacterRect(theRange.location); + nsQueryContentEvent charRect(PR_TRUE, NS_QUERY_TEXT_RECT, mGeckoChild); + charRect.InitForQueryTextRect(theRange.location, 1); mGeckoChild->DispatchWindowEvent(charRect); if (charRect.mSucceeded) r = charRect.mReply.mRect; diff --git a/widget/src/windows/Makefile.in b/widget/src/windows/Makefile.in index 64887ab765fe..a608f43496e8 100644 --- a/widget/src/windows/Makefile.in +++ b/widget/src/windows/Makefile.in @@ -107,6 +107,7 @@ CPPSRCS += \ nsBidiKeyboard.cpp \ nsSound.cpp \ nsIdleServiceWin.cpp \ + nsTextStore.cpp \ $(NULL) endif diff --git a/widget/src/windows/nsTextStore.cpp b/widget/src/windows/nsTextStore.cpp new file mode 100644 index 000000000000..daeaa347c063 --- /dev/null +++ b/widget/src/windows/nsTextStore.cpp @@ -0,0 +1,1274 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Ningjie Chen + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include + +#include "nscore.h" +#include "nsTextStore.h" +#include "nsWindow.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "prlog.h" + +/******************************************************************/ +/* nsTextStore */ +/******************************************************************/ + +ITfThreadMgr* nsTextStore::sTsfThreadMgr = NULL; +DWORD nsTextStore::sTsfClientId = 0; +nsTextStore* nsTextStore::sTsfTextStore = NULL; + +UINT nsTextStore::sFlushTIPInputMessage = 0; + +#ifdef PR_LOGGING +PRLogModuleInfo* sTextStoreLog = nsnull; +#endif + +nsTextStore::nsTextStore() +{ + mRefCnt = 1; + mEditCookie = 0; + mSinkMask = 0; + mWindow = nsnull; + mLock = 0; + mLockQueued = 0; + mTextChange.acpStart = PR_INT32_MAX; + mTextChange.acpOldEnd = mTextChange.acpNewEnd = 0; +} + +nsTextStore::~nsTextStore() +{ +} + +PRBool +nsTextStore::Create(nsWindow* aWindow, + PRUint32 aIMEState) +{ + if (!mDocumentMgr) { + // Create document manager + HRESULT hr = sTsfThreadMgr->CreateDocumentMgr( + getter_AddRefs(mDocumentMgr)); + NS_ENSURE_TRUE(SUCCEEDED(hr), PR_FALSE); + mWindow = aWindow; + // Create context and add it to document manager + hr = mDocumentMgr->CreateContext(sTsfClientId, 0, + static_cast(this), + getter_AddRefs(mContext), &mEditCookie); + if (SUCCEEDED(hr)) { + SetIMEEnabledInternal(aIMEState); + hr = mDocumentMgr->Push(mContext); + } + if (SUCCEEDED(hr)) { + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Created, window=%08x\n", aWindow)); + return PR_TRUE; + } + mContext = NULL; + mDocumentMgr = NULL; + } + return PR_FALSE; +} + +PRBool +nsTextStore::Destroy(void) +{ + Blur(); + if (mWindow) { + // When blurred, Tablet Input Panel posts "blur" messages + // and try to insert text when the message is retrieved later. + // But by that time the text store is already destroyed, + // so try to get the message early + MSG msg; + if (::PeekMessageW(&msg, mWindow->GetWindowHandle(), + sFlushTIPInputMessage, sFlushTIPInputMessage, + PM_REMOVE)) { + ::DispatchMessageW(&msg); + } + } + mContext = NULL; + if (mDocumentMgr) { + mDocumentMgr->Pop(TF_POPF_ALL); + mDocumentMgr = NULL; + } + mSink = NULL; + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Destroyed, window=%08x\n", mWindow)); + mWindow = NULL; + return PR_TRUE; +} + +PRBool +nsTextStore::Focus(void) +{ + HRESULT hr = sTsfThreadMgr->SetFocus(mDocumentMgr); + NS_ENSURE_TRUE(SUCCEEDED(hr), PR_FALSE); + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Focused\n")); + return PR_TRUE; +} + +PRBool +nsTextStore::Blur(void) +{ + sTsfThreadMgr->SetFocus(NULL); + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Blurred\n")); + return PR_TRUE; +} + +STDMETHODIMP +nsTextStore::QueryInterface(REFIID riid, + void** ppv) +{ + *ppv=NULL; + if ( (IID_IUnknown == riid) || (IID_ITextStoreACP == riid) ) { + *ppv = static_cast(this); + } else if (IID_ITfContextOwnerCompositionSink == riid) { + *ppv = static_cast(this); + } + if (*ppv) { + AddRef(); + return S_OK; + } + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) nsTextStore::AddRef() +{ + return ++mRefCnt; +} + +STDMETHODIMP_(ULONG) nsTextStore::Release() +{ + --mRefCnt; + if (0 != mRefCnt) + return mRefCnt; + delete this; + return 0; +} + +STDMETHODIMP +nsTextStore::AdviseSink(REFIID riid, + IUnknown *punk, + DWORD dwMask) +{ + NS_ENSURE_TRUE(punk && IID_ITextStoreACPSink == riid, E_INVALIDARG); + if (!mSink) { + // Install sink + punk->QueryInterface(IID_ITextStoreACPSink, getter_AddRefs(mSink)); + NS_ENSURE_TRUE(mSink, E_UNEXPECTED); + } else { + // If sink is already installed we check to see if they are the same + // Get IUnknown from both sides for comparison + nsRefPtr comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + if (comparison1 != comparison2) + return CONNECT_E_ADVISELIMIT; + } + // Update mask either for a new sink or an existing sink + mSinkMask = dwMask; + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Sink installed, punk=%08x\n", punk)); + return S_OK; +} + +STDMETHODIMP +nsTextStore::UnadviseSink(IUnknown *punk) +{ + NS_ENSURE_TRUE(punk, E_INVALIDARG); + NS_ENSURE_TRUE(mSink, CONNECT_E_NOCONNECTION); + // Get IUnknown from both sides for comparison + nsRefPtr comparison1, comparison2; + punk->QueryInterface(IID_IUnknown, getter_AddRefs(comparison1)); + mSink->QueryInterface(IID_IUnknown, getter_AddRefs(comparison2)); + // Unadvise only if sinks are the same + NS_ENSURE_TRUE(comparison1 == comparison2, CONNECT_E_NOCONNECTION); + mSink = NULL; + mSinkMask = 0; + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: Sink removed, punk=%08x\n", punk)); + return S_OK; +} + +STDMETHODIMP +nsTextStore::RequestLock(DWORD dwLockFlags, + HRESULT *phrSession) +{ + NS_ENSURE_TRUE(mSink, E_FAIL); + NS_ENSURE_TRUE(phrSession, E_INVALIDARG); + if (mLock) { + // only time when reentrant lock is allowed is when caller holds a + // read-only lock and is requesting an async write lock + if (TS_LF_READ == (mLock & TS_LF_READWRITE) && + TS_LF_READWRITE == (dwLockFlags & TS_LF_READWRITE) && + !(dwLockFlags & TS_LF_SYNC)) { + *phrSession = TS_S_ASYNC; + mLockQueued = dwLockFlags & (~TS_LF_SYNC); + } else { + // no more locks allowed + *phrSession = TS_E_SYNCHRONOUS; + return E_FAIL; + } + } else { + // put on lock + mLock = dwLockFlags & (~TS_LF_SYNC); + *phrSession = mSink->OnLockGranted(mLock); + while (mLockQueued) { + mLock = mLockQueued; + mLockQueued = 0; + mSink->OnLockGranted(mLock); + } + mLock = 0; + } + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetStatus(TS_STATUS *pdcs) +{ + NS_ENSURE_TRUE(pdcs, E_INVALIDARG); + pdcs->dwDynamicFlags = 0; + // we use a "flat" text model for TSF support so no hidden text + pdcs->dwStaticFlags = TS_SS_NOHIDDENTEXT; + return S_OK; +} + +STDMETHODIMP +nsTextStore::QueryInsert(LONG acpTestStart, + LONG acpTestEnd, + ULONG cch, + LONG *pacpResultStart, + LONG *pacpResultEnd) +{ + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: QueryInsert, start=%ld end=%ld cch=%lu\n", + acpTestStart, acpTestEnd, cch)); + // We don't test to see if these positions are + // after the end of document for performance reasons + NS_ENSURE_TRUE(0 <= acpTestStart && acpTestStart <= acpTestEnd && + pacpResultStart && pacpResultEnd, E_INVALIDARG); + + // XXX need to adjust to cluster boundary + // Assume we are given good offsets for now + *pacpResultStart = acpTestStart; + *pacpResultEnd = acpTestStart + cch; + + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: QueryInsert SUCCEEDED\n")); + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetSelection(ULONG ulIndex, + ULONG ulCount, + TS_SELECTION_ACP *pSelection, + ULONG *pcFetched) +{ + NS_ENSURE_TRUE(TS_LF_READ == (mLock & TS_LF_READ), TS_E_NOLOCK); + NS_ENSURE_TRUE(ulCount && pSelection && pcFetched, E_INVALIDARG); + + *pcFetched = 0; + NS_ENSURE_TRUE(TS_DEFAULT_SELECTION == ulIndex || 0 == ulIndex, + TS_E_NOSELECTION); + if (mCompositionView) { + // Emulate selection during compositions + *pSelection = mCompositionSelection; + } else { + // Construct and initialize an event to get selection info + nsQueryContentEvent event(PR_TRUE, NS_QUERY_SELECTED_TEXT, mWindow); + mWindow->InitEvent(event); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, E_FAIL); + // Usually the selection anchor (beginning) position corresponds to the + // TSF start and the selection focus (ending) position corresponds to + // the TSF end, but if selection is reversed the focus now corresponds + // to the TSF start and the anchor now corresponds to the TSF end + pSelection->acpStart = event.mReply.mOffset; + pSelection->acpEnd = pSelection->acpStart + event.mReply.mString.Length(); + pSelection->style.ase = event.mReply.mString.Length() && + event.mReply.mReversed ? TS_AE_START : TS_AE_END; + // No support for interim character + pSelection->style.fInterimChar = 0; + } + *pcFetched = 1; + return S_OK; +} + +HRESULT +nsTextStore::SetSelectionInternal(const TS_SELECTION_ACP* pSelection) +{ + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: SetSelection, sel=%ld-%ld\n", + pSelection->acpStart, pSelection->acpEnd)); + if (mCompositionView) { + // Emulate selection during compositions + NS_ENSURE_TRUE(pSelection->acpStart >= mCompositionStart && + pSelection->acpEnd <= mCompositionStart + + LONG(mCompositionString.Length()), TS_E_INVALIDPOS); + mCompositionSelection = *pSelection; + } else { + nsSelectionEvent event(PR_TRUE, NS_SELECTION_SET, mWindow); + event.mOffset = pSelection->acpStart; + event.mLength = PRUint32(pSelection->acpEnd - pSelection->acpStart); + event.mReversed = pSelection->style.ase == TS_AE_START; + mWindow->InitEvent(event); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, E_FAIL); + } + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: SetSelection SUCCEEDED\n")); + return S_OK; +} + +STDMETHODIMP +nsTextStore::SetSelection(ULONG ulCount, + const TS_SELECTION_ACP *pSelection) +{ + NS_ENSURE_TRUE(TS_LF_READWRITE == (mLock & TS_LF_READWRITE), TS_E_NOLOCK); + NS_ENSURE_TRUE(1 == ulCount && pSelection, E_INVALIDARG); + + return SetSelectionInternal(pSelection); +} + +STDMETHODIMP +nsTextStore::GetText(LONG acpStart, + LONG acpEnd, + WCHAR *pchPlain, + ULONG cchPlainReq, + ULONG *pcchPlainOut, + TS_RUNINFO *prgRunInfo, + ULONG ulRunInfoReq, + ULONG *pulRunInfoOut, + LONG *pacpNext) +{ + NS_ENSURE_TRUE(TS_LF_READ == (mLock & TS_LF_READ), TS_E_NOLOCK); + NS_ENSURE_TRUE(pcchPlainOut && (pchPlain || prgRunInfo) && + (!cchPlainReq == !pchPlain) && + (!ulRunInfoReq == !prgRunInfo), E_INVALIDARG); + NS_ENSURE_TRUE(0 <= acpStart && -1 <= acpEnd && + (-1 == acpEnd || acpStart <= acpEnd), TS_E_INVALIDPOS); + + // Making sure to NULL-terminate string just to be on the safe side + *pcchPlainOut = 0; + if (pchPlain && cchPlainReq) *pchPlain = NULL; + if (pulRunInfoOut) *pulRunInfoOut = 0; + if (pacpNext) *pacpNext = acpStart; + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = 0; + prgRunInfo->type = TS_RT_PLAIN; + } + PRUint32 length = -1 == acpEnd ? PR_UINT32_MAX : PRUint32(acpEnd - acpStart); + if (cchPlainReq && cchPlainReq - 1 < length) { + length = cchPlainReq - 1; + } + if (length) { + LONG compNewStart = 0, compOldEnd = 0, compNewEnd = 0; + if (mCompositionView) { + // Sometimes GetText gets called between InsertTextAtSelection and + // OnUpdateComposition. In this case the returned text would + // be out of sync because we haven't sent NS_TEXT_TEXT in + // OnUpdateComposition yet. Manually resync here. + compOldEnd = PR_MIN(LONG(length) + acpStart, + mCompositionLength + mCompositionStart); + compNewEnd = PR_MIN(LONG(length) + acpStart, + LONG(mCompositionString.Length()) + mCompositionStart); + compNewStart = PR_MAX(acpStart, mCompositionStart); + // Check if the range is affected + if (compOldEnd > compNewStart || compNewEnd > compNewStart) { + NS_ASSERTION(compOldEnd >= mCompositionStart && + compNewEnd >= mCompositionStart, "Range end is less than start\n"); + length = PRUint32(LONG(length) + compOldEnd - compNewEnd); + } + } + // Send NS_QUERY_TEXT_CONTENT to get text content + nsQueryContentEvent event(PR_TRUE, NS_QUERY_TEXT_CONTENT, mWindow); + mWindow->InitEvent(event); + event.InitForQueryTextContent(PRUint32(acpStart), length); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, E_FAIL); + + if (compOldEnd > compNewStart || compNewEnd > compNewStart) { + // Resync composition string + const PRUnichar* compStrStart = mCompositionString.BeginReading() + + PR_MAX(compNewStart - mCompositionStart, 0); + event.mReply.mString.Replace(compNewStart - acpStart, + compOldEnd - mCompositionStart, compStrStart, + compNewEnd - mCompositionStart); + length = PRUint32(LONG(length) - compOldEnd + compNewEnd); + } + NS_ENSURE_TRUE(-1 == acpEnd || event.mReply.mString.Length() == length, + TS_E_INVALIDPOS); + length = PR_MIN(length, event.mReply.mString.Length()); + + if (pchPlain && cchPlainReq) { + memcpy(pchPlain, event.mReply.mString.BeginReading(), + length * sizeof(*pchPlain)); + pchPlain[length] = NULL; + *pcchPlainOut = length; + } + if (prgRunInfo && ulRunInfoReq) { + prgRunInfo->uCount = length; + prgRunInfo->type = TS_RT_PLAIN; + if (pulRunInfoOut) *pulRunInfoOut = 1; + } + if (pacpNext) *pacpNext = acpStart + length; + } + return S_OK; +} + +STDMETHODIMP +nsTextStore::SetText(DWORD dwFlags, + LONG acpStart, + LONG acpEnd, + const WCHAR *pchText, + ULONG cch, + TS_TEXTCHANGE *pChange) +{ + // Per SDK documentation, and since we don't have better + // ways to do this, this method acts as a helper to + // call SetSelection followed by InsertTextAtSelection + NS_ENSURE_TRUE(TS_LF_READWRITE == (mLock & TS_LF_READWRITE), TS_E_NOLOCK); + TS_SELECTION_ACP selection; + selection.acpStart = acpStart; + selection.acpEnd = acpEnd; + selection.style.ase = TS_AE_END; + selection.style.fInterimChar = 0; + // Set selection to desired range + NS_ENSURE_TRUE(SUCCEEDED(SetSelectionInternal(&selection)), E_FAIL); + // Replace just selected text + return InsertTextAtSelection(TS_IAS_NOQUERY, pchText, cch, + NULL, NULL, pChange); +} + +STDMETHODIMP +nsTextStore::GetFormattedText(LONG acpStart, + LONG acpEnd, + IDataObject **ppDataObject) +{ + // no support for formatted text + return E_NOTIMPL; +} + +STDMETHODIMP +nsTextStore::GetEmbedded(LONG acpPos, + REFGUID rguidService, + REFIID riid, + IUnknown **ppunk) +{ + // embedded objects are not supported + return E_NOTIMPL; +} + +STDMETHODIMP +nsTextStore::QueryInsertEmbedded(const GUID *pguidService, + const FORMATETC *pFormatEtc, + BOOL *pfInsertable) +{ + // embedded objects are not supported + *pfInsertable = FALSE; + return S_OK; +} + +STDMETHODIMP +nsTextStore::InsertEmbedded(DWORD dwFlags, + LONG acpStart, + LONG acpEnd, + IDataObject *pDataObject, + TS_TEXTCHANGE *pChange) +{ + // embedded objects are not supported + return E_NOTIMPL; +} + +STDMETHODIMP +nsTextStore::RequestSupportedAttrs(DWORD dwFlags, + ULONG cFilterAttrs, + const TS_ATTRID *paFilterAttrs) +{ + // no attributes defined + return S_OK; +} + +STDMETHODIMP +nsTextStore::RequestAttrsAtPosition(LONG acpPos, + ULONG cFilterAttrs, + const TS_ATTRID *paFilterAttrs, + DWORD dwFlags) +{ + // no per character attributes defined + return S_OK; +} + +STDMETHODIMP +nsTextStore::RequestAttrsTransitioningAtPosition(LONG acpPos, + ULONG cFilterAttrs, + const TS_ATTRID *paFilterAttr, + DWORD dwFlags) +{ + // no per character attributes defined + return S_OK; +} + +STDMETHODIMP +nsTextStore::FindNextAttrTransition(LONG acpStart, + LONG acpHalt, + ULONG cFilterAttrs, + const TS_ATTRID *paFilterAttrs, + DWORD dwFlags, + LONG *pacpNext, + BOOL *pfFound, + LONG *plFoundOffset) +{ + NS_ENSURE_TRUE(pacpNext && pfFound && plFoundOffset, E_INVALIDARG); + // no per character attributes defined + *pacpNext = *plFoundOffset = acpHalt; + *pfFound = FALSE; + return S_OK; +} + +STDMETHODIMP +nsTextStore::RetrieveRequestedAttrs(ULONG ulCount, + TS_ATTRVAL *paAttrVals, + ULONG *pcFetched) +{ + NS_ENSURE_TRUE(pcFetched && ulCount && paAttrVals, E_INVALIDARG); + // no attributes defined + *pcFetched = 0; + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetEndACP(LONG *pacp) +{ + NS_ENSURE_TRUE(TS_LF_READ == (mLock & TS_LF_READ), TS_E_NOLOCK); + NS_ENSURE_TRUE(pacp, E_INVALIDARG); + // Flattened text is retrieved and its length returned + nsQueryContentEvent event(PR_TRUE, NS_QUERY_TEXT_CONTENT, mWindow); + mWindow->InitEvent(event); + // Return entire text + event.InitForQueryTextContent(0, PR_INT32_MAX); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, E_FAIL); + *pacp = LONG(event.mReply.mString.Length()); + return S_OK; +} + +#define TEXTSTORE_DEFAULT_VIEW (1) + +STDMETHODIMP +nsTextStore::GetActiveView(TsViewCookie *pvcView) +{ + NS_ENSURE_TRUE(pvcView, E_INVALIDARG); + *pvcView = TEXTSTORE_DEFAULT_VIEW; + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetACPFromPoint(TsViewCookie vcView, + const POINT *pt, + DWORD dwFlags, + LONG *pacp) +{ + NS_ENSURE_TRUE(TS_LF_READ == (mLock & TS_LF_READ), TS_E_NOLOCK); + NS_ENSURE_TRUE(TEXTSTORE_DEFAULT_VIEW == vcView, E_INVALIDARG); + // not supported for now + return E_NOTIMPL; +} + +STDMETHODIMP +nsTextStore::GetTextExt(TsViewCookie vcView, + LONG acpStart, + LONG acpEnd, + RECT *prc, + BOOL *pfClipped) +{ + NS_ENSURE_TRUE(TS_LF_READ == (mLock & TS_LF_READ), TS_E_NOLOCK); + NS_ENSURE_TRUE(TEXTSTORE_DEFAULT_VIEW == vcView && prc && pfClipped, + E_INVALIDARG); + NS_ENSURE_TRUE(acpStart >= 0 && acpEnd >= acpStart, TS_E_INVALIDPOS); + + // use NS_QUERY_TEXT_RECT to get rect in system, screen coordinates + nsQueryContentEvent event(PR_TRUE, NS_QUERY_TEXT_RECT, mWindow); + mWindow->InitEvent(event); + event.InitForQueryTextRect(acpStart, acpEnd - acpStart); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, TS_E_INVALIDPOS); + // IMEs don't like empty rects, fix here + if (event.mReply.mRect.width <= 0) + event.mReply.mRect.width = 1; + if (event.mReply.mRect.height <= 0) + event.mReply.mRect.height = 1; + + // convert to unclipped screen rect + nsWindow* refWindow = static_cast( + event.mReply.mFocusedWidget ? event.mReply.mFocusedWidget : mWindow); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(PR_FALSE); + NS_ENSURE_TRUE(refWindow, E_FAIL); + + nsresult rv = refWindow->WidgetToScreen(event.mReply.mRect, + event.mReply.mRect); + NS_ENSURE_SUCCESS(rv, E_FAIL); + + // get bounding screen rect to test for clipping + HRESULT hr = GetScreenExt(vcView, prc); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + // clip text rect to bounding rect + RECT textRect; + ::SetRect(&textRect, event.mReply.mRect.x, event.mReply.mRect.y, + event.mReply.mRect.XMost(), event.mReply.mRect.YMost()); + if (!::IntersectRect(prc, prc, &textRect)) + // Text is not visible + ::SetRectEmpty(prc); + + // not equal if text rect was clipped + *pfClipped = !::EqualRect(prc, &textRect); + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetScreenExt(TsViewCookie vcView, + RECT *prc) +{ + NS_ENSURE_TRUE(TEXTSTORE_DEFAULT_VIEW == vcView && prc, E_INVALIDARG); + // use NS_QUERY_EDITOR_RECT to get rect in system, screen coordinates + nsQueryContentEvent event(PR_TRUE, NS_QUERY_EDITOR_RECT, mWindow); + mWindow->InitEvent(event); + mWindow->DispatchWindowEvent(&event); + NS_ENSURE_TRUE(event.mSucceeded, E_FAIL); + + nsWindow* refWindow = static_cast( + event.mReply.mFocusedWidget ? event.mReply.mFocusedWidget : mWindow); + // Result rect is in top level widget coordinates + refWindow = refWindow->GetTopLevelWindow(PR_FALSE); + NS_ENSURE_TRUE(refWindow, E_FAIL); + + nsIntRect boundRect; + nsresult rv = refWindow->GetClientBounds(boundRect); + NS_ENSURE_SUCCESS(rv, E_FAIL); + + // Clip frame rect to window rect + boundRect.IntersectRect(event.mReply.mRect, boundRect); + rv = refWindow->WidgetToScreen(boundRect, boundRect); + NS_ENSURE_SUCCESS(rv, E_FAIL); + + if (!boundRect.IsEmpty()) { + ::SetRect(prc, boundRect.x, boundRect.y, + boundRect.XMost(), boundRect.YMost()); + } else { + ::SetRectEmpty(prc); + } + return S_OK; +} + +STDMETHODIMP +nsTextStore::GetWnd(TsViewCookie vcView, + HWND *phwnd) +{ + NS_ENSURE_TRUE(TEXTSTORE_DEFAULT_VIEW == vcView && phwnd, E_INVALIDARG); + *phwnd = mWindow->GetWindowHandle(); + return S_OK; +} + +STDMETHODIMP +nsTextStore::InsertTextAtSelection(DWORD dwFlags, + const WCHAR *pchText, + ULONG cch, + LONG *pacpStart, + LONG *pacpEnd, + TS_TEXTCHANGE *pChange) +{ + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: InsertTextAtSelection, cch=%lu\n", cch)); + NS_ENSURE_TRUE(TS_LF_READWRITE == (mLock & TS_LF_READWRITE), TS_E_NOLOCK); + NS_ENSURE_TRUE(!cch || pchText, E_INVALIDARG); + + // Get selection first + TS_SELECTION_ACP sel; + ULONG selFetched; + NS_ENSURE_TRUE(SUCCEEDED(GetSelection( + TS_DEFAULT_SELECTION, 1, &sel, &selFetched)) && selFetched, E_FAIL); + if (TS_IAS_QUERYONLY == dwFlags) { + NS_ENSURE_TRUE(pacpStart && pacpEnd, E_INVALIDARG); + // Simulate text insertion + *pacpStart = sel.acpStart; + *pacpEnd = sel.acpEnd; + if (pChange) { + pChange->acpStart = sel.acpStart; + pChange->acpOldEnd = sel.acpEnd; + pChange->acpNewEnd = sel.acpStart + cch; + } + } else { + NS_ENSURE_TRUE(pChange, E_INVALIDARG); + NS_ENSURE_TRUE(TS_IAS_NOQUERY == dwFlags || (pacpStart && pacpEnd), + E_INVALIDARG); + if (mCompositionView) { + // Emulate text insertion during compositions, because during a + // composition, editor expects the whole composition string to + // be sent in NS_TEXT_TEXT, not just the inserted part. + // The actual NS_TEXT_TEXT is sent in OnUpdateComposition, which + // should get called by TSF after this returns + mCompositionString.Replace(PRUint32(sel.acpStart - mCompositionStart), + sel.acpEnd - sel.acpStart, pchText, cch); + + mCompositionSelection.acpStart += cch; + mCompositionSelection.acpEnd = mCompositionSelection.acpStart; + mCompositionSelection.style.ase = TS_AE_END; + // OnUpdateComposition is not called here because it will + // result in fun visual artifacts + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: InsertTextAtSelection, replaced=%lu-%lu\n", + sel.acpStart - mCompositionStart, + sel.acpEnd - mCompositionStart)); + } else { + // Use a temporary composition to contain the text + nsCompositionEvent compEvent(PR_TRUE, NS_COMPOSITION_START, mWindow); + mWindow->InitEvent(compEvent); + mWindow->DispatchWindowEvent(&compEvent); + nsTextEvent event(PR_TRUE, NS_TEXT_TEXT, mWindow); + mWindow->InitEvent(event); + if (!cch) { + // XXX See OnEndComposition comment on inserting empty strings + event.theText = NS_LITERAL_STRING(" "); + mWindow->DispatchWindowEvent(&event); + } + event.theText.Assign(pchText, cch); + event.theText.ReplaceSubstring(NS_LITERAL_STRING("\r\n"), + NS_LITERAL_STRING("\n")); + mWindow->DispatchWindowEvent(&event); + compEvent.message = NS_COMPOSITION_END; + mWindow->DispatchWindowEvent(&compEvent); + } + pChange->acpStart = sel.acpStart; + pChange->acpOldEnd = sel.acpEnd; + // Get new selection + NS_ENSURE_TRUE(SUCCEEDED(GetSelection( + TS_DEFAULT_SELECTION, 1, &sel, &selFetched)) && selFetched, E_FAIL); + pChange->acpNewEnd = sel.acpEnd; + if (TS_IAS_NOQUERY != dwFlags) { + *pacpStart = pChange->acpStart; + *pacpEnd = pChange->acpNewEnd; + } + } + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: InsertTextAtSelection SUCCEEDED\n")); + return S_OK; +} + +STDMETHODIMP +nsTextStore::InsertEmbeddedAtSelection(DWORD dwFlags, + IDataObject *pDataObject, + LONG *pacpStart, + LONG *pacpEnd, + TS_TEXTCHANGE *pChange) +{ + // embedded objects are not supported + return E_NOTIMPL; +} + +static HRESULT +GetRangeExtent(ITfRange* aRange, LONG* aStart, LONG* aLength) +{ + nsRefPtr rangeACP; + aRange->QueryInterface(IID_ITfRangeACP, getter_AddRefs(rangeACP)); + NS_ENSURE_TRUE(rangeACP, E_FAIL); + return rangeACP->GetExtent(aStart, aLength); +} + +HRESULT +nsTextStore::OnStartCompositionInternal(ITfCompositionView* pComposition, + ITfRange* aRange, + PRBool aPreserveSelection) +{ + mCompositionView = pComposition; + HRESULT hr = GetRangeExtent(aRange, &mCompositionStart, &mCompositionLength); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: OnStartComposition, range=%ld-%ld\n", mCompositionStart, + mCompositionStart + mCompositionLength)); + + // Select composition range so the new composition replaces the range + nsSelectionEvent selEvent(PR_TRUE, NS_SELECTION_SET, mWindow); + mWindow->InitEvent(selEvent); + selEvent.mOffset = PRUint32(mCompositionStart); + selEvent.mLength = PRUint32(mCompositionLength); + selEvent.mReversed = PR_FALSE; + mWindow->DispatchWindowEvent(&selEvent); + NS_ENSURE_TRUE(selEvent.mSucceeded, E_FAIL); + + // Set up composition + nsQueryContentEvent queryEvent(PR_TRUE, NS_QUERY_SELECTED_TEXT, mWindow); + mWindow->InitEvent(queryEvent); + mWindow->DispatchWindowEvent(&queryEvent); + NS_ENSURE_TRUE(queryEvent.mSucceeded, E_FAIL); + mCompositionString = queryEvent.mReply.mString; + if (!aPreserveSelection) { + mCompositionSelection.acpStart = mCompositionStart; + mCompositionSelection.acpEnd = mCompositionStart + mCompositionLength; + mCompositionSelection.style.ase = TS_AE_END; + mCompositionSelection.style.fInterimChar = FALSE; + } + nsCompositionEvent event(PR_TRUE, NS_COMPOSITION_START, mWindow); + mWindow->InitEvent(event); + mWindow->DispatchWindowEvent(&event); + return S_OK; +} + +STDMETHODIMP +nsTextStore::OnStartComposition(ITfCompositionView* pComposition, + BOOL* pfOk) +{ + *pfOk = FALSE; + + // Only one composition at a time + if (mCompositionView) + return S_OK; + + nsRefPtr range; + HRESULT hr = pComposition->GetRange(getter_AddRefs(range)); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + hr = OnStartCompositionInternal(pComposition, range, PR_FALSE); + if (SUCCEEDED(hr)) + *pfOk = TRUE; + return hr; +} + +STDMETHODIMP +nsTextStore::OnUpdateComposition(ITfCompositionView* pComposition, + ITfRange* pRangeNew) +{ + NS_ENSURE_TRUE(mCompositionView && + mCompositionView == pComposition && + mDocumentMgr && mContext, E_UNEXPECTED); + + // Getting display attributes is *really* complicated! + // We first get the context and the property objects to query for + // attributes, but since a big range can have a variety of values for + // the attribute, we have to find out all the ranges that have distinct + // attribute values. Then we query for what the value represents through + // the display attribute manager and translate that to nsTextRange to be + // sent in NS_TEXT_TEXT + if (!pRangeNew) // pRangeNew is null when the update is not complete + return S_OK; + + // Get starting offset of the composition + LONG compStart = 0, compLength = 0; + HRESULT hr = GetRangeExtent(pRangeNew, &compStart, &compLength); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + if (mCompositionStart != compStart || + mCompositionString.Length() != compLength) { + // If the queried composition length is different from the length + // of our composition string, OnUpdateComposition is being called + // because a part of the original composition was committed. + // Reflect that by committing existing composition and starting + // a new one. OnEndComposition followed by OnStartComposition + // will accomplish this automagically. + OnEndComposition(pComposition); + OnStartCompositionInternal(pComposition, pRangeNew, PR_TRUE); + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: OnUpdateComposition, (reset) range=%ld-%ld\n", + compStart, compStart + compLength)); + } else { + mCompositionLength = compLength; + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: OnUpdateComposition, range=%ld-%ld\n", + compStart, compStart + compLength)); + } + + nsRefPtr prop; + hr = mContext->GetProperty(GUID_PROP_ATTRIBUTE, getter_AddRefs(prop)); + NS_ENSURE_TRUE(SUCCEEDED(hr) && prop, hr); + hr = LoadManagers(); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + // Use NS_TEXT_TEXT to set composition string + nsTextEvent event(PR_TRUE, NS_TEXT_TEXT, mWindow); + mWindow->InitEvent(event); + + VARIANT propValue; + ::VariantInit(&propValue); + nsRefPtr range; + nsRefPtr enumRanges; + hr = prop->EnumRanges(TfEditCookie(mEditCookie), + getter_AddRefs(enumRanges), pRangeNew); + NS_ENSURE_TRUE(SUCCEEDED(hr) && enumRanges, hr); + + nsAutoTArray textRanges; + nsTextRange newRange; + newRange.mStartOffset = PRUint32(mCompositionSelection.acpStart - compStart); + newRange.mEndOffset = PRUint32(mCompositionSelection.acpEnd - compStart); + newRange.mRangeType = NS_TEXTRANGE_CARETPOSITION; + textRanges.AppendElement(newRange); + // No matter if we have display attribute info or not, + // we always pass in at least one range to NS_TEXT_TEXT + newRange.mStartOffset = 0; + newRange.mEndOffset = mCompositionString.Length(); + newRange.mRangeType = NS_TEXTRANGE_RAWINPUT; + textRanges.AppendElement(newRange); + + while (S_OK == enumRanges->Next(1, getter_AddRefs(range), NULL) && range) { + + LONG start = 0, length = 0; + if (FAILED(GetRangeExtent(range, &start, &length))) continue; + + newRange.mStartOffset = PRUint32(start - compStart); + // The end of the last range in the array is + // always kept at the end of composition + newRange.mEndOffset = mCompositionString.Length(); + + // Who came up with this convoluted way that we have to follow? + ::VariantClear(&propValue); + hr = prop->GetValue(TfEditCookie(mEditCookie), range, &propValue); + if (FAILED(hr) || VT_I4 != propValue.vt) continue; + + GUID guid; + hr = mCatMgr->GetGUID(DWORD(propValue.lVal), &guid); + if (FAILED(hr)) continue; + + nsRefPtr info; + hr = mDAMgr->GetDisplayAttributeInfo( + guid, getter_AddRefs(info), NULL); + if (FAILED(hr) || !info) continue; + + TF_DISPLAYATTRIBUTE attr; + hr = info->GetAttributeInfo(&attr); + if (FAILED(hr)) continue; + + switch (attr.bAttr) { + case TF_ATTR_TARGET_CONVERTED: + newRange.mRangeType = NS_TEXTRANGE_SELECTEDCONVERTEDTEXT; + break; + case TF_ATTR_CONVERTED: + newRange.mRangeType = NS_TEXTRANGE_CONVERTEDTEXT; + break; + case TF_ATTR_TARGET_NOTCONVERTED: + newRange.mRangeType = NS_TEXTRANGE_SELECTEDRAWTEXT; + break; + default: + newRange.mRangeType = NS_TEXTRANGE_RAWINPUT; + break; + } + + nsTextRange& lastRange = textRanges[textRanges.Length() - 1]; + if (lastRange.mStartOffset == newRange.mStartOffset) { + // Replace range if last range is the same as this one + // So that ranges don't overlap and confuse the editor + lastRange = newRange; + } else { + lastRange.mEndOffset = newRange.mStartOffset; + textRanges.AppendElement(newRange); + } + } + + event.theText = mCompositionString; + event.rangeArray = textRanges.Elements(); + event.rangeCount = textRanges.Length(); + mWindow->DispatchWindowEvent(&event); + ::VariantClear(&propValue); + return S_OK; +} + +STDMETHODIMP +nsTextStore::OnEndComposition(ITfCompositionView* pComposition) +{ + NS_ENSURE_TRUE(mCompositionView && + mCompositionView == pComposition, E_UNEXPECTED); + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: OnEndComposition\n")); + + // Use NS_TEXT_TEXT to commit composition string + nsTextEvent textEvent(PR_TRUE, NS_TEXT_TEXT, mWindow); + mWindow->InitEvent(textEvent); + if (!mCompositionString.Length()) { + // XXX HACK! HACK! NS_TEXT_TEXT handler specifically rejects + // first-time empty strings as workaround for another IME bug + // and our request will be rejected if this is the first time + // we are sending NS_TEXT_TEXT. The workaround is to send it a + // non-empty dummy string first. + textEvent.theText = NS_LITERAL_STRING(" "); + mWindow->DispatchWindowEvent(&textEvent); + } + textEvent.theText = mCompositionString; + textEvent.theText.ReplaceSubstring(NS_LITERAL_STRING("\r\n"), + NS_LITERAL_STRING("\n")); + mWindow->DispatchWindowEvent(&textEvent); + + nsCompositionEvent event(PR_TRUE, NS_COMPOSITION_END, mWindow); + mWindow->InitEvent(event); + mWindow->DispatchWindowEvent(&event); + + mCompositionView = NULL; + mCompositionString.Truncate(0); + // Maintain selection + SetSelectionInternal(&mCompositionSelection); + return S_OK; +} + +nsresult +nsTextStore::OnFocusChange(PRBool aFocus, + nsWindow* aWindow, + PRUint32 aIMEEnabled) +{ + // no change notifications if TSF is disabled + if (!sTsfThreadMgr || !sTsfTextStore) + return NS_ERROR_NOT_AVAILABLE; + + if (aFocus) { + if (sTsfTextStore->Create(aWindow, aIMEEnabled)) + sTsfTextStore->Focus(); + } else { + sTsfTextStore->Destroy(); + } + return NS_OK; +} + +nsresult +nsTextStore::OnTextChangeInternal(PRUint32 aStart, + PRUint32 aOldEnd, + PRUint32 aNewEnd) +{ + if (!mLock && mSink && 0 != (mSinkMask & TS_AS_TEXT_CHANGE)) { + mTextChange.acpStart = PR_MIN(mTextChange.acpStart, LONG(aStart)); + mTextChange.acpOldEnd = PR_MAX(mTextChange.acpOldEnd, LONG(aOldEnd)); + mTextChange.acpNewEnd = PR_MAX(mTextChange.acpNewEnd, LONG(aNewEnd)); + ::PostMessageW(mWindow->GetWindowHandle(), + WM_USER_TSF_TEXTCHANGE, 0, NULL); + } + return NS_OK; +} + +void +nsTextStore::OnTextChangeMsgInternal(void) +{ + if (!mLock && mSink && 0 != (mSinkMask & TS_AS_TEXT_CHANGE) && + PR_INT32_MAX > mTextChange.acpStart) { + mSink->OnTextChange(0, &mTextChange); + mTextChange.acpStart = PR_INT32_MAX; + mTextChange.acpOldEnd = mTextChange.acpNewEnd = 0; + } +} + +nsresult +nsTextStore::OnSelectionChangeInternal(void) +{ + if (!mLock && mSink && 0 != (mSinkMask & TS_AS_SEL_CHANGE)) { + mSink->OnSelectionChange(); + } + return NS_OK; +} + +void +nsTextStore::CommitCompositionInternal(PRBool aDiscard) +{ + if (mCompositionView && aDiscard) { + mCompositionString.Truncate(0); + if (mSink && !mLock) { + TS_TEXTCHANGE textChange; + textChange.acpStart = mCompositionStart; + textChange.acpOldEnd = mCompositionStart + mCompositionLength; + textChange.acpNewEnd = mCompositionStart; + mSink->OnTextChange(0, &textChange); + } + } + // Terminate two contexts, the base context (mContext) and the top + // if the top context is not the same as the base context + nsRefPtr context = mContext; + do { + if (context) { + nsRefPtr services; + context->QueryInterface(IID_ITfContextOwnerCompositionServices, + getter_AddRefs(services)); + if (services) + services->TerminateComposition(NULL); + } + if (context != mContext) + break; + if (mDocumentMgr) + mDocumentMgr->GetTop(getter_AddRefs(context)); + } while (context != mContext); +} + +static +PRBool +GetCompartment(IUnknown* pUnk, + const GUID& aID, + ITfCompartment** aCompartment) +{ + if (!pUnk) return PR_FALSE; + + nsRefPtr compMgr; + pUnk->QueryInterface(IID_ITfCompartmentMgr, getter_AddRefs(compMgr)); + if (!compMgr) return PR_FALSE; + + return SUCCEEDED(compMgr->GetCompartment(aID, aCompartment)) && + (*aCompartment) != NULL; +} + +void +nsTextStore::SetIMEOpenState(PRBool aState) +{ + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: SetIMEOpenState, state=%lu\n", aState)); + + nsRefPtr comp; + if (!GetCompartment(sTsfThreadMgr, + GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(comp))) + return; + + VARIANT variant; + variant.vt = VT_I4; + variant.lVal = aState; + comp->SetValue(sTsfClientId, &variant); +} + +PRBool +nsTextStore::GetIMEOpenState(void) +{ + nsRefPtr comp; + if (!GetCompartment(sTsfThreadMgr, + GUID_COMPARTMENT_KEYBOARD_OPENCLOSE, + getter_AddRefs(comp))) + return PR_FALSE; + + VARIANT variant; + ::VariantInit(&variant); + if (SUCCEEDED(comp->GetValue(&variant)) && variant.vt == VT_I4) + return variant.lVal != 0; + + ::VariantClear(&variant); // clear up in case variant.vt != VT_I4 + return PR_FALSE; +} + +void +nsTextStore::SetIMEEnabledInternal(PRUint32 aState) +{ + PR_LOG(sTextStoreLog, PR_LOG_ALWAYS, + ("TSF: SetIMEEnabled, state=%lu\n", aState)); + + VARIANT variant; + variant.vt = VT_I4; + variant.lVal = aState != nsIWidget::IME_STATUS_ENABLED; + + // Set two contexts, the base context (mContext) and the top + // if the top context is not the same as the base context + nsRefPtr context = mContext; + nsRefPtr comp; + do { + if (GetCompartment(context, GUID_COMPARTMENT_KEYBOARD_DISABLED, + getter_AddRefs(comp))) + comp->SetValue(sTsfClientId, &variant); + + if (context != mContext) + break; + if (mDocumentMgr) + mDocumentMgr->GetTop(getter_AddRefs(context)); + } while (context != mContext); +} + +HRESULT +nsTextStore::LoadManagers(void) +{ + HRESULT hr; + if (!mDAMgr) { + hr = ::CoCreateInstance(CLSID_TF_DisplayAttributeMgr, NULL, + CLSCTX_INPROC_SERVER, IID_ITfDisplayAttributeMgr, + getter_AddRefs(mDAMgr)); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + } + if (!mCatMgr) { + hr = ::CoCreateInstance(CLSID_TF_CategoryMgr, NULL, CLSCTX_INPROC_SERVER, + IID_ITfCategoryMgr, getter_AddRefs(mCatMgr)); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + } + return S_OK; +} + +void +nsTextStore::Initialize(void) +{ +#ifdef PR_LOGGING + if (!sTextStoreLog) + sTextStoreLog = PR_NewLogModule("nsTextStoreWidgets"); +#endif + if (!sTsfThreadMgr) { + PRBool enableTsf = PR_TRUE; + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + nsCOMPtr prefBranch; + prefs->GetBranch(nsnull, getter_AddRefs(prefBranch)); + if (prefBranch && NS_FAILED(prefBranch->GetBoolPref( + "intl.enable_tsf_support", &enableTsf))) + enableTsf = PR_TRUE; + } + if (enableTsf) { + if (SUCCEEDED(CoCreateInstance(CLSID_TF_ThreadMgr, NULL, + CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, + reinterpret_cast(&sTsfThreadMgr)))) { + if (FAILED(sTsfThreadMgr->Activate(&sTsfClientId))) { + NS_RELEASE(sTsfThreadMgr); + NS_WARNING("failed to activate TSF\n"); + } + } else + // TSF not installed? + NS_WARNING("failed to create TSF manager\n"); + } + } + if (sTsfThreadMgr && !sTsfTextStore) { + sTsfTextStore = new nsTextStore(); + if (!sTsfTextStore) + NS_ERROR("failed to create text store\n"); + } + if (sTsfThreadMgr && !sFlushTIPInputMessage) { + sFlushTIPInputMessage = ::RegisterWindowMessageW( + NS_LITERAL_STRING("Flush TIP Input Message").get()); + } +} + +void +nsTextStore::Terminate(void) +{ + NS_IF_RELEASE(sTsfTextStore); + if (sTsfThreadMgr) { + sTsfThreadMgr->Deactivate(); + NS_RELEASE(sTsfThreadMgr); + } +} diff --git a/widget/src/windows/nsTextStore.h b/widget/src/windows/nsTextStore.h new file mode 100644 index 000000000000..06d886a9f06c --- /dev/null +++ b/widget/src/windows/nsTextStore.h @@ -0,0 +1,233 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Ningjie Chen + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef NSTEXTSTORE_H_ +#define NSTEXTSTORE_H_ + +#include "nsAutoPtr.h" +#include "nsString.h" + +#include +#include + +struct ITfThreadMgr; +struct ITfDocumentMgr; +class nsWindow; + +// It doesn't work well when we notify TSF of text change +// during a mutation observer call because things get broken. +// So we post a message and notify TSF when we get it later. +#define WM_USER_TSF_TEXTCHANGE (WM_USER + 0x100) + +/* + * Text Services Framework text store + */ + +class nsTextStore : public ITextStoreACP, + public ITfContextOwnerCompositionSink +{ +public: /*IUnknown*/ + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP QueryInterface(REFIID, void**); + STDMETHODIMP_(ULONG) Release(void); + +public: /*ITextStoreACP*/ + STDMETHODIMP AdviseSink(REFIID, IUnknown*, DWORD); + STDMETHODIMP UnadviseSink(IUnknown*); + STDMETHODIMP RequestLock(DWORD, HRESULT*); + STDMETHODIMP GetStatus(TS_STATUS*); + STDMETHODIMP QueryInsert(LONG, LONG, ULONG, LONG*, LONG*); + STDMETHODIMP GetSelection(ULONG, ULONG, TS_SELECTION_ACP*, ULONG*); + STDMETHODIMP SetSelection(ULONG, const TS_SELECTION_ACP*); + STDMETHODIMP GetText(LONG, LONG, WCHAR*, ULONG, ULONG*, TS_RUNINFO*, ULONG, + ULONG*, LONG*); + STDMETHODIMP SetText(DWORD, LONG, LONG, const WCHAR*, ULONG, TS_TEXTCHANGE*); + STDMETHODIMP GetFormattedText(LONG, LONG, IDataObject**); + STDMETHODIMP GetEmbedded(LONG, REFGUID, REFIID, IUnknown**); + STDMETHODIMP QueryInsertEmbedded(const GUID*, const FORMATETC*, BOOL*); + STDMETHODIMP InsertEmbedded(DWORD, LONG, LONG, IDataObject*, TS_TEXTCHANGE*); + STDMETHODIMP RequestSupportedAttrs(DWORD, ULONG, const TS_ATTRID*); + STDMETHODIMP RequestAttrsAtPosition(LONG, ULONG, const TS_ATTRID*, DWORD); + STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG, ULONG, + const TS_ATTRID*, DWORD); + STDMETHODIMP FindNextAttrTransition(LONG, LONG, ULONG, const TS_ATTRID*, + DWORD, LONG*, BOOL*, LONG*); + STDMETHODIMP RetrieveRequestedAttrs(ULONG, TS_ATTRVAL*, ULONG*); + STDMETHODIMP GetEndACP(LONG*); + STDMETHODIMP GetActiveView(TsViewCookie*); + STDMETHODIMP GetACPFromPoint(TsViewCookie, const POINT*, DWORD, LONG*); + STDMETHODIMP GetTextExt(TsViewCookie, LONG, LONG, RECT*, BOOL*); + STDMETHODIMP GetScreenExt(TsViewCookie, RECT*); + STDMETHODIMP GetWnd(TsViewCookie, HWND*); + STDMETHODIMP InsertTextAtSelection(DWORD, const WCHAR*, ULONG, LONG*, LONG*, + TS_TEXTCHANGE*); + STDMETHODIMP InsertEmbeddedAtSelection(DWORD, IDataObject*, LONG*, LONG*, + TS_TEXTCHANGE*); + +public: /*ITfContextOwnerCompositionSink*/ + STDMETHODIMP OnStartComposition(ITfCompositionView*, BOOL*); + STDMETHODIMP OnUpdateComposition(ITfCompositionView*, ITfRange*); + STDMETHODIMP OnEndComposition(ITfCompositionView*); + +public: + static void Initialize(void); + static void Terminate(void); + static void SetIMEOpenState(PRBool); + static PRBool GetIMEOpenState(void); + + static void CommitComposition(PRBool aDiscard) + { + if (!sTsfTextStore) return; + sTsfTextStore->CommitCompositionInternal(aDiscard); + } + + static void SetIMEEnabled(PRUint32 aState) + { + if (!sTsfTextStore) return; + sTsfTextStore->SetIMEEnabledInternal(aState); + } + + static nsresult OnFocusChange(PRBool, nsWindow*, PRUint32); + + static nsresult OnTextChange(PRUint32 aStart, + PRUint32 aOldEnd, + PRUint32 aNewEnd) + { + if (!sTsfTextStore) return NS_OK; + return sTsfTextStore->OnTextChangeInternal(aStart, aOldEnd, aNewEnd); + } + + static void OnTextChangeMsg(void) + { + if (!sTsfTextStore) return; + // Notify TSF text change + // (see comments on WM_USER_TSF_TEXTCHANGE in nsTextStore.h) + sTsfTextStore->OnTextChangeMsgInternal(); + } + + static nsresult OnSelectionChange(void) + { + if (!sTsfTextStore) return NS_OK; + return sTsfTextStore->OnSelectionChangeInternal(); + } + + static void* GetNativeData(void) + { + // Returns the address of the pointer so that the TSF automatic test can + // replace the system object with a custom implementation for testing. + Initialize(); // Apply any previous changes + return (void*) & sTsfThreadMgr; + } + +protected: + nsTextStore(); + ~nsTextStore(); + + PRBool Create(nsWindow*, PRUint32); + PRBool Destroy(void); + PRBool Focus(void); + PRBool Blur(void); + + HRESULT LoadManagers(void); + HRESULT SetSelectionInternal(const TS_SELECTION_ACP*); + HRESULT OnStartCompositionInternal(ITfCompositionView*, ITfRange*, PRBool); + void CommitCompositionInternal(PRBool); + void SetIMEEnabledInternal(PRUint32 aState); + nsresult OnTextChangeInternal(PRUint32, PRUint32, PRUint32); + void OnTextChangeMsgInternal(void); + nsresult OnSelectionChangeInternal(void); + + // TSF display attribute manager, loaded by LoadManagers + nsRefPtr mDAMgr; + // TSF category manager, loaded by LoadManagers + nsRefPtr mCatMgr; + + // Document manager for the currently focused editor + nsRefPtr mDocumentMgr; + // Edit cookie associated with the current editing context + DWORD mEditCookie; + // Editing context at the bottom of mDocumentMgr's context stack + nsRefPtr mContext; + // Currently installed notification sink + nsRefPtr mSink; + // TS_AS_* mask of what events to notify + DWORD mSinkMask; + // Window containing the focused editor + nsWindow* mWindow; + // 0 if not locked, otherwise TS_LF_* indicating the current lock + DWORD mLock; + // 0 if no lock is queued, otherwise TS_LF_* indicating the queue lock + DWORD mLockQueued; + // Cumulative text change offsets since the last notification + TS_TEXTCHANGE mTextChange; + // NULL if no composition is active, otherwise the current composition + nsRefPtr mCompositionView; + // Current copy of the active composition string. Only mCompositionString is + // changed during a InsertTextAtSelection call if we have a composition. + // mCompositionString acts as a buffer until OnUpdateComposition is called + // and mCompositionString is flushed to editor through NS_TEXT_TEXT. This + // way all changes are updated in batches to avoid inconsistencies/artifacts. + nsString mCompositionString; + // "Current selection" during a composition, in ACP offsets. + // We use a fake selection during a composition because editor code doesn't + // like us accessing the actual selection during a composition. So we leave + // the actual selection alone and get/set mCompositionSelection instead + // during GetSelection/SetSelection calls. + TS_SELECTION_ACP mCompositionSelection; + // The start and length of the current active composition, in ACP offsets + LONG mCompositionStart; + LONG mCompositionLength; + + // TSF thread manager object for the current application + static ITfThreadMgr* sTsfThreadMgr; + // TSF client ID for the current application + static DWORD sTsfClientId; + // Current text store. Currently only ONE nsTextStore instance is ever used, + // although Create is called when an editor is focused and Destroy called + // when the focused editor is blurred. + static nsTextStore* sTsfTextStore; + + // Message the Tablet Input Panel uses to flush text during blurring. + // See comments in Destroy + static UINT sFlushTIPInputMessage; + +private: + ULONG mRefCnt; +}; + +#endif /*NSTEXTSTORE_H_*/ diff --git a/widget/src/windows/nsWindow.cpp b/widget/src/windows/nsWindow.cpp index add4d595826d..2be788c8ed34 100644 --- a/widget/src/windows/nsWindow.cpp +++ b/widget/src/windows/nsWindow.cpp @@ -34,6 +34,7 @@ * Dainis Jonitis * Christian Biesinger * Mats Palmgren + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -150,6 +151,9 @@ #include "prmem.h" +#ifdef NS_ENABLE_TSF +#include "nsTextStore.h" +#endif //NS_ENABLE_TSF #ifdef WINCE @@ -746,14 +750,19 @@ nsWindow::nsWindow() : nsBaseWidget() mIsTopWidgetWindow = PR_FALSE; mLastKeyboardLayout = 0; +#ifdef NS_ENABLE_TSF + if (!sInstanceCount) + nsTextStore::Initialize(); +#endif //NS_ENABLE_TSF + #ifndef WINCE if (!sInstanceCount && SUCCEEDED(::OleInitialize(NULL))) { sIsOleInitialized = TRUE; } NS_ASSERTION(sIsOleInitialized, "***** OLE is not initialized!\n"); +#endif sInstanceCount++; -#endif } //------------------------------------------------------------------------- @@ -788,11 +797,17 @@ nsWindow::~nsWindow() SetCursor(eCursor_standard); } + sInstanceCount--; + +#ifdef NS_ENABLE_TSF + if (!sInstanceCount) + nsTextStore::Terminate(); +#endif //NS_ENABLE_TSF + #ifndef WINCE // // delete any of the IME structures that we allocated // - sInstanceCount--; if (sInstanceCount == 0) { if (sIMECompUnicode) delete sIMECompUnicode; @@ -2826,6 +2841,12 @@ void* nsWindow::GetNativeData(PRUint32 aDataType) #else return (void*)::GetDC(mWnd); #endif + +#ifdef NS_ENABLE_TSF + case NS_NATIVE_TSF_POINTER: + return nsTextStore::GetNativeData(); +#endif //NS_ENABLE_TSF + case NS_NATIVE_COLORMAP: default: break; @@ -5303,6 +5324,12 @@ PRBool nsWindow::ProcessMessage(UINT msg, WPARAM wParam, LPARAM lParam, LRESULT } #endif // WINCE +#ifdef NS_ENABLE_TSF + else if (msg == WM_USER_TSF_TEXTCHANGE) { + nsTextStore::OnTextChangeMsg(); + } +#endif //NS_ENABLE_TSF + } break; #ifndef WINCE @@ -7427,8 +7454,8 @@ PRBool nsWindow::OnIMEQueryCharPosition(LPARAM aData, LRESULT *oResult) nsIntRect r; if (!useCaretRect) { - nsQueryContentEvent charRect(PR_TRUE, NS_QUERY_CHARACTER_RECT, this); - charRect.InitForQueryCharacterRect(offset); + nsQueryContentEvent charRect(PR_TRUE, NS_QUERY_TEXT_RECT, this); + charRect.InitForQueryTextRect(offset, 1); InitEvent(charRect, &point); DispatchWindowEvent(&charRect); if (charRect.mSucceeded) @@ -7536,6 +7563,11 @@ NS_IMETHODIMP nsWindow::ResetInputState() #ifdef DEBUG_KBSTATE printf("ResetInputState\n"); #endif + +#ifdef NS_ENABLE_TSF + nsTextStore::CommitComposition(PR_FALSE); +#endif //NS_ENABLE_TSF + HIMC hIMC = ::ImmGetContext(mWnd); if (hIMC) { BOOL ret = FALSE; @@ -7553,6 +7585,11 @@ NS_IMETHODIMP nsWindow::SetIMEOpenState(PRBool aState) #ifdef DEBUG_KBSTATE printf("SetIMEOpenState %s\n", (aState ? "Open" : "Close")); #endif + +#ifdef NS_ENABLE_TSF + nsTextStore::SetIMEOpenState(aState); +#endif //NS_ENABLE_TSF + HIMC hIMC = ::ImmGetContext(mWnd); if (hIMC) { ::ImmSetOpenStatus(hIMC, aState ? TRUE : FALSE); @@ -7571,12 +7608,21 @@ NS_IMETHODIMP nsWindow::GetIMEOpenState(PRBool* aState) ::ImmReleaseContext(mWnd, hIMC); } else *aState = PR_FALSE; + +#ifdef NS_ENABLE_TSF + *aState |= nsTextStore::GetIMEOpenState(); +#endif //NS_ENABLE_TSF + return NS_OK; } //========================================================================== NS_IMETHODIMP nsWindow::SetIMEEnabled(PRUint32 aState) { +#ifdef NS_ENABLE_TSF + nsTextStore::SetIMEEnabled(aState); +#endif //NS_ENABLE_TSF + if (sIMEIsComposing) ResetInputState(); mIMEEnabled = aState; @@ -7603,6 +7649,11 @@ NS_IMETHODIMP nsWindow::CancelIMEComposition() #ifdef DEBUG_KBSTATE printf("CancelIMEComposition\n"); #endif + +#ifdef NS_ENABLE_TSF + nsTextStore::CommitComposition(PR_TRUE); +#endif //NS_ENABLE_TSF + HIMC hIMC = ::ImmGetContext(mWnd); if (hIMC) { BOOL ret = FALSE; @@ -7808,6 +7859,30 @@ static VOID CALLBACK nsGetAttentionTimerFunc(HWND hwnd, UINT uMsg, UINT idEvent, gAttentionTimerMonitor->KillTimer(hwnd); } + +#ifdef NS_ENABLE_TSF +NS_IMETHODIMP +nsWindow::OnIMEFocusChange(PRBool aFocus) +{ + return nsTextStore::OnFocusChange(aFocus, this, mIMEEnabled); +} + +NS_IMETHODIMP +nsWindow::OnIMETextChange(PRUint32 aStart, + PRUint32 aOldEnd, + PRUint32 aNewEnd) +{ + return nsTextStore::OnTextChange(aStart, aOldEnd, aNewEnd); +} + +NS_IMETHODIMP +nsWindow::OnIMESelectionChange(void) +{ + return nsTextStore::OnSelectionChange(); +} +#endif //NS_ENABLE_TSF + + // Draw user's attention to this window until it comes to foreground. NS_IMETHODIMP nsWindow::GetAttention(PRInt32 aCycleCount) diff --git a/widget/src/windows/nsWindow.h b/widget/src/windows/nsWindow.h index 5eed7a2d229e..a58fd8cc12d2 100644 --- a/widget/src/windows/nsWindow.h +++ b/widget/src/windows/nsWindow.h @@ -25,6 +25,7 @@ * Makoto Kato * Dainis Jonitis * Masayuki Nakano + * Ningjie Chen * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -73,6 +74,11 @@ struct nsFakeCharMessage; #include "gfxWindowsSurface.h" +// Text Services Framework support +#ifndef WINCE +#define NS_ENABLE_TSF +#endif //WINCE + #define IME_MAX_CHAR_POS 64 #define NSRGB_2_COLOREF(color) \ @@ -235,6 +241,12 @@ public: NS_IMETHOD CancelIMEComposition(); NS_IMETHOD GetToggledKeyState(PRUint32 aKeyCode, PRBool* aLEDState); +#ifdef NS_ENABLE_TSF + NS_IMETHOD OnIMEFocusChange(PRBool aFocus); + NS_IMETHOD OnIMETextChange(PRUint32 aStart, PRUint32 aOldEnd, PRUint32 aNewEnd); + NS_IMETHOD OnIMESelectionChange(void); +#endif //NS_ENABLE_TSF + PRBool IMEMouseHandling(PRInt32 aAction, LPARAM lParam); PRBool IMECompositionHitTest(POINT * ptPos); PRBool HandleMouseActionOfIME(PRInt32 aAction, POINT* ptPos); @@ -250,6 +262,8 @@ public: LPARAM lParam, PRBool aIsContextMenuKey = PR_FALSE, PRInt16 aButton = nsMouseEvent::eLeftButton); + virtual PRBool DispatchWindowEvent(nsGUIEvent* event); + virtual PRBool DispatchWindowEvent(nsGUIEvent*event, nsEventStatus &aStatus); #ifdef ACCESSIBILITY virtual PRBool DispatchAccessibleEvent(PRUint32 aEventType, nsIAccessible** aAccessible, nsIntPoint* aPoint = nsnull); already_AddRefed GetRootAccessible(); @@ -303,8 +317,6 @@ protected: LRESULT ProcessKeyDownMessage(const MSG &aMsg, PRBool *aEventDispatched); - virtual PRBool DispatchWindowEvent(nsGUIEvent* event); - virtual PRBool DispatchWindowEvent(nsGUIEvent*event, nsEventStatus &aStatus); // Allow Derived classes to modify the height that is passed // when the window is created or resized. diff --git a/widget/src/xpwidgets/nsBaseWidget.h b/widget/src/xpwidgets/nsBaseWidget.h index b875c0005afd..b3da4b2c2c3b 100644 --- a/widget/src/xpwidgets/nsBaseWidget.h +++ b/widget/src/xpwidgets/nsBaseWidget.h @@ -139,13 +139,16 @@ public: NS_IMETHOD BeginResizeDrag(nsGUIEvent* aEvent, PRInt32 aHorizontal, PRInt32 aVertical); virtual nsresult ActivateNativeMenuItemAt(const nsAString& indexString) { return NS_ERROR_NOT_IMPLEMENTED; } virtual nsresult ForceUpdateNativeMenuAt(const nsAString& indexString) { return NS_ERROR_NOT_IMPLEMENTED; } - NS_IMETHOD ResetInputState() { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD ResetInputState() { return NS_OK; } NS_IMETHOD SetIMEOpenState(PRBool aState) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD GetIMEOpenState(PRBool* aState) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD SetIMEEnabled(PRUint32 aState) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHOD GetIMEEnabled(PRUint32* aState) { return NS_ERROR_NOT_IMPLEMENTED; } - NS_IMETHOD CancelIMEComposition() { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD CancelIMEComposition() { return NS_OK; } NS_IMETHOD GetToggledKeyState(PRUint32 aKeyCode, PRBool* aLEDState) { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD OnIMEFocusChange(PRBool aFocus) { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD OnIMETextChange(PRUint32 aStart, PRUint32 aOldEnd, PRUint32 aNewEnd) { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD OnIMESelectionChange(void) { return NS_ERROR_NOT_IMPLEMENTED; } protected: diff --git a/widget/tests/Makefile.in b/widget/tests/Makefile.in index 698e28cbef96..e6503525e4d1 100644 --- a/widget/tests/Makefile.in +++ b/widget/tests/Makefile.in @@ -39,9 +39,22 @@ DEPTH = ../.. topsrcdir = @top_srcdir@ srcdir = @srcdir@ VPATH = @srcdir@ -relativesrcdir = widget/test +relativesrcdir = widget/tests include $(DEPTH)/config/autoconf.mk + +ifeq ($(MOZ_WIDGET_TOOLKIT),windows) +ifneq ($(OS_ARCH), WINCE) +CPP_UNIT_TESTS += TestWinTSF.cpp \ + $(NULL) +REQUIRES += appshell content docshell \ + dom embed_base gfx layout locale \ + necko string thebes uriloader view \ + webbrwsr widget xpcom \ + $(NULL) +endif +endif + include $(topsrcdir)/config/rules.mk _TEST_FILES = test_bug343416.xul \ diff --git a/widget/tests/TestWinTSF.cpp b/widget/tests/TestWinTSF.cpp new file mode 100644 index 000000000000..a70a653f9913 --- /dev/null +++ b/widget/tests/TestWinTSF.cpp @@ -0,0 +1,1830 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Ningjie Chen + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +/* This tests Mozilla's Text Services Framework implementation (bug #88831) + * + * The Mozilla implementation interacts with the TSF system through a + * system-provided COM interface, ITfThreadMgr. This tests works by swapping + * the system version of the interface with a custom version implemented in + * here. This way the Mozilla implementation thinks it's interacting with the + * system but in fact is interacting with this test program. This allows the + * test program to access and test every aspect of the Mozilla implementation. + */ + +#include +#include + +#include "TestHarness.h" + +#define WM_USER_TSF_TEXTCHANGE (WM_USER + 0x100) + +#ifndef MOZILLA_INTERNAL_API +// some of the includes make use of internal string types +#define nsAString_h___ +#define nsString_h___ +class nsAFlatString; +class nsAFlatCString; +#endif + +#include "nsWeakReference.h" +#include "nsIAppShell.h" +#include "nsWidgetsCID.h" +#include "nsIAppShellService.h" +#include "nsAppShellCID.h" +#include "nsNetUtil.h" +#include "nsIWebBrowserChrome.h" +#include "nsIXULWindow.h" +#include "nsIBaseWindow.h" +#include "nsIDOMWindowInternal.h" +#include "nsIDocShell.h" +#include "nsIWidget.h" +#include "nsIPresShell.h" +#include "nsPresContext.h" +#include "nsIFrame.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIDOMHTMLDocument.h" +#include "nsIDOMHTMLBodyElement.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMHTMLTextAreaElement.h" +#include "nsISelectionController.h" +#include "nsIViewManager.h" + +#ifndef MOZILLA_INTERNAL_API +#undef nsString_h___ +#undef nsAString_h___ +#endif + +static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID); + +class TSFImpl; + +class TestApp : public nsIWebProgressListener, public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + + TestApp() : mFailed(PR_FALSE) {} + ~TestApp() {} + + nsresult Run(void); + PRBool CheckFailed(void); + + typedef PRBool (TestApp::*test_type)(void); + +protected: + nsresult Init(void); + nsresult Term(void); + PRBool RunTest(test_type aTest, PRBool aLock = PR_TRUE); + + PRBool TestFocus(void); + PRBool TestClustering(void); + PRBool TestSelection(void); + PRBool TestText(void); + PRBool TestExtents(void); + PRBool TestComposition(void); + PRBool TestNotification(void); + + PRBool TestApp::TestSelectionInternal(char* aTestName, + LONG aStart, + LONG aEnd, + TsActiveSelEnd aSelEnd); + PRBool TestCompositionSelectionAndText(char* aTestName, + LONG aExpectedSelStart, + LONG aExpectedSelEnd, + nsString& aReferenceString); + PRBool TestNotificationTextChange(nsIWidget* aWidget, + PRUint32 aCode, + const nsAString& aCharacter, + LONG aStart, + LONG aOldEnd, + LONG aNewEnd); + nsresult GetSelCon(nsISelectionController** aSelCon); + + PRBool mFailed; + nsString mTestString; + nsRefPtr mImpl; + nsCOMPtr mAppShell; + nsCOMPtr mWindow; + nsCOMPtr mCurrentNode; + nsCOMPtr mInput; + nsCOMPtr mTextArea; + nsCOMPtr mButton; +}; + +NS_IMETHODIMP +TestApp::OnProgressChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + PRInt32 aCurSelfProgress, + PRInt32 aMaxSelfProgress, + PRInt32 aCurTotalProgress, + PRInt32 aMaxTotalProgress) +{ + return NS_OK; +} + +NS_IMETHODIMP +TestApp::OnLocationChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + nsIURI *aLocation) +{ + return NS_OK; +} + +NS_IMETHODIMP +TestApp::OnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + nsresult aStatus, + const PRUnichar *aMessage) +{ + return NS_OK; +} + +NS_IMETHODIMP +TestApp::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + PRUint32 aState) +{ + return NS_OK; +} + +// Simple TSF manager implementation for testing +// Most methods are not implemented, but the ones used by Mozilla are +// +// XXX Implement appropriate methods here as the Mozilla TSF code changes +// +class TSFImpl : public ITfThreadMgr, public ITfDocumentMgr, public ITfContext, + public ITfRangeACP, public ITfCompositionView, + public ITextStoreACPSink +{ +private: + ULONG mRefCnt; + nsRefPtr mTestApp; + +public: + TestApp::test_type mTest; + TestApp::test_type mOnFocus; + TestApp::test_type mOnBlur; + nsRefPtr mStore; + PRBool mFocused; + PRBool mContextPushed; + PRBool mDeactivated; + PRUint32 mFocusCount; + PRUint32 mBlurCount; + PRUint32 mRangeStart; + PRUint32 mRangeLength; + PRBool mTextChanged; + PRBool mSelChanged; + TS_TEXTCHANGE mTextChangeData; + +public: + TSFImpl(TestApp* test) : mTestApp(test), mTest(nsnull), + mRefCnt(0), mFocused(PR_FALSE), mDeactivated(PR_FALSE), + mFocusCount(0), mBlurCount(0), mRangeStart(0), mRangeLength(0), + mContextPushed(PR_FALSE), mOnFocus(nsnull), mOnBlur(nsnull), + mTextChanged(PR_FALSE), mSelChanged(PR_FALSE) + { + } + + ~TSFImpl() + { + } + +public: // IUnknown + + STDMETHODIMP QueryInterface(REFIID riid, void** ppUnk) + { + *ppUnk = NULL; + if (IID_IUnknown == riid || IID_ITfThreadMgr == riid) + *ppUnk = static_cast(this); + else if (IID_ITfDocumentMgr == riid) + *ppUnk = static_cast(this); + else if (IID_ITfContext == riid) + *ppUnk = static_cast(this); + else if (IID_ITfRange == riid || IID_ITfRangeACP == riid) + *ppUnk = static_cast(this); + else if (IID_ITextStoreACPSink == riid) + *ppUnk = static_cast(this); + if (*ppUnk) + AddRef(); + return *ppUnk ? S_OK : E_NOINTERFACE; + } + + STDMETHODIMP_(ULONG) AddRef(void) + { + return ++mRefCnt; + } + + STDMETHODIMP_(ULONG) Release(void) + { + if (--mRefCnt) return mRefCnt; + delete this; + return 0; + } + +public: // ITfThreadMgr + + STDMETHODIMP Activate(TfClientId *ptid) + { + *ptid = 1; + return S_OK; + } + + STDMETHODIMP Deactivate(void) + { + SetFocus(NULL); + mDeactivated = PR_TRUE; + return S_OK; + } + + STDMETHODIMP CreateDocumentMgr(ITfDocumentMgr **ppdim) + { + (*ppdim) = this; + (*ppdim)->AddRef(); + return S_OK; + } + + STDMETHODIMP EnumDocumentMgrs(IEnumTfDocumentMgrs **ppEnum) + { + NS_NOTREACHED("ITfThreadMgr::EnumDocumentMgrs"); + return E_NOTIMPL; + } + + STDMETHODIMP GetFocus(ITfDocumentMgr **ppdimFocus) + { + (*ppdimFocus) = mFocused ? this : NULL; + if (*ppdimFocus) (*ppdimFocus)->AddRef(); + return S_OK; + } + + STDMETHODIMP SetFocus(ITfDocumentMgr *pdimFocus) + { + mFocused = pdimFocus != NULL; + if (mFocused) { + ++mFocusCount; + if (mOnFocus) (mTestApp->*mOnFocus)(); + } else { + ++mBlurCount; + if (mOnBlur) (mTestApp->*mOnBlur)(); + } + return S_OK; + } + + STDMETHODIMP AssociateFocus(HWND hwnd, ITfDocumentMgr *pdimNew, + ITfDocumentMgr **ppdimPrev) + { + NS_NOTREACHED("ITfThreadMgr::AssociateFocus"); + return E_NOTIMPL; + } + + STDMETHODIMP IsThreadFocus(BOOL *pfThreadFocus) + { + *pfThreadFocus = TRUE; + return S_OK; + } + + STDMETHODIMP GetFunctionProvider(REFCLSID clsid, + ITfFunctionProvider **ppFuncProv) + { + NS_NOTREACHED("ITfThreadMgr::GetFunctionProvider"); + return E_NOTIMPL; + } + + STDMETHODIMP EnumFunctionProviders(IEnumTfFunctionProviders **ppEnum) + { + NS_NOTREACHED("ITfThreadMgr::EnumFunctionProviders"); + return E_NOTIMPL; + } + + STDMETHODIMP GetGlobalCompartment(ITfCompartmentMgr **ppCompMgr) + { + NS_NOTREACHED("ITfThreadMgr::GetGlobalCompartment"); + return E_NOTIMPL; + } + +public: // ITfDocumentMgr + + STDMETHODIMP CreateContext(TfClientId tidOwner, DWORD dwFlags, + IUnknown *punk, ITfContext **ppic, + TfEditCookie *pecTextStore) + { + punk->QueryInterface(IID_ITextStoreACP, getter_AddRefs(mStore)); + NS_ENSURE_TRUE(mStore, E_FAIL); + HRESULT hr = mStore->AdviseSink(IID_ITextStoreACPSink, + static_cast(this), + TS_AS_ALL_SINKS); + if (FAILED(hr)) mStore = NULL; + NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL); + (*ppic) = this; + (*ppic)->AddRef(); + *pecTextStore = 1; + return S_OK; + } + + STDMETHODIMP Push(ITfContext *pic) + { + mContextPushed = PR_TRUE; + return S_OK; + } + + STDMETHODIMP Pop(DWORD dwFlags) + { + if (!mStore || dwFlags != TF_POPF_ALL) return E_FAIL; + mStore->UnadviseSink(static_cast(this)); + mStore = NULL; + mContextPushed = PR_FALSE; + return S_OK; + } + + STDMETHODIMP GetTop(ITfContext **ppic) + { + (*ppic) = mContextPushed ? this : NULL; + if (*ppic) (*ppic)->AddRef(); + return S_OK; + } + + STDMETHODIMP GetBase(ITfContext **ppic) + { + (*ppic) = mContextPushed ? this : NULL; + if (*ppic) (*ppic)->AddRef(); + return S_OK; + } + + STDMETHODIMP EnumContexts(IEnumTfContexts **ppEnum) + { + NS_NOTREACHED("ITfDocumentMgr::EnumContexts"); + return E_NOTIMPL; + } + +public: // ITfContext + + STDMETHODIMP RequestEditSession(TfClientId tid, ITfEditSession *pes, + DWORD dwFlags, HRESULT *phrSession) + { + NS_NOTREACHED("ITfContext::RequestEditSession"); + return E_NOTIMPL; + } + + STDMETHODIMP InWriteSession(TfClientId tid, BOOL *pfWriteSession) + { + NS_NOTREACHED("ITfContext::InWriteSession"); + return E_NOTIMPL; + } + + STDMETHODIMP GetSelection(TfEditCookie ec, ULONG ulIndex, ULONG ulCount, + TF_SELECTION *pSelection, ULONG *pcFetched) + { + NS_NOTREACHED("ITfContext::GetSelection"); + return E_NOTIMPL; + } + + STDMETHODIMP SetSelection(TfEditCookie ec, ULONG ulCount, + const TF_SELECTION *pSelection) + { + NS_NOTREACHED("ITfContext::SetSelection"); + return E_NOTIMPL; + } + + STDMETHODIMP GetStart(TfEditCookie ec, ITfRange **ppStart) + { + NS_NOTREACHED("ITfContext::GetStart"); + return E_NOTIMPL; + } + + STDMETHODIMP GetEnd(TfEditCookie ec, ITfRange **ppEnd) + { + NS_NOTREACHED("ITfContext::GetEnd"); + return E_NOTIMPL; + } + + STDMETHODIMP GetActiveView(ITfContextView **ppView) + { + NS_NOTREACHED("ITfContext::GetActiveView"); + return E_NOTIMPL; + } + + STDMETHODIMP EnumViews(IEnumTfContextViews **ppEnum) + { + NS_NOTREACHED("ITfContext::EnumViews"); + return E_NOTIMPL; + } + + STDMETHODIMP GetStatus(TF_STATUS *pdcs) + { + NS_NOTREACHED("ITfContext::GetStatus"); + return E_NOTIMPL; + } + + STDMETHODIMP GetProperty(REFGUID guidProp, ITfProperty **ppProp) + { + NS_NOTREACHED("ITfContext::GetProperty"); + return E_NOTIMPL; + } + + STDMETHODIMP GetAppProperty(REFGUID guidProp, ITfReadOnlyProperty **ppProp) + { + NS_NOTREACHED("ITfContext::GetAppProperty"); + return E_NOTIMPL; + } + + STDMETHODIMP TrackProperties(const GUID **prgProp, ULONG cProp, + const GUID **prgAppProp, ULONG cAppProp, + ITfReadOnlyProperty **ppProperty) + { + NS_NOTREACHED("ITfContext::TrackProperties"); + return E_NOTIMPL; + } + + STDMETHODIMP EnumProperties(IEnumTfProperties **ppEnum) + { + NS_NOTREACHED("ITfContext::EnumProperties"); + return E_NOTIMPL; + } + + STDMETHODIMP GetDocumentMgr(ITfDocumentMgr **ppDm) + { + NS_NOTREACHED("ITfContext::GetDocumentMgr"); + return E_NOTIMPL; + } + + STDMETHODIMP CreateRangeBackup(TfEditCookie ec, ITfRange *pRange, + ITfRangeBackup **ppBackup) + { + NS_NOTREACHED("ITfContext::CreateRangeBackup"); + return E_NOTIMPL; + } + +public: // ITfRangeACP + + STDMETHODIMP GetText(TfEditCookie ec, DWORD dwFlags, WCHAR *pchText, + ULONG cchMax, ULONG *pcch) + { + NS_NOTREACHED("ITfRangeACP::GetText"); + return E_NOTIMPL; + } + + STDMETHODIMP SetText(TfEditCookie ec, DWORD dwFlags, const WCHAR *pchText, + LONG cch) + { + NS_NOTREACHED("ITfRangeACP::SetText"); + return E_NOTIMPL; + } + + STDMETHODIMP GetFormattedText(TfEditCookie ec, IDataObject **ppDataObject) + { + NS_NOTREACHED("ITfRangeACP::GetFormattedText"); + return E_NOTIMPL; + } + + STDMETHODIMP GetEmbedded(TfEditCookie ec, REFGUID rguidService, REFIID riid, + IUnknown **ppunk) + { + NS_NOTREACHED("ITfRangeACP::GetEmbedded"); + return E_NOTIMPL; + } + + STDMETHODIMP InsertEmbedded(TfEditCookie ec, DWORD dwFlags, + IDataObject *pDataObject) + { + NS_NOTREACHED("ITfRangeACP::InsertEmbedded"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftStart(TfEditCookie ec, LONG cchReq, LONG *pcch, + const TF_HALTCOND *pHalt) + { + NS_NOTREACHED("ITfRangeACP::ShiftStart"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftEnd(TfEditCookie ec, LONG cchReq, LONG *pcch, + const TF_HALTCOND *pHalt) + { + NS_NOTREACHED("ITfRangeACP::ShiftEnd"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftStartToRange(TfEditCookie ec, ITfRange *pRange, + TfAnchor aPos) + { + NS_NOTREACHED("ITfRangeACP::ShiftStartToRange"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftEndToRange(TfEditCookie ec, ITfRange *pRange, + TfAnchor aPos) + { + NS_NOTREACHED("ITfRangeACP::ShiftEndToRange"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftStartRegion(TfEditCookie ec, TfShiftDir dir, + BOOL *pfNoRegion) + { + NS_NOTREACHED("ITfRangeACP::ShiftStartRegion"); + return E_NOTIMPL; + } + + STDMETHODIMP ShiftEndRegion(TfEditCookie ec, TfShiftDir dir, + BOOL *pfNoRegion) + { + NS_NOTREACHED("ITfRangeACP::ShiftEndRegion"); + return E_NOTIMPL; + } + + STDMETHODIMP IsEmpty(TfEditCookie ec, BOOL *pfEmpty) + { + NS_NOTREACHED("ITfRangeACP::IsEmpty"); + return E_NOTIMPL; + } + + STDMETHODIMP Collapse(TfEditCookie ec, TfAnchor aPos) + { + NS_NOTREACHED("ITfRangeACP::Collapse"); + return E_NOTIMPL; + } + + STDMETHODIMP IsEqualStart(TfEditCookie ec, ITfRange *pWith, + TfAnchor aPos, BOOL *pfEqual) + { + NS_NOTREACHED("ITfRangeACP::IsEqualStart"); + return E_NOTIMPL; + } + + STDMETHODIMP IsEqualEnd(TfEditCookie ec, ITfRange *pWith, + TfAnchor aPos, BOOL *pfEqual) + { + NS_NOTREACHED("ITfRangeACP::IsEqualEnd"); + return E_NOTIMPL; + } + + STDMETHODIMP CompareStart(TfEditCookie ec, ITfRange *pWith, + TfAnchor aPos, LONG *plResult) + { + NS_NOTREACHED("ITfRangeACP::CompareStart"); + return E_NOTIMPL; + } + + STDMETHODIMP CompareEnd(TfEditCookie ec, ITfRange *pWith, + TfAnchor aPos, LONG *plResult) + { + NS_NOTREACHED("ITfRangeACP::CompareEnd"); + return E_NOTIMPL; + } + + STDMETHODIMP AdjustForInsert(TfEditCookie ec, ULONG cchInsert, + BOOL *pfInsertOk) + { + NS_NOTREACHED("ITfRangeACP::AdjustForInsert"); + return E_NOTIMPL; + } + + STDMETHODIMP GetGravity(TfGravity *pgStart, TfGravity *pgEnd) + { + NS_NOTREACHED("ITfRangeACP::GetGravity"); + return E_NOTIMPL; + } + + STDMETHODIMP SetGravity(TfEditCookie ec, TfGravity gStart, TfGravity gEnd) + { + NS_NOTREACHED("ITfRangeACP::SetGravity"); + return E_NOTIMPL; + } + + STDMETHODIMP Clone(ITfRange **ppClone) + { + NS_NOTREACHED("ITfRangeACP::Clone"); + return E_NOTIMPL; + } + + STDMETHODIMP GetContext(ITfContext **ppContext) + { + NS_NOTREACHED("ITfRangeACP::GetContext"); + return E_NOTIMPL; + } + + STDMETHODIMP GetExtent(LONG *pacpAnchor, LONG *pcch) + { + *pacpAnchor = LONG(mRangeStart); + *pcch = LONG(mRangeLength); + return S_OK; + } + + STDMETHODIMP SetExtent(LONG acpAnchor, LONG cch) + { + mRangeStart = PRUint32(acpAnchor); + mRangeLength = PRUint32(cch); + return S_OK; + } + +public: // ITfCompositionView + + STDMETHODIMP GetOwnerClsid(CLSID* pclsid) + { + NS_NOTREACHED("ITfCompositionView::GetOwnerClsid"); + return E_NOTIMPL; + } + + STDMETHODIMP GetRange(ITfRange** ppRange) + { + (*ppRange) = this; + (*ppRange)->AddRef(); + return S_OK; + } + +public: // ITextStoreACPSink + + STDMETHODIMP OnTextChange(DWORD dwFlags, const TS_TEXTCHANGE *pChange) + { + mTextChanged = PR_TRUE; + mTextChangeData = *pChange; + return S_OK; + } + + STDMETHODIMP OnSelectionChange(void) + { + mSelChanged = PR_TRUE; + return S_OK; + } + + STDMETHODIMP OnLayoutChange(TsLayoutCode lcode, TsViewCookie vcView) + { + return S_OK; + } + + STDMETHODIMP OnStatusChange(DWORD dwFlags) + { + return S_OK; + } + + STDMETHODIMP OnAttrsChange(LONG acpStart, LONG acpEnd, ULONG cAttrs, + const TS_ATTRID *paAttrs) + { + return S_OK; + } + + STDMETHODIMP OnLockGranted(DWORD dwLockFlags) + { + // If we have a test, run it + if (mTest && !(mTestApp->*mTest)()) + return S_FALSE; + return S_OK; + } + + STDMETHODIMP OnStartEditTransaction(void) + { + return S_OK; + } + + STDMETHODIMP OnEndEditTransaction(void) + { + return S_OK; + } +}; + +NS_IMPL_ISUPPORTS2(TestApp, nsIWebProgressListener, + nsISupportsWeakReference) + +nsresult +TestApp::Run(void) +{ + // Create a test window + // We need a full-fledged window to test for TSF functionality + nsresult rv; + mAppShell = do_GetService(kAppShellCID); + NS_ENSURE_TRUE(mAppShell, NS_ERROR_UNEXPECTED); + + nsCOMPtr appShellService( + do_GetService(NS_APPSHELLSERVICE_CONTRACTID)); + NS_ENSURE_TRUE(appShellService, NS_ERROR_UNEXPECTED); + + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), "about:blank", nsnull); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appShellService->CreateTopLevelWindow(nsnull, uri, + nsIWebBrowserChrome::CHROME_DEFAULT, + 800 /*nsIAppShellService::SIZE_TO_CONTENT*/, + 600 /*nsIAppShellService::SIZE_TO_CONTENT*/, + mAppShell, getter_AddRefs(mWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr docShell; + rv = mWindow->GetDocShell(getter_AddRefs(docShell)); + NS_ENSURE_TRUE(docShell, NS_ERROR_UNEXPECTED); + nsCOMPtr progress(do_GetInterface(docShell)); + NS_ENSURE_TRUE(progress, NS_ERROR_UNEXPECTED); + rv = progress->AddProgressListener(this, + nsIWebProgress::NOTIFY_STATE_WINDOW | + nsIWebProgress::NOTIFY_STATUS); + NS_ENSURE_SUCCESS(rv, rv); + + mAppShell->Run(); + return NS_OK; +} + +PRBool +TestApp::CheckFailed(void) +{ + // All windows should be closed by now + if (mImpl && !mImpl->mDeactivated) { + fail("TSF not terminated properly"); + mFailed = PR_TRUE; + } + mImpl = nsnull; + return mFailed; +} + +nsresult +TestApp::Init(void) +{ + // Replace TSF manager pointer + nsCOMPtr baseWindow(do_QueryInterface(mWindow)); + NS_ENSURE_TRUE(baseWindow, NS_ERROR_UNEXPECTED); + nsCOMPtr widget; + nsresult rv = baseWindow->GetMainWidget(getter_AddRefs(widget)); + NS_ENSURE_TRUE(widget, NS_ERROR_UNEXPECTED); + + ITfThreadMgr **mgr = reinterpret_cast( + widget->GetNativeData(NS_NATIVE_TSF_POINTER)); + if (!mgr) { + fail("nsIWidget::GetNativeData(NS_NATIVE_TSF_POINTER) not supported"); + return NS_ERROR_FAILURE; + } + if (*mgr) { + (*mgr)->Deactivate(); + (*mgr)->Release(); + (*mgr) = NULL; + } else { + // This is only for information. The test does not need TSF to run. + printf("TSF not initialized properly (TSF is not enabled/installed?)\n"); + } + + mImpl = new TSFImpl(this); + if (!mImpl) { + return NS_ERROR_OUT_OF_MEMORY; + } + (*mgr) = mImpl; + (*mgr)->AddRef(); + + // Apply the change + reinterpret_cast( + widget->GetNativeData(NS_NATIVE_TSF_POINTER)); + + // Create a couple of text boxes for testing + nsCOMPtr win(do_GetInterface(mWindow)); + NS_ENSURE_TRUE(win, NS_ERROR_UNEXPECTED); + nsCOMPtr document; + rv = win->GetDocument(getter_AddRefs(document)); + NS_ENSURE_TRUE(document, NS_ERROR_UNEXPECTED); + nsCOMPtr htmlDoc(do_QueryInterface(document)); + NS_ENSURE_TRUE(htmlDoc, NS_ERROR_UNEXPECTED); + nsCOMPtr htmlBody; + rv = htmlDoc->GetBody(getter_AddRefs(htmlBody)); + NS_ENSURE_TRUE(htmlBody, NS_ERROR_UNEXPECTED); + + nsCOMPtr form; + rv = htmlDoc->CreateElementNS( + NS_LITERAL_STRING("http://www.w3.org/1999/xhtml"), + NS_LITERAL_STRING("form"), + getter_AddRefs(form)); + nsCOMPtr elem; + rv = htmlDoc->CreateElementNS( + NS_LITERAL_STRING("http://www.w3.org/1999/xhtml"), + NS_LITERAL_STRING("input"), + getter_AddRefs(elem)); + NS_ENSURE_SUCCESS(rv, rv); + elem->SetAttribute(NS_LITERAL_STRING("type"), + NS_LITERAL_STRING("text")); + mInput = do_QueryInterface(elem); + NS_ENSURE_TRUE(mInput, NS_ERROR_UNEXPECTED); + rv = htmlDoc->CreateElementNS( + NS_LITERAL_STRING("http://www.w3.org/1999/xhtml"), + NS_LITERAL_STRING("textarea"), + getter_AddRefs(elem)); + NS_ENSURE_SUCCESS(rv, rv); + mTextArea = do_QueryInterface(elem); + NS_ENSURE_TRUE(mTextArea, NS_ERROR_UNEXPECTED); + rv = htmlDoc->CreateElementNS( + NS_LITERAL_STRING("http://www.w3.org/1999/xhtml"), + NS_LITERAL_STRING("input"), + getter_AddRefs(elem)); + NS_ENSURE_SUCCESS(rv, rv); + elem->SetAttribute(NS_LITERAL_STRING("type"), + NS_LITERAL_STRING("button")); + mButton = do_QueryInterface(elem); + NS_ENSURE_TRUE(mButton, NS_ERROR_UNEXPECTED); + + nsCOMPtr node; + rv = form->AppendChild(mInput, getter_AddRefs(node)); + NS_ENSURE_SUCCESS(rv, rv); + rv = form->AppendChild(mTextArea, getter_AddRefs(node)); + NS_ENSURE_SUCCESS(rv, rv); + rv = form->AppendChild(mButton, getter_AddRefs(node)); + NS_ENSURE_SUCCESS(rv, rv); + rv = htmlBody->AppendChild(form, getter_AddRefs(node)); + NS_ENSURE_SUCCESS(rv, rv); + + // set a background color manually, + // otherwise the window might be transparent + nsCOMPtr(do_QueryInterface(htmlBody))-> + SetBgColor(NS_LITERAL_STRING("white")); + + widget->Show(PR_TRUE); + widget->SetFocus(); + return NS_OK; +} + +nsresult +TestApp::Term(void) +{ + mCurrentNode = nsnull; + mInput = nsnull; + mTextArea = nsnull; + mButton = nsnull; + + nsCOMPtr win(do_GetInterface(mWindow)); + if (win) + win->Close(); + win = nsnull; + mWindow = nsnull; + + if (mAppShell) + mAppShell->Exit(); + mAppShell = nsnull; + return NS_OK; +} + +PRBool +TestApp::RunTest(test_type aTest, PRBool aLock) +{ + PRBool succeeded; + if (aLock && mImpl->mStore) { + mImpl->mTest = aTest; + HRESULT hr = E_FAIL; + mImpl->mStore->RequestLock(TS_LF_READWRITE | TS_LF_SYNC, &hr); + succeeded = hr == S_OK; + } else { + succeeded = (this->*aTest)(); + } + mFailed |= !succeeded; + return succeeded; +} + +NS_IMETHODIMP +TestApp::OnStateChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + PRUint32 aStateFlags, + nsresult aStatus) +{ + NS_ASSERTION(aStateFlags & nsIWebProgressListener::STATE_IS_WINDOW && + aStateFlags & nsIWebProgressListener::STATE_STOP, "wrong state"); + if (NS_SUCCEEDED(Init())) { + if (RunTest(&TestApp::TestFocus, PR_FALSE)) + passed("TestFocus"); + + mCurrentNode = mInput; + mInput->Focus(); + if (mImpl->mStore) { + if (RunTest(&TestApp::TestClustering)) + passed("TestClustering"); + } else { + fail("no text store (clustering)"); + mFailed = PR_TRUE; + } + + printf("Testing TSF support in text input element...\n"); + mCurrentNode = mInput; + mTestString = NS_LITERAL_STRING( + "This is a test of the Text Services Framework implementation."); + mInput->SetValue(mTestString); + mInput->Focus(); + if (mImpl->mStore) { + if (RunTest(&TestApp::TestSelection)) + passed("TestSelection (input)"); + if (RunTest(&TestApp::TestText)) + passed("TestText (input)"); + if (RunTest(&TestApp::TestExtents)) + passed("TestExtents (input)"); + if (RunTest(&TestApp::TestComposition)) + passed("TestComposition (input)"); + if (RunTest(&TestApp::TestNotification, PR_FALSE)) + passed("TestNotification (input)"); + } else { + fail("no text store (input)"); + mFailed = PR_TRUE; + } + + printf("Testing TSF support in textarea element...\n"); + mCurrentNode = mTextArea; + mTestString = NS_LITERAL_STRING( + "This is a test of the\r\nText Services Framework\r\nimplementation."); + mTextArea->SetValue(mTestString); + mTextArea->Focus(); + if (mImpl->mStore) { + if (RunTest(&TestApp::TestSelection)) + passed("TestSelection (textarea)"); + if (RunTest(&TestApp::TestText)) + passed("TestText (textarea)"); + if (RunTest(&TestApp::TestExtents)) + passed("TestExtents (textarea)"); + if (RunTest(&TestApp::TestComposition)) + passed("TestComposition (textarea)"); + if (RunTest(&TestApp::TestNotification, PR_FALSE)) + passed("TestNotification (textarea)"); + } else { + fail("no text store (textarea)"); + mFailed = PR_TRUE; + } + } else { + fail("initialization"); + mFailed = PR_TRUE; + } + Term(); + return NS_OK; +} + +PRBool +TestApp::TestFocus(void) +{ + PRUint32 focus = mImpl->mFocusCount, blur = mImpl->mBlurCount; + nsresult rv; + + /* If these fail the cause is probably one or more of: + * - nsIMEStateManager::OnTextStateFocus not called by nsEventStateManager + * - nsIMEStateManager::OnTextStateBlur not called by nsEventStateManager + * - nsWindow::OnIMEFocusChange (nsIWidget) not called by nsIMEStateManager + * - nsTextStore::Create/Focus/Destroy not called by nsWindow + * - ITfThreadMgr::CreateDocumentMgr/SetFocus not called by nsTextStore + * - ITfDocumentMgr::CreateContext/Push not called by nsTextStore + */ + + rv = mInput->Focus(); + if (!(NS_SUCCEEDED(rv) && + mImpl->mFocused && + mImpl->mStore && + mImpl->mFocusCount - focus == 1 && + mImpl->mBlurCount - blur == 0 && + mImpl->mStore)) { + fail("TestFocus: document focus was not set"); + return PR_FALSE; + } + + rv = mTextArea->Focus(); + if (!(NS_SUCCEEDED(rv) && + mImpl->mFocused && + mImpl->mStore && + mImpl->mFocusCount - focus == 2 && + mImpl->mBlurCount - blur == 1 && + mImpl->mStore)) { + fail("TestFocus: document focus was not changed"); + return PR_FALSE; + } + + rv = mButton->Focus(); + if (!(NS_SUCCEEDED(rv) && + !mImpl->mFocused && + !mImpl->mStore && + mImpl->mFocusCount - focus == 2 && + mImpl->mBlurCount - blur == 2 && + !mImpl->mStore)) { + fail("TestFocus: document was not blurred"); + return PR_FALSE; + } + return PR_TRUE; +} + +PRBool +TestApp::TestClustering(void) +{ + // Text for testing + const PRUint32 STRING_LENGTH = 2; + PRUnichar string[3]; + string[0] = 'e'; + string[1] = 0x0301; // U+0301 'acute accent' + string[2] = nsnull; + + // Replace entire string with our string + TS_TEXTCHANGE textChange; + HRESULT hr = mImpl->mStore->SetText(0, 0, -1, string, STRING_LENGTH, + &textChange); + if (!(SUCCEEDED(hr) && + 0 == textChange.acpStart && + STRING_LENGTH == textChange.acpNewEnd)) { + fail("TestClustering: SetText"); + return PR_FALSE; + } + + TsViewCookie view; + RECT rectLetter, rectAccent, rectWhole, rectCombined; + BOOL clipped, nonEmpty; + + hr = mImpl->mStore->GetActiveView(&view); + if (!(SUCCEEDED(hr))) { + fail("TestClustering: GetActiveView"); + return PR_FALSE; + } + + // Get rect of first char (the letter) + hr = mImpl->mStore->GetTextExt(view, 0, STRING_LENGTH / 2, + &rectLetter, &clipped); + if (!(SUCCEEDED(hr))) { + fail("TestClustering: GetTextExt (letter)"); + return PR_FALSE; + } + + // Get rect of second char (the accent) + hr = mImpl->mStore->GetTextExt(view, STRING_LENGTH / 2, STRING_LENGTH, + &rectAccent, &clipped); + if (!(SUCCEEDED(hr))) { + fail("TestClustering: GetTextExt (accent)"); + return PR_FALSE; + } + + // Get rect of combined char + hr = mImpl->mStore->GetTextExt(view, 0, STRING_LENGTH, + &rectWhole, &clipped); + if (!(SUCCEEDED(hr))) { + fail("TestClustering: GetTextExt (whole)"); + return PR_FALSE; + } + + nonEmpty = ::UnionRect(&rectCombined, &rectLetter, &rectAccent); + if (!(nonEmpty && + ::EqualRect(&rectCombined, &rectWhole))) { + fail("TestClustering: unexpected combined rect"); + return PR_FALSE; + } + return PR_TRUE; +} + +PRBool +TestApp::TestSelectionInternal(char* aTestName, + LONG aStart, + LONG aEnd, + TsActiveSelEnd aSelEnd) +{ + PRBool succeeded = PR_TRUE, continueTest = PR_TRUE; + TS_SELECTION_ACP sel, testSel; + ULONG selFetched; + + sel.acpStart = aStart; + sel.acpEnd = aEnd; + sel.style.ase = aSelEnd; + sel.style.fInterimChar = FALSE; + HRESULT hr = mImpl->mStore->SetSelection(1, &sel); + if (!(SUCCEEDED(hr))) { + fail("TestSelection: SetSelection (%s)", aTestName); + continueTest = succeeded = PR_FALSE; + } + + if (continueTest) { + hr = mImpl->mStore->GetSelection(TS_DEFAULT_SELECTION, 1, + &testSel, &selFetched); + if (!(SUCCEEDED(hr) && + selFetched == 1 && + !memcmp(&sel, &testSel, sizeof(sel)))) { + fail("TestSelection: unexpected GetSelection result (%s)", aTestName); + succeeded = PR_FALSE; + } + } + return succeeded; +} + +PRBool +TestApp::TestSelection(void) +{ + PRBool succeeded = PR_TRUE; + + /* If these fail the cause is probably one or more of: + * nsTextStore::GetSelection not sending NS_QUERY_SELECTED_TEXT + * NS_QUERY_SELECTED_TEXT not handled by nsContentEventHandler + * Bug in NS_QUERY_SELECTED_TEXT handler + * nsTextStore::SetSelection not sending NS_SELECTION_SET + * NS_SELECTION_SET not handled by nsContentEventHandler + * Bug in NS_SELECTION_SET handler + */ + + TS_SELECTION_ACP testSel; + ULONG selFetched; + + HRESULT hr = mImpl->mStore->GetSelection(0, 1, &testSel, &selFetched); + if (!(SUCCEEDED(hr) && + selFetched == 1)) { + fail("TestSelection: GetSelection"); + succeeded = PR_FALSE; + } + + const LONG SELECTION1_START = 0; + const LONG SELECTION1_END = mTestString.Length(); + const TsActiveSelEnd SELECTION1_SELEND = TS_AE_END; + + if (!TestSelectionInternal("normal", + SELECTION1_START, + SELECTION1_END, + SELECTION1_SELEND)) { + succeeded = PR_FALSE; + } + + const LONG SELECTION2_START = mTestString.Length() / 2; + const LONG SELECTION2_END = SELECTION2_START; + const TsActiveSelEnd SELECTION2_SELEND = TS_AE_END; + + if (!TestSelectionInternal("collapsed", + SELECTION2_START, + SELECTION2_END, + SELECTION2_SELEND)) { + succeeded = PR_FALSE; + } + + const LONG SELECTION3_START = 12; + const LONG SELECTION3_END = mTestString.Length() - 20; + const TsActiveSelEnd SELECTION3_SELEND = TS_AE_START; + + if (!TestSelectionInternal("reversed", + SELECTION3_START, + SELECTION3_END, + SELECTION3_SELEND)) { + succeeded = PR_FALSE; + } + return succeeded; +} + +PRBool +TestApp::TestText(void) +{ + const PRUint32 BUFFER_SIZE = (0x100); + const PRUint32 RUNINFO_SIZE = (0x10); + + PRBool succeeded = PR_TRUE, continueTest; + PRUnichar buffer[BUFFER_SIZE]; + TS_RUNINFO runInfo[RUNINFO_SIZE]; + ULONG bufferRet, runInfoRet; + LONG acpRet, acpCurrent; + TS_TEXTCHANGE textChange; + HRESULT hr; + + /* If these fail the cause is probably one or more of: + * nsTextStore::GetText not sending NS_QUERY_TEXT_CONTENT + * NS_QUERY_TEXT_CONTENT not handled by nsContentEventHandler + * Bug in NS_QUERY_TEXT_CONTENT handler + * nsTextStore::SetText not calling SetSelection or InsertTextAtSelection + * Bug in SetSelection or InsertTextAtSelection + * NS_SELECTION_SET bug or NS_COMPOSITION_* / NS_TEXT_TEXT bug + */ + + // Get all text + hr = mImpl->mStore->GetText(0, -1, buffer, BUFFER_SIZE, &bufferRet, + runInfo, RUNINFO_SIZE, &runInfoRet, &acpRet); + if (!(SUCCEEDED(hr) && + bufferRet <= mTestString.Length() && + !wcsncmp(mTestString.get(), buffer, bufferRet) && + acpRet == LONG(bufferRet) && + runInfoRet > 0)) { + fail("TestText: GetText 1"); + succeeded = PR_FALSE; + } + + + // Get text from GETTEXT2_START to GETTEXT2_END + const PRUint32 GETTEXT2_START = (18); + const PRUint32 GETTEXT2_END = (mTestString.Length() - 16); + const PRUint32 GETTEXT2_BUFFER_SIZE = (0x10); + + hr = mImpl->mStore->GetText(GETTEXT2_START, GETTEXT2_END, + buffer, GETTEXT2_BUFFER_SIZE, &bufferRet, + runInfo, RUNINFO_SIZE, &runInfoRet, &acpRet); + if (!(SUCCEEDED(hr) && + bufferRet <= GETTEXT2_BUFFER_SIZE && + !wcsncmp(mTestString.get() + GETTEXT2_START, buffer, bufferRet) && + acpRet == LONG(bufferRet) + GETTEXT2_START && + runInfoRet > 0)) { + fail("TestText: GetText 2"); + succeeded = PR_FALSE; + } + + + // Replace text from SETTEXT1_START to SETTEXT1_END with insertString + const PRUint32 SETTEXT1_START = (8); + const PRUint32 SETTEXT1_TAIL_LENGTH = (40); + const PRUint32 SETTEXT1_END = (mTestString.Length() - + SETTEXT1_TAIL_LENGTH); + NS_NAMED_LITERAL_STRING(insertString, "(Inserted string)"); + + continueTest = PR_TRUE; + hr = mImpl->mStore->SetText(0, SETTEXT1_START, SETTEXT1_END, + insertString.get(), insertString.Length(), &textChange); + if (!(SUCCEEDED(hr) && + textChange.acpStart == SETTEXT1_START && + textChange.acpOldEnd == LONG(SETTEXT1_END) && + textChange.acpNewEnd == LONG(SETTEXT1_START + + insertString.Length()))) { + fail("TestText: SetText 1"); + continueTest = succeeded = PR_FALSE; + } + + const PRUint32 SETTEXT1_FINAL_LENGTH = (SETTEXT1_START + + SETTEXT1_TAIL_LENGTH + + insertString.Length()); + + if (continueTest) { + acpCurrent = 0; + while (acpCurrent < LONG(SETTEXT1_FINAL_LENGTH)) { + hr = mImpl->mStore->GetText(acpCurrent, -1, &buffer[acpCurrent], + BUFFER_SIZE, &bufferRet, runInfo, + RUNINFO_SIZE, &runInfoRet, &acpRet); + if (!(SUCCEEDED(hr) && + acpRet > acpCurrent && + bufferRet <= SETTEXT1_FINAL_LENGTH && + runInfoRet > 0)) { + fail("TestText: GetText failed after SetTest 1"); + continueTest = succeeded = PR_FALSE; + break; + } + acpCurrent = acpRet; + } + } + + if (continueTest) { + if (!(acpCurrent == LONG(SETTEXT1_FINAL_LENGTH) && + !wcsncmp(buffer, mTestString.get(), SETTEXT1_START) && + !wcsncmp(&buffer[SETTEXT1_START], insertString.get(), + insertString.Length()) && + !wcsncmp(&buffer[SETTEXT1_START + insertString.Length()], + mTestString.get() + SETTEXT1_END, SETTEXT1_TAIL_LENGTH))) { + fail("TestText: unexpected GetText result after SetText 1"); + succeeded = PR_FALSE; + } + } + + + // Restore entire text to original text (mTestString) + continueTest = PR_TRUE; + hr = mImpl->mStore->SetText(0, 0, -1, mTestString.get(), + mTestString.Length(), &textChange); + if (!(SUCCEEDED(hr) && + textChange.acpStart == 0 && + textChange.acpOldEnd == LONG(SETTEXT1_FINAL_LENGTH) && + textChange.acpNewEnd == LONG(mTestString.Length()))) { + fail("TestText: SetText 2"); + continueTest = succeeded = PR_FALSE; + } + + if (continueTest) { + acpCurrent = 0; + while (acpCurrent < LONG(mTestString.Length())) { + hr = mImpl->mStore->GetText(acpCurrent, -1, &buffer[acpCurrent], + BUFFER_SIZE, &bufferRet, runInfo, RUNINFO_SIZE, &runInfoRet, &acpRet); + if (!(SUCCEEDED(hr) && + acpRet > acpCurrent && + bufferRet <= mTestString.Length() && + runInfoRet > 0)) { + fail("TestText: GetText failed after SetText 2"); + continueTest = succeeded = PR_FALSE; + break; + } + acpCurrent = acpRet; + } + } + + if (continueTest) { + if (!(acpCurrent == LONG(mTestString.Length()) && + !wcsncmp(buffer, mTestString.get(), mTestString.Length()))) { + fail("TestText: unexpected GetText result after SetText 2"); + succeeded = PR_FALSE; + } + } + return succeeded; +} + +PRBool +TestApp::TestExtents(void) +{ + TS_SELECTION_ACP sel; + sel.acpStart = 0; + sel.acpEnd = 0; + sel.style.ase = TS_AE_END; + sel.style.fInterimChar = FALSE; + mImpl->mStore->SetSelection(1, &sel); + + nsCOMPtr selCon; + if (!(NS_SUCCEEDED(GetSelCon(getter_AddRefs(selCon))) && selCon)) { + fail("TestExtents: get nsISelectionController"); + return PR_FALSE; + } + selCon->ScrollSelectionIntoView(nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, PR_TRUE); + + nsCOMPtr window(do_GetInterface(mWindow)); + if (!window) { + fail("TestExtents: get nsIDOMWindowInternal"); + return PR_FALSE; + } + RECT windowRect, screenRect, textRect1, textRect2; + BOOL clipped; + PRInt32 val; + TsViewCookie view; + HRESULT hr; + + nsresult nsr = window->GetScreenX(&val); + windowRect.left = val; + nsr |= window->GetScreenY(&val); + windowRect.top = val; + nsr |= window->GetOuterWidth(&val); + windowRect.right = windowRect.left + val; + nsr |= window->GetOuterHeight(&val); + windowRect.bottom = windowRect.top + val; + if (!(NS_SUCCEEDED(nsr))) { + fail("TestExtents: get window rect failed"); + return PR_FALSE; + } + + hr = mImpl->mStore->GetActiveView(&view); + if (!(SUCCEEDED(hr))) { + fail("TestExtents: GetActiveView"); + return PR_FALSE; + } + + PRBool succeeded = PR_TRUE; + HWND hwnd; + hr = mImpl->mStore->GetWnd(view, &hwnd); + if (!(SUCCEEDED(hr) && + ::IsWindow(hwnd))) { + fail("TestExtents: GetWnd"); + succeeded = PR_FALSE; + } + + ::SetRectEmpty(&screenRect); + hr = mImpl->mStore->GetScreenExt(view, &screenRect); + if (!(SUCCEEDED(hr) && + screenRect.left > windowRect.left && + screenRect.top > windowRect.top && + screenRect.right > screenRect.left && + screenRect.bottom > screenRect.top && + screenRect.right < windowRect.right && + screenRect.bottom < windowRect.bottom)) { + fail("TestExtents: GetScreenExt"); + succeeded = PR_FALSE; + } + + const LONG GETTEXTEXT1_START = 0; + const LONG GETTEXTEXT1_END = 0; + + ::SetRectEmpty(&textRect1); + hr = mImpl->mStore->GetTextExt(view, GETTEXTEXT1_START, GETTEXTEXT1_END, + &textRect1, &clipped); + if (!(SUCCEEDED(hr) && + textRect1.left >= screenRect.left && + textRect1.top >= screenRect.top && + textRect1.right < screenRect.right && + textRect1.bottom <= screenRect.bottom && + textRect1.right >= textRect1.left && + textRect1.bottom > textRect1.top)) { + fail("TestExtents: GetTextExt (offset %ld to %ld)", + GETTEXTEXT1_START, GETTEXTEXT1_END); + succeeded = PR_FALSE; + } + + const LONG GETTEXTEXT2_START = 10; + const LONG GETTEXTEXT2_END = 25; + + ::SetRectEmpty(&textRect2); + hr = mImpl->mStore->GetTextExt(view, GETTEXTEXT2_START, GETTEXTEXT2_END, + &textRect2, &clipped); + if (!(SUCCEEDED(hr) && + textRect2.left >= screenRect.left && + textRect2.top >= screenRect.top && + textRect2.right <= screenRect.right && + textRect2.bottom <= screenRect.bottom && + textRect2.right > textRect2.left && + textRect2.bottom > textRect2.top)) { + fail("TestExtents: GetTextExt (offset %ld to %ld)", + GETTEXTEXT2_START, GETTEXTEXT2_END); + succeeded = PR_FALSE; + } + + // Offsets must be between GETTEXTEXT2_START and GETTEXTEXT2_END + const LONG GETTEXTEXT3_START = 23; + const LONG GETTEXTEXT3_END = 23; + + ::SetRectEmpty(&textRect1); + hr = mImpl->mStore->GetTextExt(view, GETTEXTEXT3_START, GETTEXTEXT3_END, + &textRect1, &clipped); + // Rectangle must be entirely inside the previous rectangle, + // since GETTEXTEXT3_START and GETTEXTEXT3_END are between + // GETTEXTEXT2_START and GETTEXTEXT2_START + if (!(SUCCEEDED(hr) && ::IsRectEmpty(&textRect1) || + (textRect1.left >= textRect2.left && + textRect1.top >= textRect2.top && + textRect1.right <= textRect2.right && + textRect1.bottom <= textRect2.bottom && + textRect1.right >= textRect1.left && + textRect1.bottom > textRect1.top))) { + fail("TestExtents: GetTextExt (offset %ld to %ld)", + GETTEXTEXT3_START, GETTEXTEXT3_END); + succeeded = PR_FALSE; + } + return succeeded; +} + +PRBool +TestApp::TestCompositionSelectionAndText(char* aTestName, + LONG aExpectedSelStart, + LONG aExpectedSelEnd, + nsString& aReferenceString) +{ + TS_SELECTION_ACP currentSel; + ULONG selFetched = 0; + HRESULT hr = mImpl->mStore->GetSelection(TF_DEFAULT_SELECTION, 1, + ¤tSel, &selFetched); + if (!(SUCCEEDED(hr) && + 1 == selFetched && + currentSel.acpStart == aExpectedSelStart && + currentSel.acpEnd == aExpectedSelEnd)) { + fail("TestComposition: GetSelection (%s)", aTestName); + return PR_FALSE; + } + + const PRUint32 bufferSize = 0x100, runInfoSize = 0x10; + PRUnichar buffer[bufferSize]; + TS_RUNINFO runInfo[runInfoSize]; + ULONG bufferRet, runInfoRet; + LONG acpRet, acpCurrent = 0; + while (acpCurrent < LONG(aReferenceString.Length())) { + hr = mImpl->mStore->GetText(acpCurrent, aReferenceString.Length(), + &buffer[acpCurrent], bufferSize, &bufferRet, runInfo, runInfoSize, + &runInfoRet, &acpRet); + if (!(SUCCEEDED(hr) && + acpRet > acpCurrent && + bufferRet <= aReferenceString.Length() && + runInfoRet > 0)) { + fail("TestComposition: GetText (%s)", aTestName); + return PR_FALSE; + } + acpCurrent = acpRet; + } + if (!(acpCurrent == aReferenceString.Length() && + !wcsncmp(buffer, aReferenceString.get(), aReferenceString.Length()))) { + fail("TestComposition: unexpected GetText result (%s)", aTestName); + return PR_FALSE; + } + return PR_TRUE; +} + +PRBool +TestApp::TestComposition(void) +{ + nsRefPtr sink; + HRESULT hr = mImpl->mStore->QueryInterface( + IID_ITfContextOwnerCompositionSink, + getter_AddRefs(sink)); + if (!(SUCCEEDED(hr))) { + fail("TestComposition: QueryInterface"); + return PR_FALSE; + } + + const LONG PRECOMPOSITION_SEL_START = 2; + const LONG PRECOMPOSITION_SEL_END = PRECOMPOSITION_SEL_START; + const TsActiveSelEnd PRECOMPOSITION_SEL_SELEND = TS_AE_END; + + TS_SELECTION_ACP sel; + sel.acpStart = PRECOMPOSITION_SEL_START; + sel.acpEnd = PRECOMPOSITION_SEL_END; + sel.style.ase = PRECOMPOSITION_SEL_SELEND; + sel.style.fInterimChar = FALSE; + hr = mImpl->mStore->SetSelection(1, &sel); + if (!(SUCCEEDED(hr))) { + fail("TestComposition: SetSelection (pre-composition)"); + return PR_FALSE; + } + + + TS_TEXTCHANGE textChange; + NS_NAMED_LITERAL_STRING(insertString1, "Compo1"); + hr = mImpl->mStore->InsertTextAtSelection(TF_IAS_NOQUERY, + insertString1.get(), + insertString1.Length(), + NULL, NULL, &textChange); + if (!(SUCCEEDED(hr) && + sel.acpEnd == textChange.acpStart && + sel.acpEnd == textChange.acpOldEnd && + sel.acpEnd + insertString1.Length() == textChange.acpNewEnd)) { + fail("TestComposition: InsertTextAtSelection"); + return PR_FALSE; + } + sel.acpEnd = textChange.acpNewEnd; + + mImpl->mRangeStart = textChange.acpStart; + mImpl->mRangeLength = textChange.acpNewEnd - textChange.acpOldEnd; + BOOL okay = FALSE; + hr = sink->OnStartComposition(mImpl, &okay); + if (!(SUCCEEDED(hr) && + okay)) { + fail("TestComposition: OnStartComposition"); + return PR_FALSE; + } + + + NS_NAMED_LITERAL_STRING(insertString2, "Composition2"); + hr = mImpl->mStore->SetText(0, mImpl->mRangeStart + mImpl->mRangeLength, + mImpl->mRangeStart + mImpl->mRangeLength, + insertString2.get(), insertString2.Length(), + &textChange); + if (!(SUCCEEDED(hr) && + sel.acpEnd == textChange.acpStart && + sel.acpEnd == textChange.acpOldEnd && + sel.acpEnd + insertString2.Length() == textChange.acpNewEnd)) { + fail("TestComposition: SetText 1"); + return PR_FALSE; + } + sel.acpEnd = textChange.acpNewEnd; + mImpl->mRangeLength += textChange.acpNewEnd - textChange.acpOldEnd; + + + const LONG COMPOSITION3_TEXT_START_OFFSET = -8; // offset 8 from the end + const LONG COMPOSITION3_TEXT_END_OFFSET = 4; + + const LONG COMPOSITION3_TEXT_START = mImpl->mRangeStart + + mImpl->mRangeLength + + COMPOSITION3_TEXT_START_OFFSET; + const LONG COMPOSITION3_TEXT_END = COMPOSITION3_TEXT_START + + COMPOSITION3_TEXT_END_OFFSET; + + NS_NAMED_LITERAL_STRING(insertString3, "Compo3"); + hr = mImpl->mStore->SetText(0, COMPOSITION3_TEXT_START, + COMPOSITION3_TEXT_END, + insertString3.get(), insertString3.Length(), + &textChange); + if (!(SUCCEEDED(hr) && + sel.acpEnd + COMPOSITION3_TEXT_START_OFFSET == textChange.acpStart && + sel.acpEnd + COMPOSITION3_TEXT_START_OFFSET + + COMPOSITION3_TEXT_END_OFFSET == textChange.acpOldEnd && + sel.acpEnd + insertString3.Length() + COMPOSITION3_TEXT_START_OFFSET == + textChange.acpNewEnd)) { + fail("TestComposition: SetText 2"); + return PR_FALSE; + } + sel.acpEnd = textChange.acpNewEnd; + mImpl->mRangeLength += textChange.acpNewEnd - textChange.acpOldEnd; + + + nsString referenceString; + referenceString.Append(mTestString.get(), sel.acpStart); + referenceString.Append(insertString1); + referenceString.Append(insertString2.get(), + insertString2.Length() + COMPOSITION3_TEXT_START_OFFSET); + referenceString.Append(insertString3); + referenceString.Append(insertString2.get() + insertString2.Length() - + COMPOSITION3_TEXT_END_OFFSET, COMPOSITION3_TEXT_END_OFFSET); + referenceString.Append(mTestString.get() + sel.acpStart, + COMPOSITION3_TEXT_END_OFFSET); + + if (!TestCompositionSelectionAndText("composition", + sel.acpEnd, sel.acpEnd, + referenceString)) + return PR_FALSE; + + + const LONG POSTCOMPOSITION_SEL_START = sel.acpEnd - 8; + const LONG POSTCOMPOSITION_SEL_END = POSTCOMPOSITION_SEL_START + 2; + + sel.acpStart = POSTCOMPOSITION_SEL_START; + sel.acpEnd = POSTCOMPOSITION_SEL_END; + hr = mImpl->mStore->SetSelection(1, &sel); + if (!(SUCCEEDED(hr))) { + fail("TestComposition: SetSelection (composition)"); + return PR_FALSE; + } + + hr = sink->OnEndComposition(mImpl); + if (!(SUCCEEDED(hr))) { + fail("TestComposition: OnEndComposition"); + return PR_FALSE; + } + + if (!TestCompositionSelectionAndText("post-composition", + sel.acpStart, sel.acpEnd, + referenceString)) + return PR_FALSE; + + + const LONG EMPTYCOMPOSITION_START = mImpl->mRangeStart + 2; + const LONG EMPTYCOMPOSITION_LENGTH = mImpl->mRangeLength - 4; + + mImpl->mRangeStart = EMPTYCOMPOSITION_START; + mImpl->mRangeLength = EMPTYCOMPOSITION_LENGTH; + okay = FALSE; + hr = sink->OnStartComposition(mImpl, &okay); + if (!(SUCCEEDED(hr) && + okay)) { + fail("TestComposition: OnStartComposition (empty composition)"); + return PR_FALSE; + } + + hr = sink->OnEndComposition(mImpl); + if (!(SUCCEEDED(hr))) { + fail("TestComposition: OnEndComposition (empty composition)"); + return PR_FALSE; + } + + if (!TestCompositionSelectionAndText("empty composition", + mImpl->mRangeStart, + mImpl->mRangeStart + mImpl->mRangeLength, + referenceString)) + return PR_FALSE; + + return PR_TRUE; +} + +PRBool +TestApp::TestNotificationTextChange(nsIWidget* aWidget, + PRUint32 aCode, + const nsAString& aCharacter, + LONG aStart, + LONG aOldEnd, + LONG aNewEnd) +{ + MSG msg; + if (::PeekMessageW(&msg, NULL, WM_USER_TSF_TEXTCHANGE, + WM_USER_TSF_TEXTCHANGE, PM_REMOVE)) + ::DispatchMessageW(&msg); + mImpl->mTextChanged = PR_FALSE; + nsresult nsr = aWidget->SynthesizeNativeKeyEvent(0, aCode, 0, + aCharacter, aCharacter); + if (::PeekMessageW(&msg, NULL, WM_USER_TSF_TEXTCHANGE, + WM_USER_TSF_TEXTCHANGE, PM_REMOVE)) + ::DispatchMessageW(&msg); + return NS_SUCCEEDED(nsr) && + mImpl->mTextChanged && + aStart == mImpl->mTextChangeData.acpStart && + aOldEnd == mImpl->mTextChangeData.acpOldEnd && + aNewEnd == mImpl->mTextChangeData.acpNewEnd; +} + +PRBool +TestApp::TestNotification(void) +{ + nsresult nsr; + // get selection to test notification support + nsCOMPtr selCon; + if (!(NS_SUCCEEDED(GetSelCon(getter_AddRefs(selCon))) && selCon)) { + fail("TestNotification: get nsISelectionController"); + return PR_FALSE; + } + + nsr = selCon->CompleteMove(PR_FALSE, PR_FALSE); + if (!(NS_SUCCEEDED(nsr))) { + fail("TestNotification: CompleteMove"); + return PR_FALSE; + } + + mImpl->mSelChanged = PR_FALSE; + nsr = selCon->CharacterMove(PR_TRUE, PR_FALSE); + if (!(NS_SUCCEEDED(nsr) && + mImpl->mSelChanged)) { + fail("TestNotification: CharacterMove"); + return PR_FALSE; + } + + mImpl->mSelChanged = PR_FALSE; + nsr = selCon->CharacterMove(PR_TRUE, PR_TRUE); + if (!(NS_SUCCEEDED(nsr) && + mImpl->mSelChanged)) { + fail("TestNotification: CharacterMove (extend)"); + return PR_FALSE; + } + + nsCOMPtr widget; + nsCOMPtr docShell; + nsr = mWindow->GetDocShell(getter_AddRefs(docShell)); + if (NS_SUCCEEDED(nsr) && docShell) { + nsCOMPtr presShell; + nsr = docShell->GetPresShell(getter_AddRefs(presShell)); + if (NS_SUCCEEDED(nsr) && presShell) { + nsCOMPtr viewManager = presShell->GetViewManager(); + if (viewManager) { + nsr = viewManager->GetWidget(getter_AddRefs(widget)); + } + } + } + if (!(NS_SUCCEEDED(nsr) && widget)) { + fail("TestNotification: get nsIWidget"); + return PR_FALSE; + } + + NS_NAMED_LITERAL_STRING(character, ""); + NS_NAMED_LITERAL_STRING(characterA, "A"); + + // The selection test code above placed the selection at offset 1 to 2 + const LONG TEXTCHANGE1_START = 1; + const LONG TEXTCHANGE1_OLDEND = 2; + const LONG TEXTCHANGE1_NEWEND = 2; + + // replace single selected character with 'A' + if (!TestNotificationTextChange(widget, 'A', characterA, + TEXTCHANGE1_START, TEXTCHANGE1_OLDEND, TEXTCHANGE1_NEWEND)) { + fail("TestNotification: text change 1"); + return PR_FALSE; + } + + const LONG TEXTCHANGE2_START = TEXTCHANGE1_NEWEND; + const LONG TEXTCHANGE2_OLDEND = TEXTCHANGE1_NEWEND; + const LONG TEXTCHANGE2_NEWEND = TEXTCHANGE1_NEWEND + 1; + + // insert 'A' + if (!TestNotificationTextChange(widget, 'A', characterA, + TEXTCHANGE2_START, TEXTCHANGE2_OLDEND, TEXTCHANGE2_NEWEND)) { + fail("TestNotification: text change 2"); + return PR_FALSE; + } + + const LONG TEXTCHANGE3_START = TEXTCHANGE2_NEWEND - 1; + const LONG TEXTCHANGE3_OLDEND = TEXTCHANGE2_NEWEND; + const LONG TEXTCHANGE3_NEWEND = TEXTCHANGE2_NEWEND - 1; + + // backspace + if (!TestNotificationTextChange(widget, '\b', character, + TEXTCHANGE3_START, TEXTCHANGE3_OLDEND, TEXTCHANGE3_NEWEND)) { + fail("TestNotification: text change 3"); + return PR_FALSE; + } + return PR_TRUE; +} + +nsresult +TestApp::GetSelCon(nsISelectionController** aSelCon) +{ + nsCOMPtr docShell; + nsresult nsr = mWindow->GetDocShell(getter_AddRefs(docShell)); + if (NS_SUCCEEDED(nsr) && docShell) { + nsCOMPtr presShell; + nsr = docShell->GetPresShell(getter_AddRefs(presShell)); + if (NS_SUCCEEDED(nsr) && presShell) { + nsIFrame* frame = presShell->GetPrimaryFrameFor( + nsCOMPtr(do_QueryInterface(mCurrentNode))); + if (frame) { + nsPresContext* presContext = presShell->GetPresContext(); + if (presContext) { + nsr = frame->GetSelectionController(presContext, aSelCon); + } + } + } + } + return nsr; +} + +int main(int argc, char** argv) +{ + ScopedXPCOM xpcom("TestWinTSF (bug #88831)"); + if (xpcom.failed()) + return 1; + + nsRefPtr tests = new TestApp(); + if (!tests) + return 1; + + if (NS_FAILED(tests->Run())) { + fail("run failed"); + return 1; + } + return int(tests->CheckFailed()); +}