1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "CompositionTransaction.h"
8 #include "mozilla/EditorBase.h" // mEditorBase
9 #include "mozilla/SelectionState.h" // RangeUpdater
10 #include "mozilla/TextComposition.h" // TextComposition
11 #include "mozilla/dom/Selection.h" // local var
12 #include "mozilla/dom/Text.h" // mTextNode
13 #include "nsAString.h" // params
14 #include "nsDebug.h" // for NS_ASSERTION, etc
15 #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
16 #include "nsRange.h" // local var
17 #include "nsISelectionController.h" // for nsISelectionController constants
18 #include "nsQueryObject.h" // for do_QueryObject
25 already_AddRefed
<CompositionTransaction
> CompositionTransaction::Create(
26 EditorBase
& aEditorBase
, const nsAString
& aStringToInsert
,
27 const EditorDOMPointInText
& aPointToInsert
) {
28 MOZ_ASSERT(aPointToInsert
.IsSetAndValid());
30 TextComposition
* composition
= aEditorBase
.GetComposition();
31 MOZ_RELEASE_ASSERT(composition
);
32 // XXX Actually, we get different text node and offset from editor in some
33 // cases. If composition stores text node, we should use it and offset
35 EditorDOMPointInText pointToInsert
;
36 if (Text
* textNode
= composition
->GetContainerTextNode()) {
37 pointToInsert
.Set(textNode
, composition
->XPOffsetInTextNode());
39 pointToInsert
.GetContainerAsText() ==
40 composition
->GetContainerTextNode(),
41 "The editor tries to insert composition string into different node");
43 pointToInsert
.Offset() == composition
->XPOffsetInTextNode(),
44 "The editor tries to insert composition string into different offset");
46 pointToInsert
= aPointToInsert
;
48 RefPtr
<CompositionTransaction
> transaction
=
49 new CompositionTransaction(aEditorBase
, aStringToInsert
, pointToInsert
);
50 // XXX Now, it might be better to modify the text node information of
51 // the TextComposition instance in DoTransaction() because updating
52 // the information before changing actual DOM tree is pretty odd.
53 composition
->OnCreateCompositionTransaction(
54 aStringToInsert
, pointToInsert
.ContainerAsText(), pointToInsert
.Offset());
55 return transaction
.forget();
58 CompositionTransaction::CompositionTransaction(
59 EditorBase
& aEditorBase
, const nsAString
& aStringToInsert
,
60 const EditorDOMPointInText
& aPointToInsert
)
61 : mTextNode(aPointToInsert
.ContainerAsText()),
62 mOffset(aPointToInsert
.Offset()),
63 mReplaceLength(aEditorBase
.GetComposition()->XPLengthInTextNode()),
64 mRanges(aEditorBase
.GetComposition()->GetRanges()),
65 mStringToInsert(aStringToInsert
),
66 mEditorBase(&aEditorBase
),
68 MOZ_ASSERT(mTextNode
->TextLength() >= mOffset
);
71 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction
, EditTransactionBase
,
72 mEditorBase
, mTextNode
)
73 // mRangeList can't lead to cycles
75 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction
)
76 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase
)
77 NS_IMPL_ADDREF_INHERITED(CompositionTransaction
, EditTransactionBase
)
78 NS_IMPL_RELEASE_INHERITED(CompositionTransaction
, EditTransactionBase
)
80 NS_IMETHODIMP
CompositionTransaction::DoTransaction() {
81 if (NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
)) {
82 return NS_ERROR_NOT_AVAILABLE
;
85 // Fail before making any changes if there's no selection controller
86 if (NS_WARN_IF(!mEditorBase
->GetSelectionController())) {
87 return NS_ERROR_NOT_AVAILABLE
;
90 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
91 OwningNonNull
<Text
> textNode
= *mTextNode
;
93 // Advance caret: This requires the presentation shell to get the selection.
94 if (mReplaceLength
== 0) {
96 editorBase
->DoInsertText(textNode
, mOffset
, mStringToInsert
, error
);
98 NS_WARNING("EditorBase::DoInsertText() failed");
99 return error
.StealNSResult();
101 editorBase
->RangeUpdaterRef().SelAdjInsertText(textNode
, mOffset
,
102 mStringToInsert
.Length());
104 // If composition string is split to multiple text nodes, we should put
105 // whole new composition string to the first text node and remove the
106 // compostion string in other nodes.
107 uint32_t replaceableLength
= textNode
->TextLength() - mOffset
;
109 editorBase
->DoReplaceText(textNode
, mOffset
, mReplaceLength
,
110 mStringToInsert
, error
);
111 if (error
.Failed()) {
112 NS_WARNING("EditorBase::DoReplaceText() failed");
113 return error
.StealNSResult();
116 // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
117 // this transaction will remove whole composition string. Therefore,
118 // selection should be restored at start of composition string.
119 // XXX Perhaps, this is a bug of our selection managemnt at undoing.
120 editorBase
->RangeUpdaterRef().SelAdjDeleteText(textNode
, mOffset
,
122 // But some ranges which after the composition string should be restored
124 editorBase
->RangeUpdaterRef().SelAdjInsertText(textNode
, mOffset
,
125 mStringToInsert
.Length());
127 if (replaceableLength
< mReplaceLength
) {
128 // XXX Perhaps, scanning following sibling text nodes with composition
129 // string length which we know is wrong because there may be
130 // non-empty text nodes which are inserted by JS. Instead, we
131 // should remove all text in the ranges of IME selections.
132 int32_t remainLength
= mReplaceLength
- replaceableLength
;
133 IgnoredErrorResult ignoredError
;
134 for (nsIContent
* nextSibling
= textNode
->GetNextSibling();
135 nextSibling
&& nextSibling
->IsText() && remainLength
;
136 nextSibling
= nextSibling
->GetNextSibling()) {
137 OwningNonNull
<Text
> followingTextNode
=
138 *static_cast<Text
*>(nextSibling
);
139 uint32_t textLength
= followingTextNode
->TextLength();
140 editorBase
->DoDeleteText(followingTextNode
, 0, remainLength
,
142 NS_WARNING_ASSERTION(!ignoredError
.Failed(),
143 "EditorBase::DoDeleteText() failed, but ignored");
144 ignoredError
.SuppressException();
145 // XXX Needs to check whether the text is deleted as expected.
146 editorBase
->RangeUpdaterRef().SelAdjDeleteText(followingTextNode
, 0,
148 remainLength
-= textLength
;
153 nsresult rv
= SetSelectionForRanges();
154 NS_WARNING_ASSERTION(
156 "CompositionTransaction::SetSelectionForRanges() failed");
160 NS_IMETHODIMP
CompositionTransaction::UndoTransaction() {
161 if (NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
)) {
162 return NS_ERROR_NOT_AVAILABLE
;
165 // Get the selection first so we'll fail before making any changes if we
167 RefPtr
<Selection
> selection
= mEditorBase
->GetSelection();
168 if (NS_WARN_IF(!selection
)) {
169 return NS_ERROR_NOT_AVAILABLE
;
172 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
173 OwningNonNull
<Text
> textNode
= *mTextNode
;
175 editorBase
->DoDeleteText(textNode
, mOffset
, mStringToInsert
.Length(), error
);
176 if (error
.Failed()) {
177 NS_WARNING("EditorBase::DoDeleteText() failed");
178 return error
.StealNSResult();
181 // set the selection to the insertion point where the string was removed
182 nsresult rv
= selection
->CollapseInLimiter(textNode
, mOffset
);
183 NS_ASSERTION(NS_SUCCEEDED(rv
), "Selection::CollapseInLimiter() failed");
187 NS_IMETHODIMP
CompositionTransaction::Merge(nsITransaction
* aOtherTransaction
,
189 if (NS_WARN_IF(!aOtherTransaction
) || NS_WARN_IF(!aDidMerge
)) {
190 return NS_ERROR_INVALID_ARG
;
194 // Check to make sure we aren't fixed, if we are then nothing gets merged.
199 RefPtr
<EditTransactionBase
> otherTransactionBase
=
200 aOtherTransaction
->GetAsEditTransactionBase();
201 if (!otherTransactionBase
) {
205 // If aTransaction is another CompositionTransaction then merge it
206 CompositionTransaction
* otherCompositionTransaction
=
207 otherTransactionBase
->GetAsCompositionTransaction();
208 if (!otherCompositionTransaction
) {
212 // We merge the next IME transaction by adopting its insert string.
213 mStringToInsert
= otherCompositionTransaction
->mStringToInsert
;
214 mRanges
= otherCompositionTransaction
->mRanges
;
219 void CompositionTransaction::MarkFixed() { mFixed
= true; }
221 /* ============ private methods ================== */
223 nsresult
CompositionTransaction::SetSelectionForRanges() {
224 if (NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
)) {
225 return NS_ERROR_NOT_AVAILABLE
;
227 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
228 OwningNonNull
<Text
> textNode
= *mTextNode
;
229 RefPtr
<TextRangeArray
> ranges
= mRanges
;
230 nsresult rv
= SetIMESelection(editorBase
, textNode
, mOffset
,
231 mStringToInsert
.Length(), ranges
);
232 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
233 "CompositionTransaction::SetIMESelection() failed");
238 nsresult
CompositionTransaction::SetIMESelection(
239 EditorBase
& aEditorBase
, Text
* aTextNode
, uint32_t aOffsetInNode
,
240 uint32_t aLengthOfCompositionString
, const TextRangeArray
* aRanges
) {
241 RefPtr
<Selection
> selection
= aEditorBase
.GetSelection();
242 if (NS_WARN_IF(!selection
)) {
243 return NS_ERROR_NOT_INITIALIZED
;
246 SelectionBatcher
selectionBatcher(selection
);
248 // First, remove all selections of IME composition.
249 static const RawSelectionType kIMESelections
[] = {
250 nsISelectionController::SELECTION_IME_RAWINPUT
,
251 nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT
,
252 nsISelectionController::SELECTION_IME_CONVERTEDTEXT
,
253 nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT
};
255 nsCOMPtr
<nsISelectionController
> selectionController
=
256 aEditorBase
.GetSelectionController();
257 if (NS_WARN_IF(!selectionController
)) {
258 return NS_ERROR_NOT_INITIALIZED
;
261 IgnoredErrorResult ignoredError
;
262 for (uint32_t i
= 0; i
< ArrayLength(kIMESelections
); ++i
) {
263 RefPtr
<Selection
> selectionOfIME
=
264 selectionController
->GetSelection(kIMESelections
[i
]);
265 if (!selectionOfIME
) {
266 NS_WARNING("nsISelectionController::GetSelection() failed");
269 selectionOfIME
->RemoveAllRanges(ignoredError
);
270 NS_WARNING_ASSERTION(!ignoredError
.Failed(),
271 "Selection::RemoveAllRanges() failed, but ignored");
272 ignoredError
.SuppressException();
275 // Set caret position and selection of IME composition with TextRangeArray.
276 bool setCaret
= false;
277 uint32_t countOfRanges
= aRanges
? aRanges
->Length() : 0;
280 // Bounds-checking on debug builds
281 uint32_t maxOffset
= aTextNode
->Length();
284 // NOTE: composition string may be truncated when it's committed and
285 // maxlength attribute value doesn't allow input of all text of this
288 for (uint32_t i
= 0; i
< countOfRanges
; ++i
) {
289 const TextRange
& textRange
= aRanges
->ElementAt(i
);
291 // Caret needs special handling since its length may be 0 and if it's not
292 // specified explicitly, we need to handle it ourselves later.
293 if (textRange
.mRangeType
== TextRangeType::eCaret
) {
294 NS_ASSERTION(!setCaret
, "The ranges already has caret position");
295 NS_ASSERTION(!textRange
.Length(),
296 "EditorBase doesn't support wide caret");
297 int32_t caretOffset
= static_cast<int32_t>(
299 std::min(textRange
.mStartOffset
, aLengthOfCompositionString
));
300 MOZ_ASSERT(caretOffset
>= 0 &&
301 static_cast<uint32_t>(caretOffset
) <= maxOffset
);
302 rv
= selection
->CollapseInLimiter(aTextNode
, caretOffset
);
303 NS_WARNING_ASSERTION(
305 "Selection::CollapseInLimiter() failed, but might be ignored");
306 setCaret
= setCaret
|| NS_SUCCEEDED(rv
);
310 // If caret range is specified explicitly, we should show the caret if
312 aEditorBase
.HideCaret(false);
316 // If the clause length is 0, it should be a bug.
317 if (!textRange
.Length()) {
318 NS_WARNING("Any clauses must not be empty");
322 RefPtr
<nsRange
> clauseRange
;
323 int32_t startOffset
= static_cast<int32_t>(
325 std::min(textRange
.mStartOffset
, aLengthOfCompositionString
));
326 MOZ_ASSERT(startOffset
>= 0 &&
327 static_cast<uint32_t>(startOffset
) <= maxOffset
);
328 int32_t endOffset
= static_cast<int32_t>(
330 std::min(textRange
.mEndOffset
, aLengthOfCompositionString
));
331 MOZ_ASSERT(endOffset
>= startOffset
&&
332 static_cast<uint32_t>(endOffset
) <= maxOffset
);
333 clauseRange
= nsRange::Create(aTextNode
, startOffset
, aTextNode
, endOffset
,
336 NS_WARNING("nsRange::Create() failed, but might be ignored");
340 // Set the range of the clause to selection.
341 RefPtr
<Selection
> selectionOfIME
= selectionController
->GetSelection(
342 ToRawSelectionType(textRange
.mRangeType
));
343 if (!selectionOfIME
) {
345 "nsISelectionController::GetSelection() failed, but might be "
350 IgnoredErrorResult ignoredError
;
351 selectionOfIME
->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange
,
353 if (ignoredError
.Failed()) {
355 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but "
360 // Set the style of the clause.
361 rv
= selectionOfIME
->SetTextRangeStyle(clauseRange
, textRange
.mRangeStyle
);
363 NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored");
364 break; // but this is unexpected...
368 // If the ranges doesn't include explicit caret position, let's set the
369 // caret to the end of composition string.
371 int32_t caretOffset
=
372 static_cast<int32_t>(aOffsetInNode
+ aLengthOfCompositionString
);
373 MOZ_ASSERT(caretOffset
>= 0 &&
374 static_cast<uint32_t>(caretOffset
) <= maxOffset
);
375 rv
= selection
->CollapseInLimiter(aTextNode
, caretOffset
);
376 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
377 "Selection::CollapseInLimiter() failed");
379 // If caret range isn't specified explicitly, we should hide the caret.
380 // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
381 // However, when there is no range, we should keep showing caret.
383 aEditorBase
.HideCaret(true);
390 } // namespace mozilla