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/. */
5 
6 #include "CompositionTransaction.h"
7 
8 #include "mozilla/EditorBase.h"         // mEditorBase
9 #include "mozilla/SelectionState.h"     // RangeUpdater
10 #include "mozilla/dom/Selection.h"      // local var
11 #include "mozilla/dom/Text.h"           // mTextNode
12 #include "nsAString.h"                  // params
13 #include "nsDebug.h"                    // for NS_ASSERTION, etc
14 #include "nsError.h"                    // for NS_SUCCEEDED, NS_FAILED, etc
15 #include "nsIPresShell.h"               // nsISelectionController constants
16 #include "nsRange.h"                    // local var
17 #include "nsQueryObject.h"              // for do_QueryObject
18 
19 namespace mozilla {
20 
21 using namespace dom;
22 
CompositionTransaction(Text & aTextNode,uint32_t aOffset,uint32_t aReplaceLength,TextRangeArray * aTextRangeArray,const nsAString & aStringToInsert,EditorBase & aEditorBase,RangeUpdater * aRangeUpdater)23 CompositionTransaction::CompositionTransaction(
24                           Text& aTextNode,
25                           uint32_t aOffset,
26                           uint32_t aReplaceLength,
27                           TextRangeArray* aTextRangeArray,
28                           const nsAString& aStringToInsert,
29                           EditorBase& aEditorBase,
30                           RangeUpdater* aRangeUpdater)
31   : mTextNode(&aTextNode)
32   , mOffset(aOffset)
33   , mReplaceLength(aReplaceLength)
34   , mRanges(aTextRangeArray)
35   , mStringToInsert(aStringToInsert)
36   , mEditorBase(aEditorBase)
37   , mRangeUpdater(aRangeUpdater)
38   , mFixed(false)
39 {
40   MOZ_ASSERT(mTextNode->TextLength() >= mOffset);
41 }
42 
~CompositionTransaction()43 CompositionTransaction::~CompositionTransaction()
44 {
45 }
46 
47 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase,
48                                    mTextNode)
49 // mRangeList can't lead to cycles
50 
51 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction)
52   if (aIID.Equals(NS_GET_IID(CompositionTransaction))) {
53     foundInterface = static_cast<nsITransaction*>(this);
54   } else
55 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
56 
NS_IMPL_ADDREF_INHERITED(CompositionTransaction,EditTransactionBase)57 NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase)
58 NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase)
59 
60 NS_IMETHODIMP
61 CompositionTransaction::DoTransaction()
62 {
63   // Fail before making any changes if there's no selection controller
64   nsCOMPtr<nsISelectionController> selCon;
65   mEditorBase.GetSelectionController(getter_AddRefs(selCon));
66   NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED);
67 
68   // Advance caret: This requires the presentation shell to get the selection.
69   if (mReplaceLength == 0) {
70     nsresult rv = mTextNode->InsertData(mOffset, mStringToInsert);
71     if (NS_WARN_IF(NS_FAILED(rv))) {
72       return rv;
73     }
74     mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert);
75   } else {
76     uint32_t replaceableLength = mTextNode->TextLength() - mOffset;
77     nsresult rv =
78       mTextNode->ReplaceData(mOffset, mReplaceLength, mStringToInsert);
79     if (NS_WARN_IF(NS_FAILED(rv))) {
80       return rv;
81     }
82     mRangeUpdater->SelAdjDeleteText(mTextNode, mOffset, mReplaceLength);
83     mRangeUpdater->SelAdjInsertText(*mTextNode, mOffset, mStringToInsert);
84 
85     // If IME text node is multiple node, ReplaceData doesn't remove all IME
86     // text.  So we need remove remained text into other text node.
87     if (replaceableLength < mReplaceLength) {
88       int32_t remainLength = mReplaceLength - replaceableLength;
89       nsCOMPtr<nsINode> node = mTextNode->GetNextSibling();
90       while (node && node->IsNodeOfType(nsINode::eTEXT) &&
91              remainLength > 0) {
92         Text* text = static_cast<Text*>(node.get());
93         uint32_t textLength = text->TextLength();
94         text->DeleteData(0, remainLength);
95         mRangeUpdater->SelAdjDeleteText(text, 0, remainLength);
96         remainLength -= textLength;
97         node = node->GetNextSibling();
98       }
99     }
100   }
101 
102   nsresult rv = SetSelectionForRanges();
103   NS_ENSURE_SUCCESS(rv, rv);
104 
105   return NS_OK;
106 }
107 
108 NS_IMETHODIMP
UndoTransaction()109 CompositionTransaction::UndoTransaction()
110 {
111   // Get the selection first so we'll fail before making any changes if we
112   // can't get it
113   RefPtr<Selection> selection = mEditorBase.GetSelection();
114   NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
115 
116   nsresult rv = mTextNode->DeleteData(mOffset, mStringToInsert.Length());
117   NS_ENSURE_SUCCESS(rv, rv);
118 
119   // set the selection to the insertion point where the string was removed
120   rv = selection->Collapse(mTextNode, mOffset);
121   NS_ASSERTION(NS_SUCCEEDED(rv),
122                "Selection could not be collapsed after undo of IME insert.");
123   NS_ENSURE_SUCCESS(rv, rv);
124 
125   return NS_OK;
126 }
127 
128 NS_IMETHODIMP
Merge(nsITransaction * aTransaction,bool * aDidMerge)129 CompositionTransaction::Merge(nsITransaction* aTransaction,
130                               bool* aDidMerge)
131 {
132   NS_ENSURE_ARG_POINTER(aTransaction && aDidMerge);
133 
134   // Check to make sure we aren't fixed, if we are then nothing gets absorbed
135   if (mFixed) {
136     *aDidMerge = false;
137     return NS_OK;
138   }
139 
140   // If aTransaction is another CompositionTransaction then absorb it
141   RefPtr<CompositionTransaction> otherTransaction =
142     do_QueryObject(aTransaction);
143   if (otherTransaction) {
144     // We absorb the next IME transaction by adopting its insert string
145     mStringToInsert = otherTransaction->mStringToInsert;
146     mRanges = otherTransaction->mRanges;
147     *aDidMerge = true;
148     return NS_OK;
149   }
150 
151   *aDidMerge = false;
152   return NS_OK;
153 }
154 
155 void
MarkFixed()156 CompositionTransaction::MarkFixed()
157 {
158   mFixed = true;
159 }
160 
161 NS_IMETHODIMP
GetTxnDescription(nsAString & aString)162 CompositionTransaction::GetTxnDescription(nsAString& aString)
163 {
164   aString.AssignLiteral("CompositionTransaction: ");
165   aString += mStringToInsert;
166   return NS_OK;
167 }
168 
169 /* ============ private methods ================== */
170 
171 nsresult
SetSelectionForRanges()172 CompositionTransaction::SetSelectionForRanges()
173 {
174   return SetIMESelection(mEditorBase, mTextNode, mOffset,
175                          mStringToInsert.Length(), mRanges);
176 }
177 
178 // static
179 nsresult
SetIMESelection(EditorBase & aEditorBase,Text * aTextNode,uint32_t aOffsetInNode,uint32_t aLengthOfCompositionString,const TextRangeArray * aRanges)180 CompositionTransaction::SetIMESelection(EditorBase& aEditorBase,
181                                         Text* aTextNode,
182                                         uint32_t aOffsetInNode,
183                                         uint32_t aLengthOfCompositionString,
184                                         const TextRangeArray* aRanges)
185 {
186   RefPtr<Selection> selection = aEditorBase.GetSelection();
187   NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
188 
189   nsresult rv = selection->StartBatchChanges();
190   NS_ENSURE_SUCCESS(rv, rv);
191 
192   // First, remove all selections of IME composition.
193   static const RawSelectionType kIMESelections[] = {
194     nsISelectionController::SELECTION_IME_RAWINPUT,
195     nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
196     nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
197     nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT
198   };
199 
200   nsCOMPtr<nsISelectionController> selCon;
201   aEditorBase.GetSelectionController(getter_AddRefs(selCon));
202   NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED);
203 
204   for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) {
205     nsCOMPtr<nsISelection> selectionOfIME;
206     if (NS_FAILED(selCon->GetSelection(kIMESelections[i],
207                                        getter_AddRefs(selectionOfIME)))) {
208       continue;
209     }
210     rv = selectionOfIME->RemoveAllRanges();
211     NS_ASSERTION(NS_SUCCEEDED(rv),
212                  "Failed to remove all ranges of IME selection");
213   }
214 
215   // Set caret position and selection of IME composition with TextRangeArray.
216   bool setCaret = false;
217   uint32_t countOfRanges = aRanges ? aRanges->Length() : 0;
218 
219 #ifdef DEBUG
220   // Bounds-checking on debug builds
221   uint32_t maxOffset = aTextNode->Length();
222 #endif
223 
224   // NOTE: composition string may be truncated when it's committed and
225   //       maxlength attribute value doesn't allow input of all text of this
226   //       composition.
227   for (uint32_t i = 0; i < countOfRanges; ++i) {
228     const TextRange& textRange = aRanges->ElementAt(i);
229 
230     // Caret needs special handling since its length may be 0 and if it's not
231     // specified explicitly, we need to handle it ourselves later.
232     if (textRange.mRangeType == TextRangeType::eCaret) {
233       NS_ASSERTION(!setCaret, "The ranges already has caret position");
234       NS_ASSERTION(!textRange.Length(),
235                    "EditorBase doesn't support wide caret");
236       int32_t caretOffset = static_cast<int32_t>(
237         aOffsetInNode +
238           std::min(textRange.mStartOffset, aLengthOfCompositionString));
239       MOZ_ASSERT(caretOffset >= 0 &&
240                  static_cast<uint32_t>(caretOffset) <= maxOffset);
241       rv = selection->Collapse(aTextNode, caretOffset);
242       setCaret = setCaret || NS_SUCCEEDED(rv);
243       if (NS_WARN_IF(!setCaret)) {
244         continue;
245       }
246       // If caret range is specified explicitly, we should show the caret if
247       // it should be so.
248       aEditorBase.HideCaret(false);
249       continue;
250     }
251 
252     // If the clause length is 0, it should be a bug.
253     if (!textRange.Length()) {
254       NS_WARNING("Any clauses must not be empty");
255       continue;
256     }
257 
258     RefPtr<nsRange> clauseRange;
259     int32_t startOffset = static_cast<int32_t>(
260       aOffsetInNode +
261         std::min(textRange.mStartOffset, aLengthOfCompositionString));
262     MOZ_ASSERT(startOffset >= 0 &&
263                static_cast<uint32_t>(startOffset) <= maxOffset);
264     int32_t endOffset = static_cast<int32_t>(
265       aOffsetInNode +
266         std::min(textRange.mEndOffset, aLengthOfCompositionString));
267     MOZ_ASSERT(endOffset >= startOffset &&
268                static_cast<uint32_t>(endOffset) <= maxOffset);
269     rv = nsRange::CreateRange(aTextNode, startOffset,
270                               aTextNode, endOffset,
271                               getter_AddRefs(clauseRange));
272     if (NS_FAILED(rv)) {
273       NS_WARNING("Failed to create a DOM range for a clause of composition");
274       break;
275     }
276 
277     // Set the range of the clause to selection.
278     nsCOMPtr<nsISelection> selectionOfIME;
279     rv = selCon->GetSelection(ToRawSelectionType(textRange.mRangeType),
280                               getter_AddRefs(selectionOfIME));
281     if (NS_FAILED(rv)) {
282       NS_WARNING("Failed to get IME selection");
283       break;
284     }
285 
286     rv = selectionOfIME->AddRange(clauseRange);
287     if (NS_FAILED(rv)) {
288       NS_WARNING("Failed to add selection range for a clause of composition");
289       break;
290     }
291 
292     // Set the style of the clause.
293     nsCOMPtr<nsISelectionPrivate> selectionOfIMEPriv =
294                                     do_QueryInterface(selectionOfIME);
295     if (!selectionOfIMEPriv) {
296       NS_WARNING("Failed to get nsISelectionPrivate interface from selection");
297       continue; // Since this is additional feature, we can continue this job.
298     }
299     rv = selectionOfIMEPriv->SetTextRangeStyle(clauseRange,
300                                                textRange.mRangeStyle);
301     if (NS_FAILED(rv)) {
302       NS_WARNING("Failed to set selection style");
303       break; // but this is unexpected...
304     }
305   }
306 
307   // If the ranges doesn't include explicit caret position, let's set the
308   // caret to the end of composition string.
309   if (!setCaret) {
310     int32_t caretOffset =
311       static_cast<int32_t>(aOffsetInNode + aLengthOfCompositionString);
312     MOZ_ASSERT(caretOffset >= 0 &&
313                static_cast<uint32_t>(caretOffset) <= maxOffset);
314     rv = selection->Collapse(aTextNode, caretOffset);
315     NS_ASSERTION(NS_SUCCEEDED(rv),
316                  "Failed to set caret at the end of composition string");
317 
318     // If caret range isn't specified explicitly, we should hide the caret.
319     // Hiding the caret benefits a Windows build (see bug 555642 comment #6).
320     // However, when there is no range, we should keep showing caret.
321     if (countOfRanges) {
322       aEditorBase.HideCaret(true);
323     }
324   }
325 
326   rv = selection->EndBatchChangesInternal();
327   NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to end batch changes");
328 
329   return rv;
330 }
331 
332 } // namespace mozilla
333