1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* ***** BEGIN LICENSE BLOCK *****
3 : * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 : *
5 : * The contents of this file are subject to the Mozilla Public License Version
6 : * 1.1 (the "License"); you may not use this file except in compliance with
7 : * the License. You may obtain a copy of the License at
8 : * http://www.mozilla.org/MPL/
9 : *
10 : * Software distributed under the License is distributed on an "AS IS" basis,
11 : * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 : * for the specific language governing rights and limitations under the
13 : * License.
14 : *
15 : * The Original Code is Real-time Spellchecking
16 : *
17 : * The Initial Developer of the Original Code is Mozdev Group, Inc.
18 : * Portions created by the Initial Developer are Copyright (C) 2004
19 : * the Initial Developer. All Rights Reserved.
20 : *
21 : * Contributor(s): Neil Deakin (neil@mozdevgroup.com)
22 : * Scott MacGregor (mscott@mozilla.org)
23 : * Brett Wilson <brettw@gmail.com>
24 : *
25 : * Alternatively, the contents of this file may be used under the terms of
26 : * either the GNU General Public License Version 2 or later (the "GPL"), or
27 : * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 : * in which case the provisions of the GPL or the LGPL are applicable instead
29 : * of those above. If you wish to allow use of your version of this file only
30 : * under the terms of either the GPL or the LGPL, and not to allow others to
31 : * use your version of this file under the terms of the MPL, indicate your
32 : * decision by deleting the provisions above and replace them with the notice
33 : * and other provisions required by the GPL or the LGPL. If you do not delete
34 : * the provisions above, a recipient may use your version of this file under
35 : * the terms of any one of the MPL, the GPL or the LGPL.
36 : *
37 : * ***** END LICENSE BLOCK ***** */
38 :
39 : /**
40 : * This class is called by the editor to handle spellchecking after various
41 : * events. The main entrypoint is SpellCheckAfterEditorChange, which is called
42 : * when the text is changed.
43 : *
44 : * It is VERY IMPORTANT that we do NOT do any operations that might cause DOM
45 : * notifications to be flushed when we are called from the editor. This is
46 : * because the call might originate from a frame, and flushing the
47 : * notifications might cause that frame to be deleted.
48 : *
49 : * Using the WordUtil class to find words causes DOM notifications to be
50 : * flushed because it asks for style information. As a result, we post an event
51 : * and do all of the spellchecking in that event handler, which occurs later.
52 : * We store all DOM pointers in ranges because they are kept up-to-date with
53 : * DOM changes that may have happened while the event was on the queue.
54 : *
55 : * We also allow the spellcheck to be suspended and resumed later. This makes
56 : * large pastes or initializations with a lot of text not hang the browser UI.
57 : *
58 : * An optimization is the mNeedsCheckAfterNavigation flag. This is set to
59 : * true when we get any change, and false once there is no possibility
60 : * something changed that we need to check on navigation. Navigation events
61 : * tend to be a little tricky because we want to check the current word on
62 : * exit if something has changed. If we navigate inside the word, we don't want
63 : * to do anything. As a result, this flag is cleared in FinishNavigationEvent
64 : * when we know that we are checking as a result of navigation.
65 : */
66 :
67 : #include "mozInlineSpellChecker.h"
68 : #include "mozInlineSpellWordUtil.h"
69 : #include "mozISpellI18NManager.h"
70 : #include "nsCOMPtr.h"
71 : #include "nsCRT.h"
72 : #include "nsIDocument.h"
73 : #include "nsIDOMNode.h"
74 : #include "nsIDOMDocument.h"
75 : #include "nsIDOMElement.h"
76 : #include "nsIDOMEventTarget.h"
77 : #include "nsIDOMMouseEvent.h"
78 : #include "nsIDOMKeyEvent.h"
79 : #include "nsIDOMNode.h"
80 : #include "nsIDOMNodeList.h"
81 : #include "nsRange.h"
82 : #include "nsIPlaintextEditor.h"
83 : #include "nsIPrefBranch.h"
84 : #include "nsIPrefService.h"
85 : #include "nsIRunnable.h"
86 : #include "nsISelection.h"
87 : #include "nsISelectionPrivate.h"
88 : #include "nsISelectionController.h"
89 : #include "nsIServiceManager.h"
90 : #include "nsITextServicesFilter.h"
91 : #include "nsString.h"
92 : #include "nsThreadUtils.h"
93 : #include "nsUnicharUtils.h"
94 : #include "nsIContent.h"
95 : #include "nsEventListenerManager.h"
96 : #include "nsGUIEvent.h"
97 : #include "nsRange.h"
98 : #include "nsContentUtils.h"
99 :
100 : // Set to spew messages to the console about what is happening.
101 : //#define DEBUG_INLINESPELL
102 :
103 : // the number of milliseconds that we will take at once to do spellchecking
104 : #define INLINESPELL_CHECK_TIMEOUT 50
105 :
106 : // The number of words to check before we look at the time to see if
107 : // INLINESPELL_CHECK_TIMEOUT ms have elapsed. This prevents us from spending
108 : // too much time checking the clock. Note that misspelled words count for
109 : // more than one word in this calculation.
110 : #define INLINESPELL_TIMEOUT_CHECK_FREQUENCY 50
111 :
112 : // This number is the number of checked words a misspelled word counts for
113 : // when we're checking the time to see if the alloted time is up for
114 : // spellchecking. Misspelled words take longer to process since we have to
115 : // create a range, so they count more. The exact number isn't very important
116 : // since this just controls how often we check the current time.
117 : #define MISSPELLED_WORD_COUNT_PENALTY 4
118 :
119 :
120 : static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
121 : nsINode* aPossibleAncestor);
122 :
123 : static const char kMaxSpellCheckSelectionSize[] = "extensions.spellcheck.inline.max-misspellings";
124 :
125 0 : mozInlineSpellStatus::mozInlineSpellStatus(mozInlineSpellChecker* aSpellChecker)
126 0 : : mSpellChecker(aSpellChecker), mWordCount(0)
127 : {
128 0 : }
129 :
130 : // mozInlineSpellStatus::InitForEditorChange
131 : //
132 : // This is the most complicated case. For changes, we need to compute the
133 : // range of stuff that changed based on the old and new caret positions,
134 : // as well as use a range possibly provided by the editor (start and end,
135 : // which are usually NULL) to get a range with the union of these.
136 :
137 : nsresult
138 0 : mozInlineSpellStatus::InitForEditorChange(
139 : PRInt32 aAction,
140 : nsIDOMNode* aAnchorNode, PRInt32 aAnchorOffset,
141 : nsIDOMNode* aPreviousNode, PRInt32 aPreviousOffset,
142 : nsIDOMNode* aStartNode, PRInt32 aStartOffset,
143 : nsIDOMNode* aEndNode, PRInt32 aEndOffset)
144 : {
145 : nsresult rv;
146 :
147 0 : nsCOMPtr<nsIDOMDocument> doc;
148 0 : rv = GetDocument(getter_AddRefs(doc));
149 0 : NS_ENSURE_SUCCESS(rv, rv);
150 :
151 : // save the anchor point as a range so we can find the current word later
152 : rv = PositionToCollapsedRange(doc, aAnchorNode, aAnchorOffset,
153 0 : getter_AddRefs(mAnchorRange));
154 0 : NS_ENSURE_SUCCESS(rv, rv);
155 :
156 0 : if (aAction == mozInlineSpellChecker::kOpDeleteSelection) {
157 : // Deletes are easy, the range is just the current anchor. We set the range
158 : // to check to be empty, FinishInitOnEvent will fill in the range to be
159 : // the current word.
160 0 : mOp = eOpChangeDelete;
161 0 : mRange = nsnull;
162 0 : return NS_OK;
163 : }
164 :
165 0 : mOp = eOpChange;
166 :
167 : // range to check
168 0 : mRange = new nsRange();
169 :
170 : // ...we need to put the start and end in the correct order
171 : PRInt16 cmpResult;
172 0 : rv = mAnchorRange->ComparePoint(aPreviousNode, aPreviousOffset, &cmpResult);
173 0 : NS_ENSURE_SUCCESS(rv, rv);
174 0 : if (cmpResult < 0) {
175 : // previous anchor node is before the current anchor
176 0 : rv = mRange->SetStart(aPreviousNode, aPreviousOffset);
177 0 : NS_ENSURE_SUCCESS(rv, rv);
178 0 : rv = mRange->SetEnd(aAnchorNode, aAnchorOffset);
179 : } else {
180 : // previous anchor node is after (or the same as) the current anchor
181 0 : rv = mRange->SetStart(aAnchorNode, aAnchorOffset);
182 0 : NS_ENSURE_SUCCESS(rv, rv);
183 0 : rv = mRange->SetEnd(aPreviousNode, aPreviousOffset);
184 : }
185 0 : NS_ENSURE_SUCCESS(rv, rv);
186 :
187 : // On insert save this range: DoSpellCheck optimizes things in this range.
188 : // Otherwise, just leave this NULL.
189 0 : if (aAction == mozInlineSpellChecker::kOpInsertText)
190 0 : mCreatedRange = mRange;
191 :
192 : // if we were given a range, we need to expand our range to encompass it
193 0 : if (aStartNode && aEndNode) {
194 0 : rv = mRange->ComparePoint(aStartNode, aStartOffset, &cmpResult);
195 0 : NS_ENSURE_SUCCESS(rv, rv);
196 0 : if (cmpResult < 0) { // given range starts before
197 0 : rv = mRange->SetStart(aStartNode, aStartOffset);
198 0 : NS_ENSURE_SUCCESS(rv, rv);
199 : }
200 :
201 0 : rv = mRange->ComparePoint(aEndNode, aEndOffset, &cmpResult);
202 0 : NS_ENSURE_SUCCESS(rv, rv);
203 0 : if (cmpResult > 0) { // given range ends after
204 0 : rv = mRange->SetEnd(aEndNode, aEndOffset);
205 0 : NS_ENSURE_SUCCESS(rv, rv);
206 : }
207 : }
208 :
209 0 : return NS_OK;
210 : }
211 :
212 : // mozInlineSpellStatis::InitForNavigation
213 : //
214 : // For navigation events, we just need to store the new and old positions.
215 : //
216 : // In some cases, we detect that we shouldn't check. If this event should
217 : // not be processed, *aContinue will be false.
218 :
219 : nsresult
220 0 : mozInlineSpellStatus::InitForNavigation(
221 : bool aForceCheck, PRInt32 aNewPositionOffset,
222 : nsIDOMNode* aOldAnchorNode, PRInt32 aOldAnchorOffset,
223 : nsIDOMNode* aNewAnchorNode, PRInt32 aNewAnchorOffset,
224 : bool* aContinue)
225 : {
226 : nsresult rv;
227 0 : mOp = eOpNavigation;
228 :
229 0 : mForceNavigationWordCheck = aForceCheck;
230 0 : mNewNavigationPositionOffset = aNewPositionOffset;
231 :
232 : // get the root node for checking
233 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
234 0 : NS_ENSURE_SUCCESS(rv, rv);
235 0 : nsCOMPtr<nsIDOMElement> rootElt;
236 0 : rv = editor->GetRootElement(getter_AddRefs(rootElt));
237 0 : NS_ENSURE_SUCCESS(rv, rv);
238 :
239 : // the anchor node might not be in the DOM anymore, check
240 0 : nsCOMPtr<nsINode> root = do_QueryInterface(rootElt, &rv);
241 0 : NS_ENSURE_SUCCESS(rv, rv);
242 0 : nsCOMPtr<nsINode> currentAnchor = do_QueryInterface(aOldAnchorNode, &rv);
243 0 : NS_ENSURE_SUCCESS(rv, rv);
244 0 : if (root && currentAnchor && ! ContentIsDescendantOf(currentAnchor, root)) {
245 0 : *aContinue = false;
246 0 : return NS_OK;
247 : }
248 :
249 0 : nsCOMPtr<nsIDOMDocument> doc;
250 0 : rv = GetDocument(getter_AddRefs(doc));
251 0 : NS_ENSURE_SUCCESS(rv, rv);
252 :
253 : rv = PositionToCollapsedRange(doc, aOldAnchorNode, aOldAnchorOffset,
254 0 : getter_AddRefs(mOldNavigationAnchorRange));
255 0 : NS_ENSURE_SUCCESS(rv, rv);
256 : rv = PositionToCollapsedRange(doc, aNewAnchorNode, aNewAnchorOffset,
257 0 : getter_AddRefs(mAnchorRange));
258 0 : NS_ENSURE_SUCCESS(rv, rv);
259 :
260 0 : *aContinue = true;
261 0 : return NS_OK;
262 : }
263 :
264 : // mozInlineSpellStatus::InitForSelection
265 : //
266 : // It is easy for selections since we always re-check the spellcheck
267 : // selection.
268 :
269 : nsresult
270 0 : mozInlineSpellStatus::InitForSelection()
271 : {
272 0 : mOp = eOpSelection;
273 0 : return NS_OK;
274 : }
275 :
276 : // mozInlineSpellStatus::InitForRange
277 : //
278 : // Called to cause the spellcheck of the given range. This will look like
279 : // a change operation over the given range.
280 :
281 : nsresult
282 0 : mozInlineSpellStatus::InitForRange(nsRange* aRange)
283 : {
284 0 : mOp = eOpChange;
285 0 : mRange = aRange;
286 0 : return NS_OK;
287 : }
288 :
289 : // mozInlineSpellStatus::FinishInitOnEvent
290 : //
291 : // Called when the event is triggered to complete initialization that
292 : // might require the WordUtil. This calls to the operation-specific
293 : // initializer, and also sets the range to be the entire element if it
294 : // is NULL.
295 : //
296 : // Watch out: the range might still be NULL if there is nothing to do,
297 : // the caller will have to check for this.
298 :
299 : nsresult
300 0 : mozInlineSpellStatus::FinishInitOnEvent(mozInlineSpellWordUtil& aWordUtil)
301 : {
302 : nsresult rv;
303 0 : if (! mRange) {
304 : rv = mSpellChecker->MakeSpellCheckRange(nsnull, 0, nsnull, 0,
305 0 : getter_AddRefs(mRange));
306 0 : NS_ENSURE_SUCCESS(rv, rv);
307 : }
308 :
309 0 : switch (mOp) {
310 : case eOpChange:
311 0 : if (mAnchorRange)
312 0 : return FillNoCheckRangeFromAnchor(aWordUtil);
313 0 : break;
314 : case eOpChangeDelete:
315 0 : if (mAnchorRange) {
316 0 : rv = FillNoCheckRangeFromAnchor(aWordUtil);
317 0 : NS_ENSURE_SUCCESS(rv, rv);
318 : }
319 : // Delete events will have no range for the changed text (because it was
320 : // deleted), and InitForEditorChange will set it to NULL. Here, we select
321 : // the entire word to cause any underlining to be removed.
322 0 : mRange = mNoCheckRange;
323 0 : break;
324 : case eOpNavigation:
325 0 : return FinishNavigationEvent(aWordUtil);
326 : case eOpSelection:
327 : // this gets special handling in ResumeCheck
328 0 : break;
329 : case eOpResume:
330 : // everything should be initialized already in this case
331 0 : break;
332 : default:
333 0 : NS_NOTREACHED("Bad operation");
334 0 : return NS_ERROR_NOT_INITIALIZED;
335 : }
336 0 : return NS_OK;
337 : }
338 :
339 : // mozInlineSpellStatus::FinishNavigationEvent
340 : //
341 : // This verifies that we need to check the word at the previous caret
342 : // position. Now that we have the word util, we can find the word belonging
343 : // to the previous caret position. If the new position is inside that word,
344 : // we don't want to do anything. In this case, we'll NULL out mRange so
345 : // that the caller will know not to continue.
346 : //
347 : // Notice that we don't set mNoCheckRange. We check here whether the cursor
348 : // is in the word that needs checking, so it isn't necessary. Plus, the
349 : // spellchecker isn't guaranteed to only check the given word, and it could
350 : // remove the underline from the new word under the cursor.
351 :
352 : nsresult
353 0 : mozInlineSpellStatus::FinishNavigationEvent(mozInlineSpellWordUtil& aWordUtil)
354 : {
355 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor);
356 0 : if (! editor)
357 0 : return NS_ERROR_FAILURE; // editor is gone
358 :
359 0 : NS_ASSERTION(mAnchorRange, "No anchor for navigation!");
360 0 : nsCOMPtr<nsIDOMNode> newAnchorNode, oldAnchorNode;
361 : PRInt32 newAnchorOffset, oldAnchorOffset;
362 :
363 : // get the DOM position of the old caret, the range should be collapsed
364 0 : nsresult rv = mOldNavigationAnchorRange->GetStartContainer(
365 0 : getter_AddRefs(oldAnchorNode));
366 0 : NS_ENSURE_SUCCESS(rv, rv);
367 0 : rv = mOldNavigationAnchorRange->GetStartOffset(&oldAnchorOffset);
368 0 : NS_ENSURE_SUCCESS(rv, rv);
369 :
370 : // find the word on the old caret position, this is the one that we MAY need
371 : // to check
372 0 : nsRefPtr<nsRange> oldWord;
373 : rv = aWordUtil.GetRangeForWord(oldAnchorNode, oldAnchorOffset,
374 0 : getter_AddRefs(oldWord));
375 0 : NS_ENSURE_SUCCESS(rv, rv);
376 :
377 : // aWordUtil.GetRangeForWord flushes pending notifications, check editor again.
378 0 : editor = do_QueryReferent(mSpellChecker->mEditor);
379 0 : if (! editor)
380 0 : return NS_ERROR_FAILURE; // editor is gone
381 :
382 : // get the DOM position of the new caret, the range should be collapsed
383 0 : rv = mAnchorRange->GetStartContainer(getter_AddRefs(newAnchorNode));
384 0 : NS_ENSURE_SUCCESS(rv, rv);
385 0 : rv = mAnchorRange->GetStartOffset(&newAnchorOffset);
386 0 : NS_ENSURE_SUCCESS(rv, rv);
387 :
388 : // see if the new cursor position is in the word of the old cursor position
389 0 : bool isInRange = false;
390 0 : if (! mForceNavigationWordCheck) {
391 0 : rv = oldWord->IsPointInRange(newAnchorNode,
392 : newAnchorOffset + mNewNavigationPositionOffset,
393 0 : &isInRange);
394 0 : NS_ENSURE_SUCCESS(rv, rv);
395 : }
396 :
397 0 : if (isInRange) {
398 : // caller should give up
399 0 : mRange = nsnull;
400 : } else {
401 : // check the old word
402 0 : mRange = oldWord;
403 :
404 : // Once we've spellchecked the current word, we don't need to spellcheck
405 : // for any more navigation events.
406 0 : mSpellChecker->mNeedsCheckAfterNavigation = false;
407 : }
408 0 : return NS_OK;
409 : }
410 :
411 : // mozInlineSpellStatus::FillNoCheckRangeFromAnchor
412 : //
413 : // Given the mAnchorRange object, computes the range of the word it is on
414 : // (if any) and fills that range into mNoCheckRange. This is used for
415 : // change and navigation events to know which word we should skip spell
416 : // checking on
417 :
418 : nsresult
419 0 : mozInlineSpellStatus::FillNoCheckRangeFromAnchor(
420 : mozInlineSpellWordUtil& aWordUtil)
421 : {
422 0 : nsCOMPtr<nsIDOMNode> anchorNode;
423 0 : nsresult rv = mAnchorRange->GetStartContainer(getter_AddRefs(anchorNode));
424 0 : NS_ENSURE_SUCCESS(rv, rv);
425 :
426 : PRInt32 anchorOffset;
427 0 : rv = mAnchorRange->GetStartOffset(&anchorOffset);
428 0 : NS_ENSURE_SUCCESS(rv, rv);
429 :
430 : return aWordUtil.GetRangeForWord(anchorNode, anchorOffset,
431 0 : getter_AddRefs(mNoCheckRange));
432 : }
433 :
434 : // mozInlineSpellStatus::GetDocument
435 : //
436 : // Returns the nsIDOMDocument object for the document for the
437 : // current spellchecker.
438 :
439 : nsresult
440 0 : mozInlineSpellStatus::GetDocument(nsIDOMDocument** aDocument)
441 : {
442 : nsresult rv;
443 0 : *aDocument = nsnull;
444 0 : if (! mSpellChecker->mEditor)
445 0 : return NS_ERROR_UNEXPECTED;
446 :
447 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mSpellChecker->mEditor, &rv);
448 0 : NS_ENSURE_SUCCESS(rv, rv);
449 :
450 0 : nsCOMPtr<nsIDOMDocument> domDoc;
451 0 : rv = editor->GetDocument(getter_AddRefs(domDoc));
452 0 : NS_ENSURE_SUCCESS(rv, rv);
453 0 : NS_ENSURE_TRUE(domDoc, NS_ERROR_NULL_POINTER);
454 0 : domDoc.forget(aDocument);
455 0 : return NS_OK;
456 : }
457 :
458 : // mozInlineSpellStatus::PositionToCollapsedRange
459 : //
460 : // Converts a given DOM position to a collapsed range covering that
461 : // position. We use ranges to store DOM positions becuase they stay
462 : // updated as the DOM is changed.
463 :
464 : nsresult
465 0 : mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument,
466 : nsIDOMNode* aNode, PRInt32 aOffset, nsIDOMRange** aRange)
467 : {
468 0 : *aRange = nsnull;
469 0 : nsCOMPtr<nsIDOMRange> range;
470 0 : nsresult rv = aDocument->CreateRange(getter_AddRefs(range));
471 0 : NS_ENSURE_SUCCESS(rv, rv);
472 :
473 0 : rv = range->SetStart(aNode, aOffset);
474 0 : NS_ENSURE_SUCCESS(rv, rv);
475 0 : rv = range->SetEnd(aNode, aOffset);
476 0 : NS_ENSURE_SUCCESS(rv, rv);
477 :
478 0 : range.swap(*aRange);
479 0 : return NS_OK;
480 : }
481 :
482 : // mozInlineSpellResume
483 :
484 : class mozInlineSpellResume : public nsRunnable
485 0 : {
486 : public:
487 0 : mozInlineSpellResume(const mozInlineSpellStatus& aStatus) : mStatus(aStatus) {}
488 : mozInlineSpellStatus mStatus;
489 0 : nsresult Post()
490 : {
491 0 : return NS_DispatchToMainThread(this);
492 : }
493 :
494 0 : NS_IMETHOD Run()
495 : {
496 0 : mStatus.mSpellChecker->ResumeCheck(&mStatus);
497 0 : return NS_OK;
498 : }
499 : };
500 :
501 :
502 0 : NS_INTERFACE_MAP_BEGIN(mozInlineSpellChecker)
503 0 : NS_INTERFACE_MAP_ENTRY(nsIInlineSpellChecker)
504 0 : NS_INTERFACE_MAP_ENTRY(nsIEditActionListener)
505 0 : NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
506 0 : NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
507 0 : NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
508 0 : NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozInlineSpellChecker)
509 0 : NS_INTERFACE_MAP_END
510 :
511 0 : NS_IMPL_CYCLE_COLLECTING_ADDREF(mozInlineSpellChecker)
512 0 : NS_IMPL_CYCLE_COLLECTING_RELEASE(mozInlineSpellChecker)
513 :
514 1464 : NS_IMPL_CYCLE_COLLECTION_5(mozInlineSpellChecker,
515 : mSpellCheck,
516 : mTextServicesDocument,
517 : mTreeWalker,
518 : mConverter,
519 : mCurrentSelectionAnchorNode)
520 :
521 : mozInlineSpellChecker::SpellCheckingState
522 : mozInlineSpellChecker::gCanEnableSpellChecking =
523 : mozInlineSpellChecker::SpellCheck_Uninitialized;
524 :
525 0 : mozInlineSpellChecker::mozInlineSpellChecker() :
526 : mNumWordsInSpellSelection(0),
527 : mMaxNumWordsInSpellSelection(250),
528 : mNeedsCheckAfterNavigation(false),
529 0 : mFullSpellCheckScheduled(false)
530 : {
531 0 : nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
532 0 : if (prefs)
533 0 : prefs->GetIntPref(kMaxSpellCheckSelectionSize, &mMaxNumWordsInSpellSelection);
534 0 : mMaxMisspellingsPerCheck = mMaxNumWordsInSpellSelection * 3 / 4;
535 0 : }
536 :
537 0 : mozInlineSpellChecker::~mozInlineSpellChecker()
538 : {
539 0 : }
540 :
541 : NS_IMETHODIMP
542 0 : mozInlineSpellChecker::GetSpellChecker(nsIEditorSpellCheck **aSpellCheck)
543 : {
544 0 : *aSpellCheck = mSpellCheck;
545 0 : NS_IF_ADDREF(*aSpellCheck);
546 0 : return NS_OK;
547 : }
548 :
549 : NS_IMETHODIMP
550 0 : mozInlineSpellChecker::Init(nsIEditor *aEditor)
551 : {
552 0 : mEditor = do_GetWeakReference(aEditor);
553 0 : return NS_OK;
554 : }
555 :
556 : // mozInlineSpellChecker::Cleanup
557 : //
558 : // Called by the editor when the editor is going away. This is important
559 : // because we remove listeners. We do NOT clean up anything else in this
560 : // function, because it can get called while DoSpellCheck is running!
561 : //
562 : // Getting the style information there can cause DOM notifications to be
563 : // flushed, which can cause editors to go away which will bring us here.
564 : // We can not do anything that will cause DoSpellCheck to freak out.
565 :
566 0 : nsresult mozInlineSpellChecker::Cleanup(bool aDestroyingFrames)
567 : {
568 0 : mNumWordsInSpellSelection = 0;
569 0 : nsCOMPtr<nsISelection> spellCheckSelection;
570 0 : nsresult rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
571 0 : if (NS_FAILED(rv)) {
572 : // Ensure we still unregister event listeners (but return a failure code)
573 0 : UnregisterEventListeners();
574 : } else {
575 0 : if (!aDestroyingFrames) {
576 0 : spellCheckSelection->RemoveAllRanges();
577 : }
578 :
579 0 : rv = UnregisterEventListeners();
580 : }
581 0 : mEditor = nsnull;
582 :
583 0 : return rv;
584 : }
585 :
586 : // mozInlineSpellChecker::CanEnableInlineSpellChecking
587 : //
588 : // This function can be called to see if it seems likely that we can enable
589 : // spellchecking before actually creating the InlineSpellChecking objects.
590 : //
591 : // The problem is that we can't get the dictionary list without actually
592 : // creating a whole bunch of spellchecking objects. This function tries to
593 : // do that and caches the result so we don't have to keep allocating those
594 : // objects if there are no dictionaries or spellchecking.
595 : //
596 : // Whenever dictionaries are added or removed at runtime, this value must be
597 : // updated before an observer notification is sent out about the change, to
598 : // avoid editors getting a wrong cached result.
599 :
600 : bool // static
601 0 : mozInlineSpellChecker::CanEnableInlineSpellChecking()
602 : {
603 : nsresult rv;
604 0 : if (gCanEnableSpellChecking == SpellCheck_Uninitialized) {
605 0 : gCanEnableSpellChecking = SpellCheck_NotAvailable;
606 :
607 : nsCOMPtr<nsIEditorSpellCheck> spellchecker =
608 0 : do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &rv);
609 0 : NS_ENSURE_SUCCESS(rv, false);
610 :
611 0 : bool canSpellCheck = false;
612 0 : rv = spellchecker->CanSpellCheck(&canSpellCheck);
613 0 : NS_ENSURE_SUCCESS(rv, false);
614 :
615 0 : if (canSpellCheck)
616 0 : gCanEnableSpellChecking = SpellCheck_Available;
617 : }
618 0 : return (gCanEnableSpellChecking == SpellCheck_Available);
619 : }
620 :
621 : void // static
622 0 : mozInlineSpellChecker::UpdateCanEnableInlineSpellChecking()
623 : {
624 0 : gCanEnableSpellChecking = SpellCheck_Uninitialized;
625 0 : }
626 :
627 : // mozInlineSpellChecker::RegisterEventListeners
628 : //
629 : // The inline spell checker listens to mouse events and keyboard navigation+ // events.
630 :
631 : nsresult
632 0 : mozInlineSpellChecker::RegisterEventListeners()
633 : {
634 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
635 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
636 :
637 0 : editor->AddEditActionListener(this);
638 :
639 0 : nsCOMPtr<nsIDOMDocument> doc;
640 0 : nsresult rv = editor->GetDocument(getter_AddRefs(doc));
641 0 : NS_ENSURE_SUCCESS(rv, rv);
642 :
643 0 : nsCOMPtr<nsIDOMEventTarget> piTarget = do_QueryInterface(doc, &rv);
644 0 : NS_ENSURE_SUCCESS(rv, rv);
645 :
646 0 : piTarget->AddEventListener(NS_LITERAL_STRING("blur"), this,
647 0 : true, false);
648 0 : piTarget->AddEventListener(NS_LITERAL_STRING("click"), this,
649 0 : false, false);
650 0 : piTarget->AddEventListener(NS_LITERAL_STRING("keypress"), this,
651 0 : false, false);
652 0 : return NS_OK;
653 : }
654 :
655 : // mozInlineSpellChecker::UnregisterEventListeners
656 :
657 : nsresult
658 0 : mozInlineSpellChecker::UnregisterEventListeners()
659 : {
660 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
661 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
662 :
663 0 : editor->RemoveEditActionListener(this);
664 :
665 0 : nsCOMPtr<nsIDOMDocument> doc;
666 0 : editor->GetDocument(getter_AddRefs(doc));
667 0 : NS_ENSURE_TRUE(doc, NS_ERROR_NULL_POINTER);
668 :
669 0 : nsCOMPtr<nsIDOMEventTarget> piTarget = do_QueryInterface(doc);
670 0 : NS_ENSURE_TRUE(piTarget, NS_ERROR_NULL_POINTER);
671 :
672 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
673 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("click"), this, false);
674 0 : piTarget->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, false);
675 0 : return NS_OK;
676 : }
677 :
678 : // mozInlineSpellChecker::GetEnableRealTimeSpell
679 :
680 : NS_IMETHODIMP
681 0 : mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled)
682 : {
683 0 : NS_ENSURE_ARG_POINTER(aEnabled);
684 0 : *aEnabled = mSpellCheck != nsnull;
685 0 : return NS_OK;
686 : }
687 :
688 : // mozInlineSpellChecker::SetEnableRealTimeSpell
689 :
690 : NS_IMETHODIMP
691 0 : mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
692 : {
693 0 : if (!aEnabled) {
694 0 : mSpellCheck = nsnull;
695 0 : return Cleanup(false);
696 : }
697 :
698 0 : if (!mSpellCheck) {
699 0 : nsresult res = NS_OK;
700 0 : nsCOMPtr<nsIEditorSpellCheck> spellchecker = do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &res);
701 0 : if (NS_SUCCEEDED(res) && spellchecker)
702 : {
703 0 : nsCOMPtr<nsITextServicesFilter> filter = do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1", &res);
704 0 : spellchecker->SetFilter(filter);
705 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
706 0 : res = spellchecker->InitSpellChecker(editor, false);
707 0 : NS_ENSURE_SUCCESS(res, res);
708 :
709 0 : nsCOMPtr<nsITextServicesDocument> tsDoc = do_CreateInstance("@mozilla.org/textservices/textservicesdocument;1", &res);
710 0 : NS_ENSURE_SUCCESS(res, res);
711 :
712 0 : res = tsDoc->SetFilter(filter);
713 0 : NS_ENSURE_SUCCESS(res, res);
714 :
715 0 : res = tsDoc->InitWithEditor(editor);
716 0 : NS_ENSURE_SUCCESS(res, res);
717 :
718 0 : mTextServicesDocument = tsDoc;
719 0 : mSpellCheck = spellchecker;
720 :
721 : // spell checking is enabled, register our event listeners to track navigation
722 0 : RegisterEventListeners();
723 : }
724 : }
725 :
726 : // spellcheck the current contents. SpellCheckRange doesn't supply a created
727 : // range to DoSpellCheck, which in our case is the entire range. But this
728 : // optimization doesn't matter because there is nothing in the spellcheck
729 : // selection when starting, which triggers a better optimization.
730 0 : return SpellCheckRange(nsnull);
731 : }
732 :
733 : // mozInlineSpellChecker::SpellCheckAfterEditorChange
734 : //
735 : // Called by the editor when nearly anything happens to change the content.
736 : //
737 : // The start and end positions specify a range for the thing that happened,
738 : // but these are usually NULL, even when you'd think they would be useful
739 : // because you want the range (for example, pasting). We ignore them in
740 : // this case.
741 :
742 : NS_IMETHODIMP
743 0 : mozInlineSpellChecker::SpellCheckAfterEditorChange(
744 : PRInt32 aAction, nsISelection *aSelection,
745 : nsIDOMNode *aPreviousSelectedNode, PRInt32 aPreviousSelectedOffset,
746 : nsIDOMNode *aStartNode, PRInt32 aStartOffset,
747 : nsIDOMNode *aEndNode, PRInt32 aEndOffset)
748 : {
749 : nsresult rv;
750 0 : NS_ENSURE_ARG_POINTER(aSelection);
751 0 : if (!mSpellCheck)
752 0 : return NS_OK; // disabling spell checking is not an error
753 :
754 : // this means something has changed, and we never check the current word,
755 : // therefore, we should spellcheck for subsequent caret navigations
756 0 : mNeedsCheckAfterNavigation = true;
757 :
758 : // the anchor node is the position of the caret
759 0 : nsCOMPtr<nsIDOMNode> anchorNode;
760 0 : rv = aSelection->GetAnchorNode(getter_AddRefs(anchorNode));
761 0 : NS_ENSURE_SUCCESS(rv, rv);
762 : PRInt32 anchorOffset;
763 0 : rv = aSelection->GetAnchorOffset(&anchorOffset);
764 0 : NS_ENSURE_SUCCESS(rv, rv);
765 :
766 0 : mozInlineSpellStatus status(this);
767 : rv = status.InitForEditorChange(aAction,
768 : anchorNode, anchorOffset,
769 : aPreviousSelectedNode, aPreviousSelectedOffset,
770 : aStartNode, aStartOffset,
771 0 : aEndNode, aEndOffset);
772 0 : NS_ENSURE_SUCCESS(rv, rv);
773 0 : rv = ScheduleSpellCheck(status);
774 0 : NS_ENSURE_SUCCESS(rv, rv);
775 :
776 : // remember the current caret position after every change
777 0 : SaveCurrentSelectionPosition();
778 0 : return NS_OK;
779 : }
780 :
781 : // mozInlineSpellChecker::SpellCheckRange
782 : //
783 : // Spellchecks all the words in the given range.
784 : // Supply a NULL range and this will check the entire editor.
785 :
786 : nsresult
787 0 : mozInlineSpellChecker::SpellCheckRange(nsIDOMRange* aRange)
788 : {
789 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
790 :
791 0 : mozInlineSpellStatus status(this);
792 0 : nsRange* range = static_cast<nsRange*>(aRange);
793 0 : nsresult rv = status.InitForRange(range);
794 0 : NS_ENSURE_SUCCESS(rv, rv);
795 0 : return ScheduleSpellCheck(status);
796 : }
797 :
798 : // mozInlineSpellChecker::GetMisspelledWord
799 :
800 : NS_IMETHODIMP
801 0 : mozInlineSpellChecker::GetMisspelledWord(nsIDOMNode *aNode, PRInt32 aOffset,
802 : nsIDOMRange **newword)
803 : {
804 0 : NS_ENSURE_ARG_POINTER(aNode);
805 0 : nsCOMPtr<nsISelection> spellCheckSelection;
806 0 : nsresult res = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
807 0 : NS_ENSURE_SUCCESS(res, res);
808 :
809 0 : return IsPointInSelection(spellCheckSelection, aNode, aOffset, newword);
810 : }
811 :
812 : // mozInlineSpellChecker::ReplaceWord
813 :
814 : NS_IMETHODIMP
815 0 : mozInlineSpellChecker::ReplaceWord(nsIDOMNode *aNode, PRInt32 aOffset,
816 : const nsAString &newword)
817 : {
818 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
819 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
820 0 : NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
821 :
822 0 : nsCOMPtr<nsIDOMRange> range;
823 0 : nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
824 0 : NS_ENSURE_SUCCESS(res, res);
825 :
826 0 : if (range)
827 : {
828 0 : editor->BeginTransaction();
829 :
830 0 : nsCOMPtr<nsISelection> selection;
831 0 : res = editor->GetSelection(getter_AddRefs(selection));
832 0 : NS_ENSURE_SUCCESS(res, res);
833 0 : selection->RemoveAllRanges();
834 0 : selection->AddRange(range);
835 0 : editor->DeleteSelection(nsIEditor::eNone);
836 :
837 0 : nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
838 0 : textEditor->InsertText(newword);
839 :
840 0 : editor->EndTransaction();
841 : }
842 :
843 0 : return NS_OK;
844 : }
845 :
846 : // mozInlineSpellChecker::AddWordToDictionary
847 :
848 : NS_IMETHODIMP
849 0 : mozInlineSpellChecker::AddWordToDictionary(const nsAString &word)
850 : {
851 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
852 :
853 0 : nsAutoString wordstr(word);
854 0 : nsresult rv = mSpellCheck->AddWordToDictionary(wordstr.get());
855 0 : NS_ENSURE_SUCCESS(rv, rv);
856 :
857 0 : mozInlineSpellStatus status(this);
858 0 : rv = status.InitForSelection();
859 0 : NS_ENSURE_SUCCESS(rv, rv);
860 0 : return ScheduleSpellCheck(status);
861 : }
862 :
863 : // mozInlineSpellChecker::RemoveWordFromDictionary
864 :
865 : NS_IMETHODIMP
866 0 : mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word)
867 : {
868 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
869 :
870 0 : nsAutoString wordstr(word);
871 0 : nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get());
872 0 : NS_ENSURE_SUCCESS(rv, rv);
873 :
874 0 : mozInlineSpellStatus status(this);
875 0 : rv = status.InitForRange(nsnull);
876 0 : NS_ENSURE_SUCCESS(rv, rv);
877 0 : return ScheduleSpellCheck(status);
878 : }
879 :
880 : // mozInlineSpellChecker::IgnoreWord
881 :
882 : NS_IMETHODIMP
883 0 : mozInlineSpellChecker::IgnoreWord(const nsAString &word)
884 : {
885 0 : NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
886 :
887 0 : nsAutoString wordstr(word);
888 0 : nsresult rv = mSpellCheck->IgnoreWordAllOccurrences(wordstr.get());
889 0 : NS_ENSURE_SUCCESS(rv, rv);
890 :
891 0 : mozInlineSpellStatus status(this);
892 0 : rv = status.InitForSelection();
893 0 : NS_ENSURE_SUCCESS(rv, rv);
894 0 : return ScheduleSpellCheck(status);
895 : }
896 :
897 : // mozInlineSpellChecker::IgnoreWords
898 :
899 : NS_IMETHODIMP
900 0 : mozInlineSpellChecker::IgnoreWords(const PRUnichar **aWordsToIgnore,
901 : PRUint32 aCount)
902 : {
903 : // add each word to the ignore list and then recheck the document
904 0 : for (PRUint32 index = 0; index < aCount; index++)
905 0 : mSpellCheck->IgnoreWordAllOccurrences(aWordsToIgnore[index]);
906 :
907 0 : mozInlineSpellStatus status(this);
908 0 : nsresult rv = status.InitForSelection();
909 0 : NS_ENSURE_SUCCESS(rv, rv);
910 0 : return ScheduleSpellCheck(status);
911 : }
912 :
913 0 : NS_IMETHODIMP mozInlineSpellChecker::WillCreateNode(const nsAString & aTag, nsIDOMNode *aParent, PRInt32 aPosition)
914 : {
915 0 : return NS_OK;
916 : }
917 :
918 0 : NS_IMETHODIMP mozInlineSpellChecker::DidCreateNode(const nsAString & aTag, nsIDOMNode *aNode, nsIDOMNode *aParent,
919 : PRInt32 aPosition, nsresult aResult)
920 : {
921 0 : return NS_OK;
922 : }
923 :
924 0 : NS_IMETHODIMP mozInlineSpellChecker::WillInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
925 : PRInt32 aPosition)
926 : {
927 0 : return NS_OK;
928 : }
929 :
930 0 : NS_IMETHODIMP mozInlineSpellChecker::DidInsertNode(nsIDOMNode *aNode, nsIDOMNode *aParent,
931 : PRInt32 aPosition, nsresult aResult)
932 : {
933 :
934 0 : return NS_OK;
935 : }
936 :
937 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteNode(nsIDOMNode *aChild)
938 : {
939 0 : return NS_OK;
940 : }
941 :
942 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteNode(nsIDOMNode *aChild, nsresult aResult)
943 : {
944 0 : return NS_OK;
945 : }
946 :
947 0 : NS_IMETHODIMP mozInlineSpellChecker::WillSplitNode(nsIDOMNode *aExistingRightNode, PRInt32 aOffset)
948 : {
949 0 : return NS_OK;
950 : }
951 :
952 : NS_IMETHODIMP
953 0 : mozInlineSpellChecker::DidSplitNode(nsIDOMNode *aExistingRightNode,
954 : PRInt32 aOffset,
955 : nsIDOMNode *aNewLeftNode, nsresult aResult)
956 : {
957 0 : return SpellCheckBetweenNodes(aNewLeftNode, 0, aNewLeftNode, 0);
958 : }
959 :
960 0 : NS_IMETHODIMP mozInlineSpellChecker::WillJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, nsIDOMNode *aParent)
961 : {
962 0 : return NS_OK;
963 : }
964 :
965 0 : NS_IMETHODIMP mozInlineSpellChecker::DidJoinNodes(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode,
966 : nsIDOMNode *aParent, nsresult aResult)
967 : {
968 0 : return SpellCheckBetweenNodes(aRightNode, 0, aRightNode, 0);
969 : }
970 :
971 0 : NS_IMETHODIMP mozInlineSpellChecker::WillInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, const nsAString & aString)
972 : {
973 0 : return NS_OK;
974 : }
975 :
976 0 : NS_IMETHODIMP mozInlineSpellChecker::DidInsertText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset,
977 : const nsAString & aString, nsresult aResult)
978 : {
979 0 : return NS_OK;
980 : }
981 :
982 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength)
983 : {
984 0 : return NS_OK;
985 : }
986 :
987 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteText(nsIDOMCharacterData *aTextNode, PRInt32 aOffset, PRInt32 aLength, nsresult aResult)
988 : {
989 0 : return NS_OK;
990 : }
991 :
992 0 : NS_IMETHODIMP mozInlineSpellChecker::WillDeleteSelection(nsISelection *aSelection)
993 : {
994 0 : return NS_OK;
995 : }
996 :
997 0 : NS_IMETHODIMP mozInlineSpellChecker::DidDeleteSelection(nsISelection *aSelection)
998 : {
999 0 : return NS_OK;
1000 : }
1001 :
1002 : // mozInlineSpellChecker::MakeSpellCheckRange
1003 : //
1004 : // Given begin and end positions, this function constructs a range as
1005 : // required for ScheduleSpellCheck. If the start and end nodes are NULL,
1006 : // then the entire range will be selected, and you can supply -1 as the
1007 : // offset to the end range to select all of that node.
1008 : //
1009 : // If the resulting range would be empty, NULL is put into *aRange and the
1010 : // function succeeds.
1011 :
1012 : nsresult
1013 0 : mozInlineSpellChecker::MakeSpellCheckRange(
1014 : nsIDOMNode* aStartNode, PRInt32 aStartOffset,
1015 : nsIDOMNode* aEndNode, PRInt32 aEndOffset,
1016 : nsRange** aRange)
1017 : {
1018 : nsresult rv;
1019 0 : *aRange = nsnull;
1020 :
1021 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1022 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1023 :
1024 0 : nsCOMPtr<nsIDOMDocument> doc;
1025 0 : rv = editor->GetDocument(getter_AddRefs(doc));
1026 0 : NS_ENSURE_SUCCESS(rv, rv);
1027 0 : NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
1028 :
1029 0 : nsCOMPtr<nsIDOMRange> range;
1030 0 : rv = doc->CreateRange(getter_AddRefs(range));
1031 0 : NS_ENSURE_SUCCESS(rv, rv);
1032 :
1033 : // possibly use full range of the editor
1034 0 : nsCOMPtr<nsIDOMElement> rootElem;
1035 0 : if (! aStartNode || ! aEndNode) {
1036 0 : rv = editor->GetRootElement(getter_AddRefs(rootElem));
1037 0 : NS_ENSURE_SUCCESS(rv, rv);
1038 :
1039 0 : aStartNode = rootElem;
1040 0 : aStartOffset = 0;
1041 :
1042 0 : aEndNode = rootElem;
1043 0 : aEndOffset = -1;
1044 : }
1045 :
1046 0 : if (aEndOffset == -1) {
1047 0 : nsCOMPtr<nsIDOMNodeList> childNodes;
1048 0 : rv = aEndNode->GetChildNodes(getter_AddRefs(childNodes));
1049 0 : NS_ENSURE_SUCCESS(rv, rv);
1050 :
1051 : PRUint32 childCount;
1052 0 : rv = childNodes->GetLength(&childCount);
1053 0 : NS_ENSURE_SUCCESS(rv, rv);
1054 :
1055 0 : aEndOffset = childCount;
1056 : }
1057 :
1058 : // sometimes we are are requested to check an empty range (possibly an empty
1059 : // document). This will result in assertions later.
1060 0 : if (aStartNode == aEndNode && aStartOffset == aEndOffset)
1061 0 : return NS_OK;
1062 :
1063 0 : rv = range->SetStart(aStartNode, aStartOffset);
1064 0 : NS_ENSURE_SUCCESS(rv, rv);
1065 0 : if (aEndOffset)
1066 0 : rv = range->SetEnd(aEndNode, aEndOffset);
1067 : else
1068 0 : rv = range->SetEndAfter(aEndNode);
1069 0 : NS_ENSURE_SUCCESS(rv, rv);
1070 :
1071 0 : *aRange = static_cast<nsRange*>(range.forget().get());
1072 0 : return NS_OK;
1073 : }
1074 :
1075 : nsresult
1076 0 : mozInlineSpellChecker::SpellCheckBetweenNodes(nsIDOMNode *aStartNode,
1077 : PRInt32 aStartOffset,
1078 : nsIDOMNode *aEndNode,
1079 : PRInt32 aEndOffset)
1080 : {
1081 0 : nsRefPtr<nsRange> range;
1082 : nsresult rv = MakeSpellCheckRange(aStartNode, aStartOffset,
1083 : aEndNode, aEndOffset,
1084 0 : getter_AddRefs(range));
1085 0 : NS_ENSURE_SUCCESS(rv, rv);
1086 :
1087 0 : if (! range)
1088 0 : return NS_OK; // range is empty: nothing to do
1089 :
1090 0 : mozInlineSpellStatus status(this);
1091 0 : rv = status.InitForRange(range);
1092 0 : NS_ENSURE_SUCCESS(rv, rv);
1093 0 : return ScheduleSpellCheck(status);
1094 : }
1095 :
1096 : // mozInlineSpellChecker::SkipSpellCheckForNode
1097 : //
1098 : // There are certain conditions when we don't want to spell check a node. In
1099 : // particular quotations, moz signatures, etc. This routine returns false
1100 : // for these cases.
1101 :
1102 : nsresult
1103 0 : mozInlineSpellChecker::SkipSpellCheckForNode(nsIEditor* aEditor,
1104 : nsIDOMNode *aNode,
1105 : bool *checkSpelling)
1106 : {
1107 0 : *checkSpelling = true;
1108 0 : NS_ENSURE_ARG_POINTER(aNode);
1109 :
1110 : PRUint32 flags;
1111 0 : aEditor->GetFlags(&flags);
1112 0 : if (flags & nsIPlaintextEditor::eEditorMailMask)
1113 : {
1114 0 : nsCOMPtr<nsIDOMNode> parent;
1115 0 : aNode->GetParentNode(getter_AddRefs(parent));
1116 :
1117 0 : while (parent)
1118 : {
1119 0 : nsCOMPtr<nsIDOMElement> parentElement = do_QueryInterface(parent);
1120 0 : if (!parentElement)
1121 : break;
1122 :
1123 0 : nsAutoString parentTagName;
1124 0 : parentElement->GetTagName(parentTagName);
1125 :
1126 0 : if (parentTagName.Equals(NS_LITERAL_STRING("blockquote"), nsCaseInsensitiveStringComparator()))
1127 : {
1128 0 : nsAutoString quotetype;
1129 0 : parentElement->GetAttribute(NS_LITERAL_STRING("type"), quotetype);
1130 0 : if (quotetype.Equals(NS_LITERAL_STRING("cite"), nsCaseInsensitiveStringComparator()))
1131 : {
1132 0 : *checkSpelling = false;
1133 : break;
1134 : }
1135 : }
1136 0 : else if (parentTagName.Equals(NS_LITERAL_STRING("pre"), nsCaseInsensitiveStringComparator()))
1137 : {
1138 0 : nsAutoString classname;
1139 0 : parentElement->GetAttribute(NS_LITERAL_STRING("class"),classname);
1140 0 : if (classname.Equals(NS_LITERAL_STRING("moz-signature")))
1141 0 : *checkSpelling = false;
1142 : }
1143 :
1144 0 : nsCOMPtr<nsIDOMNode> nextParent;
1145 0 : parent->GetParentNode(getter_AddRefs(nextParent));
1146 0 : parent = nextParent;
1147 : }
1148 : }
1149 : else {
1150 : // XXX Do we really want this for all editable content?
1151 0 : nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
1152 0 : *checkSpelling = content->IsEditable();
1153 : }
1154 :
1155 0 : return NS_OK;
1156 : }
1157 :
1158 : // mozInlineSpellChecker::ScheduleSpellCheck
1159 : //
1160 : // This is called by code to do the actual spellchecking. We will set up
1161 : // the proper structures for calls to DoSpellCheck.
1162 :
1163 : nsresult
1164 0 : mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
1165 : {
1166 0 : if (mFullSpellCheckScheduled) {
1167 : // Just ignore this; we're going to spell-check everything anyway
1168 0 : return NS_OK;
1169 : }
1170 :
1171 0 : mozInlineSpellResume* resume = new mozInlineSpellResume(aStatus);
1172 0 : NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
1173 :
1174 0 : nsresult rv = resume->Post();
1175 0 : if (NS_FAILED(rv)) {
1176 0 : delete resume;
1177 0 : } else if (aStatus.IsFullSpellCheck()) {
1178 : // We're going to check everything. Suppress further spell-check attempts
1179 : // until that happens.
1180 0 : mFullSpellCheckScheduled = true;
1181 : }
1182 0 : return rv;
1183 : }
1184 :
1185 : // mozInlineSpellChecker::DoSpellCheckSelection
1186 : //
1187 : // Called to re-check all misspelled words. We iterate over all ranges in
1188 : // the selection and call DoSpellCheck on them. This is used when a word
1189 : // is ignored or added to the dictionary: all instances of that word should
1190 : // be removed from the selection.
1191 : //
1192 : // FIXME-PERFORMANCE: This takes as long as it takes and is not resumable.
1193 : // Typically, checking this small amount of text is relatively fast, but
1194 : // for large numbers of words, a lag may be noticeable.
1195 :
1196 : nsresult
1197 0 : mozInlineSpellChecker::DoSpellCheckSelection(mozInlineSpellWordUtil& aWordUtil,
1198 : nsISelection* aSpellCheckSelection,
1199 : mozInlineSpellStatus* aStatus)
1200 : {
1201 : nsresult rv;
1202 :
1203 : // clear out mNumWordsInSpellSelection since we'll be rebuilding the ranges.
1204 0 : mNumWordsInSpellSelection = 0;
1205 :
1206 : // Since we could be modifying the ranges for the spellCheckSelection while
1207 : // looping on the spell check selection, keep a separate array of range
1208 : // elements inside the selection
1209 0 : nsCOMArray<nsIDOMRange> ranges;
1210 :
1211 : PRInt32 count;
1212 0 : aSpellCheckSelection->GetRangeCount(&count);
1213 :
1214 : PRInt32 idx;
1215 0 : nsCOMPtr<nsIDOMRange> checkRange;
1216 0 : for (idx = 0; idx < count; idx ++) {
1217 0 : aSpellCheckSelection->GetRangeAt(idx, getter_AddRefs(checkRange));
1218 0 : if (checkRange) {
1219 0 : if (! ranges.AppendObject(checkRange))
1220 0 : return NS_ERROR_OUT_OF_MEMORY;
1221 : }
1222 : }
1223 :
1224 : // We have saved the ranges above. Clearing the spellcheck selection here
1225 : // isn't necessary (rechecking each word will modify it as necessary) but
1226 : // provides better performance. By ensuring that no ranges need to be
1227 : // removed in DoSpellCheck, we can save checking range inclusion which is
1228 : // slow.
1229 0 : aSpellCheckSelection->RemoveAllRanges();
1230 :
1231 : // We use this state object for all calls, and just update its range. Note
1232 : // that we don't need to call FinishInit since we will be filling in the
1233 : // necessary information.
1234 0 : mozInlineSpellStatus status(this);
1235 0 : rv = status.InitForRange(nsnull);
1236 0 : NS_ENSURE_SUCCESS(rv, rv);
1237 :
1238 : bool doneChecking;
1239 0 : for (idx = 0; idx < count; idx ++) {
1240 0 : checkRange = ranges[idx];
1241 0 : if (checkRange) {
1242 : // We can consider this word as "added" since we know it has no spell
1243 : // check range over it that needs to be deleted. All the old ranges
1244 : // were cleared above. We also need to clear the word count so that we
1245 : // check all words instead of stopping early.
1246 0 : status.mRange = static_cast<nsRange*>(checkRange.get());
1247 : rv = DoSpellCheck(aWordUtil, aSpellCheckSelection, &status,
1248 0 : &doneChecking);
1249 0 : NS_ENSURE_SUCCESS(rv, rv);
1250 0 : NS_ASSERTION(doneChecking, "We gave the spellchecker one word, but it didn't finish checking?!?!");
1251 :
1252 0 : status.mWordCount = 0;
1253 : }
1254 : }
1255 :
1256 0 : return NS_OK;
1257 : }
1258 :
1259 : // mozInlineSpellChecker::DoSpellCheck
1260 : //
1261 : // This function checks words intersecting the given range, excluding those
1262 : // inside mStatus->mNoCheckRange (can be NULL). Words inside aNoCheckRange
1263 : // will have any spell selection removed (this is used to hide the
1264 : // underlining for the word that the caret is in). aNoCheckRange should be
1265 : // on word boundaries.
1266 : //
1267 : // mResume->mCreatedRange is a possibly NULL range of new text that was
1268 : // inserted. Inside this range, we don't bother to check whether things are
1269 : // inside the spellcheck selection, which speeds up large paste operations
1270 : // considerably.
1271 : //
1272 : // Normal case when editing text by typing
1273 : // h e l l o w o r k d h o w a r e y o u
1274 : // ^ caret
1275 : // [-------] mRange
1276 : // [-------] mNoCheckRange
1277 : // -> does nothing (range is the same as the no check range)
1278 : //
1279 : // Case when pasting:
1280 : // [---------- pasted text ----------]
1281 : // h e l l o w o r k d h o w a r e y o u
1282 : // ^ caret
1283 : // [---] aNoCheckRange
1284 : // -> recheck all words in range except those in aNoCheckRange
1285 : //
1286 : // If checking is complete, *aDoneChecking will be set. If there is more
1287 : // but we ran out of time, this will be false and the range will be
1288 : // updated with the stuff that still needs checking.
1289 :
1290 0 : nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
1291 : nsISelection *aSpellCheckSelection,
1292 : mozInlineSpellStatus* aStatus,
1293 : bool* aDoneChecking)
1294 : {
1295 0 : *aDoneChecking = true;
1296 :
1297 : // get the editor for SkipSpellCheckForNode, this may fail in reasonable
1298 : // circumstances since the editor could have gone away
1299 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1300 0 : if (! editor)
1301 0 : return NS_ERROR_FAILURE;
1302 :
1303 : bool iscollapsed;
1304 0 : nsresult rv = aStatus->mRange->GetCollapsed(&iscollapsed);
1305 0 : NS_ENSURE_SUCCESS(rv, rv);
1306 0 : if (iscollapsed)
1307 0 : return NS_OK;
1308 :
1309 0 : nsCOMPtr<nsISelectionPrivate> privSel = do_QueryInterface(aSpellCheckSelection);
1310 :
1311 : // see if the selection has any ranges, if not, then we can optimize checking
1312 : // range inclusion later (we have no ranges when we are initially checking or
1313 : // when there are no misspelled words yet).
1314 : PRInt32 originalRangeCount;
1315 0 : rv = aSpellCheckSelection->GetRangeCount(&originalRangeCount);
1316 0 : NS_ENSURE_SUCCESS(rv, rv);
1317 :
1318 : // set the starting DOM position to be the beginning of our range
1319 : {
1320 : // Scope for the node/offset pairs here so they don't get
1321 : // accidentally used later
1322 0 : nsINode* beginNode = aStatus->mRange->GetStartParent();
1323 0 : PRInt32 beginOffset = aStatus->mRange->StartOffset();
1324 0 : nsINode* endNode = aStatus->mRange->GetEndParent();
1325 0 : PRInt32 endOffset = aStatus->mRange->EndOffset();
1326 :
1327 : // Now check that we're still looking at a range that's under
1328 : // aWordUtil.GetRootNode()
1329 0 : nsINode* rootNode = aWordUtil.GetRootNode();
1330 0 : if (!nsContentUtils::ContentIsDescendantOf(beginNode, rootNode) ||
1331 0 : !nsContentUtils::ContentIsDescendantOf(endNode, rootNode)) {
1332 : // Just bail out and don't try to spell-check this
1333 0 : return NS_OK;
1334 : }
1335 :
1336 0 : aWordUtil.SetEnd(endNode, endOffset);
1337 0 : aWordUtil.SetPosition(beginNode, beginOffset);
1338 : }
1339 :
1340 : // aWordUtil.SetPosition flushes pending notifications, check editor again.
1341 0 : editor = do_QueryReferent(mEditor);
1342 0 : if (! editor)
1343 0 : return NS_ERROR_FAILURE;
1344 :
1345 0 : PRInt32 wordsSinceTimeCheck = 0;
1346 0 : PRTime beginTime = PR_Now();
1347 :
1348 0 : nsAutoString wordText;
1349 0 : nsRefPtr<nsRange> wordRange;
1350 : bool dontCheckWord;
1351 0 : while (NS_SUCCEEDED(aWordUtil.GetNextWord(wordText,
1352 : getter_AddRefs(wordRange),
1353 : &dontCheckWord)) &&
1354 0 : wordRange) {
1355 0 : wordsSinceTimeCheck ++;
1356 :
1357 : // get the range for the current word.
1358 : // Not using nsINode here for now because we have to call into
1359 : // selection APIs that use nsIDOMNode. :(
1360 0 : nsCOMPtr<nsIDOMNode> beginNode, endNode;
1361 : PRInt32 beginOffset, endOffset;
1362 0 : wordRange->GetStartContainer(getter_AddRefs(beginNode));
1363 0 : wordRange->GetEndContainer(getter_AddRefs(endNode));
1364 0 : wordRange->GetStartOffset(&beginOffset);
1365 0 : wordRange->GetEndOffset(&endOffset);
1366 :
1367 : #ifdef DEBUG_INLINESPELL
1368 : printf("->Got word \"%s\"", NS_ConvertUTF16toUTF8(wordText).get());
1369 : if (dontCheckWord)
1370 : printf(" (not checking)");
1371 : printf("\n");
1372 : #endif
1373 :
1374 : // see if there is a spellcheck range that already intersects the word
1375 : // and remove it. We only need to remove old ranges, so don't bother if
1376 : // there were no ranges when we started out.
1377 0 : if (originalRangeCount > 0) {
1378 : // likewise, if this word is inside new text, we won't bother testing
1379 0 : bool inCreatedRange = false;
1380 0 : if (aStatus->mCreatedRange)
1381 0 : aStatus->mCreatedRange->IsPointInRange(beginNode, beginOffset, &inCreatedRange);
1382 0 : if (! inCreatedRange) {
1383 0 : nsTArray<nsRange*> ranges;
1384 0 : nsCOMPtr<nsINode> firstNode = do_QueryInterface(beginNode);
1385 0 : nsCOMPtr<nsINode> lastNode = do_QueryInterface(endNode);
1386 0 : rv = privSel->GetRangesForIntervalArray(firstNode, beginOffset,
1387 : lastNode, endOffset,
1388 0 : true, &ranges);
1389 0 : NS_ENSURE_SUCCESS(rv, rv);
1390 0 : for (PRUint32 i = 0; i < ranges.Length(); i++)
1391 0 : RemoveRange(aSpellCheckSelection, ranges[i]);
1392 : }
1393 : }
1394 :
1395 : // some words are special and don't need checking
1396 0 : if (dontCheckWord)
1397 0 : continue;
1398 :
1399 : // some nodes we don't spellcheck
1400 : bool checkSpelling;
1401 0 : rv = SkipSpellCheckForNode(editor, beginNode, &checkSpelling);
1402 0 : NS_ENSURE_SUCCESS(rv, rv);
1403 0 : if (!checkSpelling)
1404 0 : continue;
1405 :
1406 : // Don't check spelling if we're inside the noCheckRange. This needs to
1407 : // be done after we clear any old selection because the excluded word
1408 : // might have been previously marked.
1409 : //
1410 : // We do a simple check to see if the beginning of our word is in the
1411 : // exclusion range. Because the exclusion range is a multiple of a word,
1412 : // this is sufficient.
1413 0 : if (aStatus->mNoCheckRange) {
1414 0 : bool inExclusion = false;
1415 0 : aStatus->mNoCheckRange->IsPointInRange(beginNode, beginOffset,
1416 0 : &inExclusion);
1417 0 : if (inExclusion)
1418 0 : continue;
1419 : }
1420 :
1421 : // check spelling and add to selection if misspelled
1422 : bool isMisspelled;
1423 0 : aWordUtil.NormalizeWord(wordText);
1424 0 : rv = mSpellCheck->CheckCurrentWordNoSuggest(wordText.get(), &isMisspelled);
1425 0 : if (NS_FAILED(rv))
1426 0 : continue;
1427 :
1428 0 : if (isMisspelled) {
1429 : // misspelled words count extra toward the max
1430 0 : wordsSinceTimeCheck += MISSPELLED_WORD_COUNT_PENALTY;
1431 0 : AddRange(aSpellCheckSelection, wordRange);
1432 :
1433 0 : aStatus->mWordCount ++;
1434 0 : if (aStatus->mWordCount >= mMaxMisspellingsPerCheck ||
1435 0 : SpellCheckSelectionIsFull())
1436 : break;
1437 : }
1438 :
1439 : // see if we've run out of time, only check every N words for perf
1440 0 : if (wordsSinceTimeCheck >= INLINESPELL_TIMEOUT_CHECK_FREQUENCY) {
1441 0 : wordsSinceTimeCheck = 0;
1442 0 : if (PR_Now() > PRTime(beginTime + INLINESPELL_CHECK_TIMEOUT * PR_USEC_PER_MSEC)) {
1443 : // stop checking, our time limit has been exceeded
1444 :
1445 : // move the range to encompass the stuff that needs checking
1446 0 : rv = aStatus->mRange->SetStart(endNode, endOffset);
1447 0 : if (NS_FAILED(rv)) {
1448 : // The range might be unhappy because the beginning is after the
1449 : // end. This is possible when the requested end was in the middle
1450 : // of a word, just ignore this situation and assume we're done.
1451 0 : return NS_OK;
1452 : }
1453 0 : *aDoneChecking = false;
1454 0 : return NS_OK;
1455 : }
1456 : }
1457 : }
1458 :
1459 0 : return NS_OK;
1460 : }
1461 :
1462 : // mozInlineSpellChecker::ResumeCheck
1463 : //
1464 : // Called by the resume event when it fires. We will try to pick up where
1465 : // the last resume left off.
1466 :
1467 : nsresult
1468 0 : mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
1469 : {
1470 0 : if (aStatus->IsFullSpellCheck()) {
1471 : // Allow posting new spellcheck resume events from inside
1472 : // ResumeCheck, now that we're actually firing.
1473 0 : NS_ASSERTION(mFullSpellCheckScheduled,
1474 : "How could this be false? The full spell check is "
1475 : "calling us!!");
1476 0 : mFullSpellCheckScheduled = false;
1477 : }
1478 :
1479 0 : if (! mSpellCheck)
1480 0 : return NS_OK; // spell checking has been turned off
1481 :
1482 0 : nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
1483 0 : if (! editor)
1484 0 : return NS_OK; // editor is gone
1485 :
1486 0 : mozInlineSpellWordUtil wordUtil;
1487 0 : nsresult rv = wordUtil.Init(mEditor);
1488 0 : if (NS_FAILED(rv))
1489 0 : return NS_OK; // editor doesn't like us, don't assert
1490 :
1491 0 : nsCOMPtr<nsISelection> spellCheckSelection;
1492 0 : rv = GetSpellCheckSelection(getter_AddRefs(spellCheckSelection));
1493 0 : NS_ENSURE_SUCCESS(rv, rv);
1494 :
1495 0 : nsAutoString currentDictionary;
1496 0 : rv = mSpellCheck->GetCurrentDictionary(currentDictionary);
1497 0 : if (NS_FAILED(rv)) {
1498 : // no active dictionary
1499 : PRInt32 count;
1500 0 : spellCheckSelection->GetRangeCount(&count);
1501 0 : for (PRInt32 index = count - 1; index >= 0; index--) {
1502 0 : nsCOMPtr<nsIDOMRange> checkRange;
1503 0 : spellCheckSelection->GetRangeAt(index, getter_AddRefs(checkRange));
1504 0 : if (checkRange) {
1505 0 : RemoveRange(spellCheckSelection, checkRange);
1506 : }
1507 : }
1508 0 : return NS_OK;
1509 : }
1510 :
1511 0 : CleanupRangesInSelection(spellCheckSelection);
1512 :
1513 0 : rv = aStatus->FinishInitOnEvent(wordUtil);
1514 0 : NS_ENSURE_SUCCESS(rv, rv);
1515 0 : if (! aStatus->mRange)
1516 0 : return NS_OK; // empty range, nothing to do
1517 :
1518 0 : bool doneChecking = true;
1519 0 : if (aStatus->mOp == mozInlineSpellStatus::eOpSelection)
1520 0 : rv = DoSpellCheckSelection(wordUtil, spellCheckSelection, aStatus);
1521 : else
1522 0 : rv = DoSpellCheck(wordUtil, spellCheckSelection, aStatus, &doneChecking);
1523 0 : NS_ENSURE_SUCCESS(rv, rv);
1524 :
1525 0 : if (! doneChecking)
1526 0 : rv = ScheduleSpellCheck(*aStatus);
1527 0 : return rv;
1528 : }
1529 :
1530 : // mozInlineSpellChecker::IsPointInSelection
1531 : //
1532 : // Determines if a given (node,offset) point is inside the given
1533 : // selection. If so, the specific range of the selection that
1534 : // intersects is places in *aRange. (There may be multiple disjoint
1535 : // ranges in a selection.)
1536 : //
1537 : // If there is no intersection, *aRange will be NULL.
1538 :
1539 : nsresult
1540 0 : mozInlineSpellChecker::IsPointInSelection(nsISelection *aSelection,
1541 : nsIDOMNode *aNode,
1542 : PRInt32 aOffset,
1543 : nsIDOMRange **aRange)
1544 : {
1545 0 : *aRange = nsnull;
1546 :
1547 0 : nsCOMPtr<nsISelectionPrivate> privSel(do_QueryInterface(aSelection));
1548 :
1549 0 : nsTArray<nsRange*> ranges;
1550 0 : nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
1551 0 : nsresult rv = privSel->GetRangesForIntervalArray(node, aOffset, node, aOffset,
1552 0 : true, &ranges);
1553 0 : NS_ENSURE_SUCCESS(rv, rv);
1554 :
1555 0 : if (ranges.Length() == 0)
1556 0 : return NS_OK; // no matches
1557 :
1558 : // there may be more than one range returned, and we don't know what do
1559 : // do with that, so just get the first one
1560 0 : NS_ADDREF(*aRange = ranges[0]);
1561 0 : return NS_OK;
1562 : }
1563 :
1564 : nsresult
1565 0 : mozInlineSpellChecker::CleanupRangesInSelection(nsISelection *aSelection)
1566 : {
1567 : // integrity check - remove ranges that have collapsed to nothing. This
1568 : // can happen if the node containing a highlighted word was removed.
1569 0 : NS_ENSURE_ARG_POINTER(aSelection);
1570 :
1571 : PRInt32 count;
1572 0 : aSelection->GetRangeCount(&count);
1573 :
1574 0 : for (PRInt32 index = 0; index < count; index++)
1575 : {
1576 0 : nsCOMPtr<nsIDOMRange> checkRange;
1577 0 : aSelection->GetRangeAt(index, getter_AddRefs(checkRange));
1578 :
1579 0 : if (checkRange)
1580 : {
1581 : bool collapsed;
1582 0 : checkRange->GetCollapsed(&collapsed);
1583 0 : if (collapsed)
1584 : {
1585 0 : RemoveRange(aSelection, checkRange);
1586 0 : index--;
1587 0 : count--;
1588 : }
1589 : }
1590 : }
1591 :
1592 0 : return NS_OK;
1593 : }
1594 :
1595 :
1596 : // mozInlineSpellChecker::RemoveRange
1597 : //
1598 : // For performance reasons, we have an upper bound on the number of word
1599 : // ranges in the spell check selection. When removing a range from the
1600 : // selection, we need to decrement mNumWordsInSpellSelection
1601 :
1602 : nsresult
1603 0 : mozInlineSpellChecker::RemoveRange(nsISelection* aSpellCheckSelection,
1604 : nsIDOMRange* aRange)
1605 : {
1606 0 : NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1607 0 : NS_ENSURE_ARG_POINTER(aRange);
1608 :
1609 0 : nsresult rv = aSpellCheckSelection->RemoveRange(aRange);
1610 0 : if (NS_SUCCEEDED(rv) && mNumWordsInSpellSelection)
1611 0 : mNumWordsInSpellSelection--;
1612 :
1613 0 : return rv;
1614 : }
1615 :
1616 :
1617 : // mozInlineSpellChecker::AddRange
1618 : //
1619 : // For performance reasons, we have an upper bound on the number of word
1620 : // ranges we'll add to the spell check selection. Once we reach that upper
1621 : // bound, stop adding the ranges
1622 :
1623 : nsresult
1624 0 : mozInlineSpellChecker::AddRange(nsISelection* aSpellCheckSelection,
1625 : nsIDOMRange* aRange)
1626 : {
1627 0 : NS_ENSURE_ARG_POINTER(aSpellCheckSelection);
1628 0 : NS_ENSURE_ARG_POINTER(aRange);
1629 :
1630 0 : nsresult rv = NS_OK;
1631 :
1632 0 : if (!SpellCheckSelectionIsFull())
1633 : {
1634 0 : rv = aSpellCheckSelection->AddRange(aRange);
1635 0 : if (NS_SUCCEEDED(rv))
1636 0 : mNumWordsInSpellSelection++;
1637 : }
1638 :
1639 0 : return rv;
1640 : }
1641 :
1642 0 : nsresult mozInlineSpellChecker::GetSpellCheckSelection(nsISelection ** aSpellCheckSelection)
1643 : {
1644 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1645 0 : NS_ENSURE_TRUE(editor, NS_ERROR_NULL_POINTER);
1646 :
1647 0 : nsCOMPtr<nsISelectionController> selcon;
1648 0 : nsresult rv = editor->GetSelectionController(getter_AddRefs(selcon));
1649 0 : NS_ENSURE_SUCCESS(rv, rv);
1650 :
1651 0 : nsCOMPtr<nsISelection> spellCheckSelection;
1652 0 : return selcon->GetSelection(nsISelectionController::SELECTION_SPELLCHECK, aSpellCheckSelection);
1653 : }
1654 :
1655 0 : nsresult mozInlineSpellChecker::SaveCurrentSelectionPosition()
1656 : {
1657 0 : nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
1658 0 : NS_ENSURE_TRUE(editor, NS_OK);
1659 :
1660 : // figure out the old caret position based on the current selection
1661 0 : nsCOMPtr<nsISelection> selection;
1662 0 : nsresult rv = editor->GetSelection(getter_AddRefs(selection));
1663 0 : NS_ENSURE_SUCCESS(rv, rv);
1664 :
1665 0 : rv = selection->GetFocusNode(getter_AddRefs(mCurrentSelectionAnchorNode));
1666 0 : NS_ENSURE_SUCCESS(rv, rv);
1667 :
1668 0 : selection->GetFocusOffset(&mCurrentSelectionOffset);
1669 :
1670 0 : return NS_OK;
1671 : }
1672 :
1673 : // This is a copy of nsContentUtils::ContentIsDescendantOf. Another crime
1674 : // for XPCOM's rap sheet
1675 : bool // static
1676 0 : ContentIsDescendantOf(nsINode* aPossibleDescendant,
1677 : nsINode* aPossibleAncestor)
1678 : {
1679 0 : NS_PRECONDITION(aPossibleDescendant, "The possible descendant is null!");
1680 0 : NS_PRECONDITION(aPossibleAncestor, "The possible ancestor is null!");
1681 :
1682 0 : do {
1683 0 : if (aPossibleDescendant == aPossibleAncestor)
1684 0 : return true;
1685 0 : aPossibleDescendant = aPossibleDescendant->GetNodeParent();
1686 : } while (aPossibleDescendant);
1687 :
1688 0 : return false;
1689 : }
1690 :
1691 : // mozInlineSpellChecker::HandleNavigationEvent
1692 : //
1693 : // Acts upon mouse clicks and keyboard navigation changes, spell checking
1694 : // the previous word if the new navigation location moves us to another
1695 : // word.
1696 : //
1697 : // This is complicated by the fact that our mouse events are happening after
1698 : // selection has been changed to account for the mouse click. But keyboard
1699 : // events are happening before the caret selection has changed. Working
1700 : // around this by letting keyboard events setting forceWordSpellCheck to
1701 : // true. aNewPositionOffset also tries to work around this for the
1702 : // DOM_VK_RIGHT and DOM_VK_LEFT cases.
1703 :
1704 : nsresult
1705 0 : mozInlineSpellChecker::HandleNavigationEvent(bool aForceWordSpellCheck,
1706 : PRInt32 aNewPositionOffset)
1707 : {
1708 : nsresult rv;
1709 :
1710 : // If we already handled the navigation event and there is no possibility
1711 : // anything has changed since then, we don't have to do anything. This
1712 : // optimization makes a noticeable difference when you hold down a navigation
1713 : // key like Page Down.
1714 0 : if (! mNeedsCheckAfterNavigation)
1715 0 : return NS_OK;
1716 :
1717 0 : nsCOMPtr<nsIDOMNode> currentAnchorNode = mCurrentSelectionAnchorNode;
1718 0 : PRInt32 currentAnchorOffset = mCurrentSelectionOffset;
1719 :
1720 : // now remember the new focus position resulting from the event
1721 0 : rv = SaveCurrentSelectionPosition();
1722 0 : NS_ENSURE_SUCCESS(rv, rv);
1723 :
1724 : bool shouldPost;
1725 0 : mozInlineSpellStatus status(this);
1726 : rv = status.InitForNavigation(aForceWordSpellCheck, aNewPositionOffset,
1727 : currentAnchorNode, currentAnchorOffset,
1728 : mCurrentSelectionAnchorNode, mCurrentSelectionOffset,
1729 0 : &shouldPost);
1730 0 : NS_ENSURE_SUCCESS(rv, rv);
1731 0 : if (shouldPost) {
1732 0 : rv = ScheduleSpellCheck(status);
1733 0 : NS_ENSURE_SUCCESS(rv, rv);
1734 : }
1735 :
1736 0 : return NS_OK;
1737 : }
1738 :
1739 0 : NS_IMETHODIMP mozInlineSpellChecker::HandleEvent(nsIDOMEvent* aEvent)
1740 : {
1741 0 : nsAutoString eventType;
1742 0 : aEvent->GetType(eventType);
1743 :
1744 0 : if (eventType.EqualsLiteral("blur")) {
1745 0 : return Blur(aEvent);
1746 : }
1747 0 : if (eventType.EqualsLiteral("click")) {
1748 0 : return MouseClick(aEvent);
1749 : }
1750 0 : if (eventType.EqualsLiteral("keypress")) {
1751 0 : return KeyPress(aEvent);
1752 : }
1753 :
1754 0 : return NS_OK;
1755 : }
1756 :
1757 0 : nsresult mozInlineSpellChecker::Blur(nsIDOMEvent* aEvent)
1758 : {
1759 : // force spellcheck on blur, for instance when tabbing out of a textbox
1760 0 : HandleNavigationEvent(true);
1761 0 : return NS_OK;
1762 : }
1763 :
1764 0 : nsresult mozInlineSpellChecker::MouseClick(nsIDOMEvent *aMouseEvent)
1765 : {
1766 0 : nsCOMPtr<nsIDOMMouseEvent>mouseEvent = do_QueryInterface(aMouseEvent);
1767 0 : NS_ENSURE_TRUE(mouseEvent, NS_OK);
1768 :
1769 : // ignore any errors from HandleNavigationEvent as we don't want to prevent
1770 : // anyone else from seeing this event.
1771 : PRUint16 button;
1772 0 : mouseEvent->GetButton(&button);
1773 0 : HandleNavigationEvent(button != 0);
1774 0 : return NS_OK;
1775 : }
1776 :
1777 0 : nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
1778 : {
1779 0 : nsCOMPtr<nsIDOMKeyEvent>keyEvent = do_QueryInterface(aKeyEvent);
1780 0 : NS_ENSURE_TRUE(keyEvent, NS_OK);
1781 :
1782 : PRUint32 keyCode;
1783 0 : keyEvent->GetKeyCode(&keyCode);
1784 :
1785 : // we only care about navigation keys that moved selection
1786 0 : switch (keyCode)
1787 : {
1788 : case nsIDOMKeyEvent::DOM_VK_RIGHT:
1789 : case nsIDOMKeyEvent::DOM_VK_LEFT:
1790 0 : HandleNavigationEvent(false, keyCode == nsIDOMKeyEvent::DOM_VK_RIGHT ? 1 : -1);
1791 0 : break;
1792 : case nsIDOMKeyEvent::DOM_VK_UP:
1793 : case nsIDOMKeyEvent::DOM_VK_DOWN:
1794 : case nsIDOMKeyEvent::DOM_VK_HOME:
1795 : case nsIDOMKeyEvent::DOM_VK_END:
1796 : case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
1797 : case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
1798 0 : HandleNavigationEvent(true /* force a spelling correction */);
1799 0 : break;
1800 : }
1801 :
1802 0 : return NS_OK;
1803 : }
1804 :
1805 0 : NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
1806 : {
1807 0 : if (!mSpellCheck) {
1808 0 : return NS_OK;
1809 : }
1810 :
1811 0 : nsAutoString previousDictionary;
1812 0 : if (NS_FAILED(mSpellCheck->GetCurrentDictionary(previousDictionary))) {
1813 0 : previousDictionary.Truncate();
1814 : }
1815 :
1816 0 : nsresult rv = mSpellCheck->UpdateCurrentDictionary();
1817 :
1818 0 : nsAutoString currentDictionary;
1819 0 : if (NS_FAILED(mSpellCheck->GetCurrentDictionary(currentDictionary))) {
1820 0 : currentDictionary.Truncate();
1821 : }
1822 :
1823 0 : if (!previousDictionary.Equals(currentDictionary)) {
1824 0 : rv = SpellCheckRange(nsnull);
1825 : }
1826 :
1827 0 : return rv;
1828 4392 : }
|