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/Logging.h"
10 #include "mozilla/SelectionState.h" // RangeUpdater
11 #include "mozilla/TextComposition.h" // TextComposition
12 #include "mozilla/ToString.h"
13 #include "mozilla/dom/Selection.h" // local var
14 #include "mozilla/dom/Text.h" // mTextNode
15 #include "nsAString.h" // params
16 #include "nsDebug.h" // for NS_ASSERTION, etc
17 #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc
18 #include "nsRange.h" // local var
19 #include "nsISelectionController.h" // for nsISelectionController constants
20 #include "nsQueryObject.h" // for do_QueryObject
27 already_AddRefed
<CompositionTransaction
> CompositionTransaction::Create(
28 EditorBase
& aEditorBase
, const nsAString
& aStringToInsert
,
29 const EditorDOMPointInText
& aPointToInsert
) {
30 MOZ_ASSERT(aPointToInsert
.IsSetAndValid());
32 TextComposition
* composition
= aEditorBase
.GetComposition();
33 MOZ_RELEASE_ASSERT(composition
);
34 // XXX Actually, we get different text node and offset from editor in some
35 // cases. If composition stores text node, we should use it and offset
37 EditorDOMPointInText pointToInsert
;
38 if (Text
* textNode
= composition
->GetContainerTextNode()) {
39 pointToInsert
.Set(textNode
, composition
->XPOffsetInTextNode());
41 pointToInsert
.GetContainerAsText() ==
42 composition
->GetContainerTextNode(),
43 "The editor tries to insert composition string into different node");
45 pointToInsert
.Offset() == composition
->XPOffsetInTextNode(),
46 "The editor tries to insert composition string into different offset");
48 pointToInsert
= aPointToInsert
;
50 RefPtr
<CompositionTransaction
> transaction
=
51 new CompositionTransaction(aEditorBase
, aStringToInsert
, pointToInsert
);
52 return transaction
.forget();
55 CompositionTransaction::CompositionTransaction(
56 EditorBase
& aEditorBase
, const nsAString
& aStringToInsert
,
57 const EditorDOMPointInText
& aPointToInsert
)
58 : mTextNode(aPointToInsert
.ContainerAsText()),
59 mOffset(aPointToInsert
.Offset()),
60 mReplaceLength(aEditorBase
.GetComposition()->XPLengthInTextNode()),
61 mRanges(aEditorBase
.GetComposition()->GetRanges()),
62 mStringToInsert(aStringToInsert
),
63 mEditorBase(&aEditorBase
),
65 MOZ_ASSERT(mTextNode
->TextLength() >= mOffset
);
68 std::ostream
& operator<<(std::ostream
& aStream
,
69 const CompositionTransaction
& aTransaction
) {
70 aStream
<< "{ mTextNode=" << aTransaction
.mTextNode
.get();
71 if (aTransaction
.mTextNode
) {
72 aStream
<< " (" << *aTransaction
.mTextNode
<< ")";
74 aStream
<< ", mOffset=" << aTransaction
.mOffset
75 << ", mReplaceLength=" << aTransaction
.mReplaceLength
76 << ", mRanges={ Length()=" << aTransaction
.mRanges
->Length() << " }"
77 << ", mStringToInsert=\""
78 << NS_ConvertUTF16toUTF8(aTransaction
.mStringToInsert
).get() << "\""
79 << ", mEditorBase=" << aTransaction
.mEditorBase
.get() << " }";
83 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction
, EditTransactionBase
,
84 mEditorBase
, mTextNode
)
85 // mRangeList can't lead to cycles
87 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction
)
88 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase
)
89 NS_IMPL_ADDREF_INHERITED(CompositionTransaction
, EditTransactionBase
)
90 NS_IMPL_RELEASE_INHERITED(CompositionTransaction
, EditTransactionBase
)
92 NS_IMETHODIMP
CompositionTransaction::DoTransaction() {
93 MOZ_LOG(GetLogModule(), LogLevel::Info
,
94 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__
,
95 ToString(*this).c_str()));
97 if (NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
)) {
98 return NS_ERROR_NOT_AVAILABLE
;
101 // Fail before making any changes if there's no selection controller
102 if (NS_WARN_IF(!mEditorBase
->GetSelectionController())) {
103 return NS_ERROR_NOT_AVAILABLE
;
106 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
107 OwningNonNull
<Text
> textNode
= *mTextNode
;
109 // Advance caret: This requires the presentation shell to get the selection.
110 if (mReplaceLength
== 0) {
112 editorBase
->DoInsertText(textNode
, mOffset
, mStringToInsert
, error
);
113 if (error
.Failed()) {
114 NS_WARNING("EditorBase::DoInsertText() failed");
115 return error
.StealNSResult();
117 editorBase
->RangeUpdaterRef().SelAdjInsertText(textNode
, mOffset
,
118 mStringToInsert
.Length());
120 // If composition string is split to multiple text nodes, we should put
121 // whole new composition string to the first text node and remove the
122 // compostion string in other nodes.
123 // TODO: This should be handled by `TextComposition` because this assumes
124 // that composition string has never touched by JS. However, it
125 // would occur if the web app is a corrabolation software which
126 // multiple users can modify anyware in an editor.
127 // TODO: And if composition starts from a following text node, the offset
128 // here is outdated and it will cause inserting composition string
129 // **before** the proper point from point of view of the users.
130 uint32_t replaceableLength
= textNode
->TextLength() - mOffset
;
132 editorBase
->DoReplaceText(textNode
, mOffset
, mReplaceLength
,
133 mStringToInsert
, error
);
134 if (error
.Failed()) {
135 NS_WARNING("EditorBase::DoReplaceText() failed");
136 return error
.StealNSResult();
139 // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
140 // this transaction will remove whole composition string. Therefore,
141 // selection should be restored at start of composition string.
142 // XXX Perhaps, this is a bug of our selection managemnt at undoing.
143 editorBase
->RangeUpdaterRef().SelAdjDeleteText(textNode
, mOffset
,
145 // But some ranges which after the composition string should be restored
147 editorBase
->RangeUpdaterRef().SelAdjInsertText(textNode
, mOffset
,
148 mStringToInsert
.Length());
150 if (replaceableLength
< mReplaceLength
) {
151 // XXX Perhaps, scanning following sibling text nodes with composition
152 // string length which we know is wrong because there may be
153 // non-empty text nodes which are inserted by JS. Instead, we
154 // should remove all text in the ranges of IME selections.
155 uint32_t remainLength
= mReplaceLength
- replaceableLength
;
156 IgnoredErrorResult ignoredError
;
157 for (nsIContent
* nextSibling
= textNode
->GetNextSibling();
158 nextSibling
&& nextSibling
->IsText() && remainLength
;
159 nextSibling
= nextSibling
->GetNextSibling()) {
160 OwningNonNull
<Text
> followingTextNode
=
161 *static_cast<Text
*>(nextSibling
);
162 uint32_t textLength
= followingTextNode
->TextLength();
163 editorBase
->DoDeleteText(followingTextNode
, 0, remainLength
,
165 NS_WARNING_ASSERTION(!ignoredError
.Failed(),
166 "EditorBase::DoDeleteText() failed, but ignored");
167 ignoredError
.SuppressException();
168 // XXX Needs to check whether the text is deleted as expected.
169 editorBase
->RangeUpdaterRef().SelAdjDeleteText(followingTextNode
, 0,
171 remainLength
-= textLength
;
176 nsresult rv
= SetSelectionForRanges();
177 NS_WARNING_ASSERTION(
179 "CompositionTransaction::SetSelectionForRanges() failed");
181 if (TextComposition
* composition
= editorBase
->GetComposition()) {
182 composition
->OnUpdateCompositionInEditor(mStringToInsert
, textNode
,
189 NS_IMETHODIMP
CompositionTransaction::UndoTransaction() {
190 MOZ_LOG(GetLogModule(), LogLevel::Info
,
191 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__
,
192 ToString(*this).c_str()));
194 if (MOZ_UNLIKELY(NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
))) {
195 return NS_ERROR_NOT_AVAILABLE
;
198 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
199 OwningNonNull
<Text
> textNode
= *mTextNode
;
200 IgnoredErrorResult error
;
201 editorBase
->DoDeleteText(textNode
, mOffset
, mStringToInsert
.Length(), error
);
202 if (MOZ_UNLIKELY(error
.Failed())) {
203 NS_WARNING("EditorBase::DoDeleteText() failed");
204 return error
.StealNSResult();
207 // set the selection to the insertion point where the string was removed
208 editorBase
->CollapseSelectionTo(EditorRawDOMPoint(textNode
, mOffset
), error
);
209 NS_ASSERTION(!error
.Failed(), "EditorBase::CollapseSelectionTo() failed");
210 return error
.StealNSResult();
213 NS_IMETHODIMP
CompositionTransaction::RedoTransaction() {
214 MOZ_LOG(GetLogModule(), LogLevel::Info
,
215 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__
,
216 ToString(*this).c_str()));
217 return DoTransaction();
220 NS_IMETHODIMP
CompositionTransaction::Merge(nsITransaction
* aOtherTransaction
,
222 MOZ_LOG(GetLogModule(), LogLevel::Debug
,
223 ("%p CompositionTransaction::%s(aOtherTransaction=%p) this=%s", this,
224 __FUNCTION__
, aOtherTransaction
, ToString(*this).c_str()));
226 if (NS_WARN_IF(!aOtherTransaction
) || NS_WARN_IF(!aDidMerge
)) {
227 return NS_ERROR_INVALID_ARG
;
231 // Check to make sure we aren't fixed, if we are then nothing gets merged.
233 MOZ_LOG(GetLogModule(), LogLevel::Debug
,
234 ("%p CompositionTransaction::%s returned false due to fixed", this,
239 RefPtr
<EditTransactionBase
> otherTransactionBase
=
240 aOtherTransaction
->GetAsEditTransactionBase();
241 if (!otherTransactionBase
) {
242 MOZ_LOG(GetLogModule(), LogLevel::Debug
,
243 ("%p CompositionTransaction::%s returned false due to not edit "
245 this, __FUNCTION__
));
249 // If aTransaction is another CompositionTransaction then merge it
250 CompositionTransaction
* otherCompositionTransaction
=
251 otherTransactionBase
->GetAsCompositionTransaction();
252 if (!otherCompositionTransaction
) {
256 // We merge the next IME transaction by adopting its insert string.
257 mStringToInsert
= otherCompositionTransaction
->mStringToInsert
;
258 mRanges
= otherCompositionTransaction
->mRanges
;
260 MOZ_LOG(GetLogModule(), LogLevel::Debug
,
261 ("%p CompositionTransaction::%s returned true", this, __FUNCTION__
));
265 void CompositionTransaction::MarkFixed() { mFixed
= true; }
267 /* ============ private methods ================== */
269 nsresult
CompositionTransaction::SetSelectionForRanges() {
270 if (NS_WARN_IF(!mEditorBase
) || NS_WARN_IF(!mTextNode
)) {
271 return NS_ERROR_NOT_AVAILABLE
;
273 OwningNonNull
<EditorBase
> editorBase
= *mEditorBase
;
274 OwningNonNull
<Text
> textNode
= *mTextNode
;
275 RefPtr
<TextRangeArray
> ranges
= mRanges
;
276 nsresult rv
= SetIMESelection(editorBase
, textNode
, mOffset
,
277 mStringToInsert
.Length(), ranges
);
278 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
279 "CompositionTransaction::SetIMESelection() failed");
284 nsresult
CompositionTransaction::SetIMESelection(
285 EditorBase
& aEditorBase
, Text
* aTextNode
, uint32_t aOffsetInNode
,
286 uint32_t aLengthOfCompositionString
, const TextRangeArray
* aRanges
) {
287 RefPtr
<Selection
> selection
= aEditorBase
.GetSelection();
288 if (NS_WARN_IF(!selection
)) {
289 return NS_ERROR_NOT_INITIALIZED
;
292 SelectionBatcher
selectionBatcher(selection
, __FUNCTION__
);
294 // First, remove all selections of IME composition.
295 static const RawSelectionType kIMESelections
[] = {
296 nsISelectionController::SELECTION_IME_RAWINPUT
,
297 nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT
,
298 nsISelectionController::SELECTION_IME_CONVERTEDTEXT
,
299 nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT
};
301 nsCOMPtr
<nsISelectionController
> selectionController
=
302 aEditorBase
.GetSelectionController();
303 if (NS_WARN_IF(!selectionController
)) {
304 return NS_ERROR_NOT_INITIALIZED
;
307 IgnoredErrorResult ignoredError
;
308 for (uint32_t i
= 0; i
< ArrayLength(kIMESelections
); ++i
) {
309 RefPtr
<Selection
> selectionOfIME
=
310 selectionController
->GetSelection(kIMESelections
[i
]);
311 if (!selectionOfIME
) {
312 NS_WARNING("nsISelectionController::GetSelection() failed");
315 selectionOfIME
->RemoveAllRanges(ignoredError
);
316 NS_WARNING_ASSERTION(!ignoredError
.Failed(),
317 "Selection::RemoveAllRanges() failed, but ignored");
318 ignoredError
.SuppressException();
321 // Set caret position and selection of IME composition with TextRangeArray.
322 bool setCaret
= false;
323 uint32_t countOfRanges
= aRanges
? aRanges
->Length() : 0;
326 // Bounds-checking on debug builds
327 uint32_t maxOffset
= aTextNode
->Length();
330 // NOTE: composition string may be truncated when it's committed and
331 // maxlength attribute value doesn't allow input of all text of this
334 for (uint32_t i
= 0; i
< countOfRanges
; ++i
) {
335 const TextRange
& textRange
= aRanges
->ElementAt(i
);
337 // Caret needs special handling since its length may be 0 and if it's not
338 // specified explicitly, we need to handle it ourselves later.
339 if (textRange
.mRangeType
== TextRangeType::eCaret
) {
340 NS_ASSERTION(!setCaret
, "The ranges already has caret position");
341 NS_ASSERTION(!textRange
.Length(),
342 "EditorBase doesn't support wide caret");
343 CheckedUint32
caretOffset(aOffsetInNode
);
345 std::min(textRange
.mStartOffset
, aLengthOfCompositionString
);
346 MOZ_ASSERT(caretOffset
.isValid());
347 MOZ_ASSERT(caretOffset
.value() <= maxOffset
);
348 rv
= selection
->CollapseInLimiter(aTextNode
, caretOffset
.value());
349 NS_WARNING_ASSERTION(
351 "Selection::CollapseInLimiter() failed, but might be ignored");
352 setCaret
= setCaret
|| NS_SUCCEEDED(rv
);
356 // If caret range is specified explicitly, we should show the caret if
358 aEditorBase
.HideCaret(false);
362 // If the clause length is 0, it should be a bug.
363 if (!textRange
.Length()) {
364 NS_WARNING("Any clauses must not be empty");
368 RefPtr
<nsRange
> clauseRange
;
369 CheckedUint32 startOffset
= aOffsetInNode
;
370 startOffset
+= std::min(textRange
.mStartOffset
, aLengthOfCompositionString
);
371 MOZ_ASSERT(startOffset
.isValid());
372 MOZ_ASSERT(startOffset
.value() <= maxOffset
);
373 CheckedUint32 endOffset
= aOffsetInNode
;
374 endOffset
+= std::min(textRange
.mEndOffset
, aLengthOfCompositionString
);
375 MOZ_ASSERT(endOffset
.isValid());
376 MOZ_ASSERT(endOffset
.value() >= startOffset
.value());
377 MOZ_ASSERT(endOffset
.value() <= maxOffset
);
378 clauseRange
= nsRange::Create(aTextNode
, startOffset
.value(), aTextNode
,
379 endOffset
.value(), IgnoreErrors());
381 NS_WARNING("nsRange::Create() failed, but might be ignored");
385 // Set the range of the clause to selection.
386 RefPtr
<Selection
> selectionOfIME
= selectionController
->GetSelection(
387 ToRawSelectionType(textRange
.mRangeType
));
388 if (!selectionOfIME
) {
390 "nsISelectionController::GetSelection() failed, but might be "
395 IgnoredErrorResult ignoredError
;
396 selectionOfIME
->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange
,
398 if (ignoredError
.Failed()) {
400 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but "
405 // Set the style of the clause.
406 rv
= selectionOfIME
->SetTextRangeStyle(clauseRange
, textRange
.mRangeStyle
);
408 NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored");
409 break; // but this is unexpected...
413 // If the ranges doesn't include explicit caret position, let's set the
414 // caret to the end of composition string.
416 CheckedUint32 caretOffset
= aOffsetInNode
;
417 caretOffset
+= aLengthOfCompositionString
;
418 MOZ_ASSERT(caretOffset
.isValid());
419 MOZ_ASSERT(caretOffset
.value() <= maxOffset
);
420 rv
= selection
->CollapseInLimiter(aTextNode
, caretOffset
.value());
421 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
),
422 "Selection::CollapseInLimiter() failed");
424 // If caret range isn't specified explicitly, we should hide the caret.
425 // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
426 // However, when there is no range, we should keep showing caret.
428 aEditorBase
.HideCaret(true);
435 } // namespace mozilla