Bug 856270 - Update spell checking to use nsIContentPrefService2 (part 1: core changes). r=ehsan, sr=gavin

This commit is contained in:
Drew Willcoxon 2013-06-05 17:07:54 -07:00
parent 9a0b22c391
commit 7b004ec642
9 changed files with 576 additions and 130 deletions

View File

@ -19,6 +19,7 @@
#include "nsIChromeRegistry.h" // for nsIXULChromeRegistry
#include "nsIContent.h" // for nsIContent
#include "nsIContentPrefService.h" // for nsIContentPrefService, etc
#include "nsIContentPrefService2.h" // for nsIContentPrefService2, etc
#include "nsIDOMDocument.h" // for nsIDOMDocument
#include "nsIDOMElement.h" // for nsIDOMElement
#include "nsIDOMRange.h" // for nsIDOMRange
@ -62,34 +63,11 @@ class UpdateDictionnaryHolder {
#define CPS_PREF_NAME NS_LITERAL_STRING("spellcheck.lang")
class LastDictionary MOZ_FINAL {
public:
/**
* Store current dictionary for editor document url. Use content pref
* service.
* Gets the URI of aEditor's document.
*/
NS_IMETHOD StoreCurrentDictionary(nsIEditor* aEditor, const nsAString& aDictionary);
/**
* Get last stored current dictionary for editor document url.
*/
NS_IMETHOD FetchLastDictionary(nsIEditor* aEditor, nsAString& aDictionary);
/**
* Forget last current dictionary stored for editor document url.
*/
NS_IMETHOD ClearCurrentDictionary(nsIEditor* aEditor);
/**
* get uri of editor's document.
*
*/
static nsresult GetDocumentURI(nsIEditor* aEditor, nsIURI * *aURI);
};
// static
nsresult
LastDictionary::GetDocumentURI(nsIEditor* aEditor, nsIURI * *aURI)
static nsresult
GetDocumentURI(nsIEditor* aEditor, nsIURI * *aURI)
{
NS_ENSURE_ARG_POINTER(aEditor);
NS_ENSURE_ARG_POINTER(aURI);
@ -124,8 +102,55 @@ GetLoadContext(nsIEditor* aEditor)
return loadContext.forget();
}
/**
* Fetches the dictionary stored in content prefs and maintains state during the
* fetch, which is asynchronous.
*/
class DictionaryFetcher MOZ_FINAL : public nsIContentPrefCallback2
{
public:
NS_DECL_ISUPPORTS
DictionaryFetcher(nsEditorSpellCheck* aSpellCheck,
nsIEditorSpellCheckCallback* aCallback,
uint32_t aGroup)
: mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}
NS_IMETHOD Fetch(nsIEditor* aEditor);
NS_IMETHOD HandleResult(nsIContentPref* aPref)
{
nsCOMPtr<nsIVariant> value;
nsresult rv = aPref->GetValue(getter_AddRefs(value));
NS_ENSURE_SUCCESS(rv, rv);
value->GetAsAString(mDictionary);
return NS_OK;
}
NS_IMETHOD HandleCompletion(uint16_t reason)
{
mSpellCheck->DictionaryFetched(this);
return NS_OK;
}
NS_IMETHOD HandleError(nsresult error)
{
return NS_OK;
}
nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
uint32_t mGroup;
nsString mRootContentLang;
nsString mRootDocContentLang;
nsString mDictionary;
private:
nsCOMPtr<nsEditorSpellCheck> mSpellCheck;
};
NS_IMPL_ISUPPORTS1(DictionaryFetcher, nsIContentPrefCallback2)
NS_IMETHODIMP
LastDictionary::FetchLastDictionary(nsIEditor* aEditor, nsAString& aDictionary)
DictionaryFetcher::Fetch(nsIEditor* aEditor)
{
NS_ENSURE_ARG_POINTER(aEditor);
@ -135,29 +160,28 @@ LastDictionary::FetchLastDictionary(nsIEditor* aEditor, nsAString& aDictionary)
rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContentPrefService> contentPrefService =
nsAutoCString docUriSpec;
rv = docUri->GetSpec(docUriSpec);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_AVAILABLE);
nsCOMPtr<nsIWritableVariant> uri = do_CreateInstance(NS_VARIANT_CONTRACTID);
NS_ENSURE_TRUE(uri, NS_ERROR_OUT_OF_MEMORY);
uri->SetAsISupports(docUri);
nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
bool hasPref;
if (NS_SUCCEEDED(contentPrefService->HasPref(uri, CPS_PREF_NAME, loadContext, &hasPref)) && hasPref) {
nsCOMPtr<nsIVariant> pref;
contentPrefService->GetPref(uri, CPS_PREF_NAME, loadContext, nullptr, getter_AddRefs(pref));
pref->GetAsAString(aDictionary);
} else {
aDictionary.Truncate();
}
rv = contentPrefService->GetByDomainAndName(NS_ConvertUTF8toUTF16(docUriSpec),
CPS_PREF_NAME, loadContext,
this);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMETHODIMP
LastDictionary::StoreCurrentDictionary(nsIEditor* aEditor, const nsAString& aDictionary)
/**
* Stores the current dictionary for aEditor's document URL.
*/
static nsresult
StoreCurrentDictionary(nsIEditor* aEditor, const nsAString& aDictionary)
{
NS_ENSURE_ARG_POINTER(aEditor);
@ -167,24 +191,29 @@ LastDictionary::StoreCurrentDictionary(nsIEditor* aEditor, const nsAString& aDic
rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIWritableVariant> uri = do_CreateInstance(NS_VARIANT_CONTRACTID);
NS_ENSURE_TRUE(uri, NS_ERROR_OUT_OF_MEMORY);
uri->SetAsISupports(docUri);
nsAutoCString docUriSpec;
rv = docUri->GetSpec(docUriSpec);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIWritableVariant> prefValue = do_CreateInstance(NS_VARIANT_CONTRACTID);
NS_ENSURE_TRUE(prefValue, NS_ERROR_OUT_OF_MEMORY);
prefValue->SetAsAString(aDictionary);
nsCOMPtr<nsIContentPrefService> contentPrefService =
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
return contentPrefService->SetPref(uri, CPS_PREF_NAME, prefValue, loadContext);
return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
CPS_PREF_NAME, prefValue, loadContext,
nullptr);
}
NS_IMETHODIMP
LastDictionary::ClearCurrentDictionary(nsIEditor* aEditor)
/**
* Forgets the current dictionary stored for aEditor's document URL.
*/
static nsresult
ClearCurrentDictionary(nsIEditor* aEditor)
{
NS_ENSURE_ARG_POINTER(aEditor);
@ -194,20 +223,19 @@ LastDictionary::ClearCurrentDictionary(nsIEditor* aEditor)
rv = GetDocumentURI(aEditor, getter_AddRefs(docUri));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIWritableVariant> uri = do_CreateInstance(NS_VARIANT_CONTRACTID);
NS_ENSURE_TRUE(uri, NS_ERROR_OUT_OF_MEMORY);
uri->SetAsISupports(docUri);
nsAutoCString docUriSpec;
rv = docUri->GetSpec(docUriSpec);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContentPrefService> contentPrefService =
nsCOMPtr<nsIContentPrefService2> contentPrefService =
do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
nsCOMPtr<nsILoadContext> loadContext = GetLoadContext(aEditor);
return contentPrefService->RemovePref(uri, CPS_PREF_NAME, loadContext);
return contentPrefService->RemoveByDomainAndName(
NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME, loadContext, nullptr);
}
LastDictionary* nsEditorSpellCheck::gDictionaryStore = nullptr;
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsEditorSpellCheck)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsEditorSpellCheck)
@ -226,6 +254,7 @@ nsEditorSpellCheck::nsEditorSpellCheck()
: mSuggestedWordIndex(0)
, mDictionaryIndex(0)
, mEditor(nullptr)
, mDictionaryFetcherGroup(0)
, mUpdateDictionaryRunning(false)
{
}
@ -261,19 +290,39 @@ nsEditorSpellCheck::CanSpellCheck(bool* _retval)
return NS_OK;
}
// Instances of this class can be used as either runnables or RAII helpers.
class CallbackCaller MOZ_FINAL : public nsRunnable
{
public:
explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
: mCallback(aCallback) {}
~CallbackCaller()
{
Run();
}
NS_IMETHOD Run()
{
if (mCallback) {
mCallback->EditorSpellCheckDone();
mCallback = nullptr;
}
return NS_OK;
}
private:
nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
};
NS_IMETHODIMP
nsEditorSpellCheck::InitSpellChecker(nsIEditor* aEditor, bool aEnableSelectionChecking)
nsEditorSpellCheck::InitSpellChecker(nsIEditor* aEditor, bool aEnableSelectionChecking, nsIEditorSpellCheckCallback* aCallback)
{
NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
mEditor = aEditor;
nsresult rv;
if (!gDictionaryStore) {
gDictionaryStore = new LastDictionary();
}
// We can spell check with any editor type
nsCOMPtr<nsITextServicesDocument>tsDoc =
do_CreateInstance("@mozilla.org/textservices/textservicesdocument;1", &rv);
@ -345,7 +394,17 @@ nsEditorSpellCheck::InitSpellChecker(nsIEditor* aEditor, bool aEnableSelectionCh
// do not fail if UpdateCurrentDictionary fails because this method may
// succeed later.
UpdateCurrentDictionary();
rv = UpdateCurrentDictionary(aCallback);
if (NS_FAILED(rv) && aCallback) {
// However, if it does fail, we still need to call the callback since we
// discard the failure. Do it asynchronously so that the caller is always
// guaranteed async behavior.
nsRefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
NS_ENSURE_STATE(caller);
rv = NS_DispatchToMainThread(caller);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
@ -529,8 +588,14 @@ nsEditorSpellCheck::SetCurrentDictionary(const nsAString& aDictionary)
nsRefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
// The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
// UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
// is on the stack.
if (!mUpdateDictionaryRunning) {
// Ignore pending dictionary fetchers by increasing this number.
mDictionaryFetcherGroup++;
nsDefaultStringComparator comparator;
nsAutoString langCode;
int32_t dashIdx = aDictionary.FindChar('-');
@ -543,11 +608,11 @@ nsEditorSpellCheck::SetCurrentDictionary(const nsAString& aDictionary)
if (mPreferredLang.IsEmpty() || !nsStyleUtil::DashMatchCompare(mPreferredLang, langCode, comparator)) {
// When user sets dictionary manually, we store this value associated
// with editor url.
gDictionaryStore->StoreCurrentDictionary(mEditor, aDictionary);
StoreCurrentDictionary(mEditor, aDictionary);
} else {
// If user sets a dictionary matching (even partially), lang defined by
// document, we consider content pref has been canceled, and we clear it.
gDictionaryStore->ClearCurrentDictionary(mEditor);
ClearCurrentDictionary(mEditor);
}
// Also store it in as a preference. It will be used as a default value
@ -613,14 +678,12 @@ nsEditorSpellCheck::DeleteSuggestedWordList()
}
NS_IMETHODIMP
nsEditorSpellCheck::UpdateCurrentDictionary()
nsEditorSpellCheck::UpdateCurrentDictionary(nsIEditorSpellCheckCallback* aCallback)
{
nsresult rv;
nsRefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
UpdateDictionnaryHolder holder(this);
// Get language with html5 algorithm
nsCOMPtr<nsIContent> rootContent;
nsCOMPtr<nsIHTMLEditor> htmlEditor = do_QueryInterface(mEditor);
@ -634,27 +697,53 @@ nsEditorSpellCheck::UpdateCurrentDictionary()
}
NS_ENSURE_TRUE(rootContent, NS_ERROR_FAILURE);
mPreferredLang.Truncate();
rootContent->GetLang(mPreferredLang);
DictionaryFetcher* fetcher = new DictionaryFetcher(this, aCallback,
mDictionaryFetcherGroup);
rootContent->GetLang(fetcher->mRootContentLang);
nsCOMPtr<nsIDocument> doc = rootContent->GetCurrentDoc();
NS_ENSURE_STATE(doc);
doc->GetContentLanguage(fetcher->mRootDocContentLang);
// Tell the spellchecker what dictionary to use:
rv = fetcher->Fetch(mEditor);
NS_ENSURE_SUCCESS(rv, rv);
// First try to get dictionary from content prefs. If we have one, do not got
return NS_OK;
}
nsresult
nsEditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher)
{
nsRefPtr<nsEditorSpellCheck> kungFuDeathGrip = this;
nsresult rv = NS_OK;
// Important: declare the holder after the callback caller so that the former
// is destructed first so that it's not active when the callback is called.
CallbackCaller callbackCaller(aFetcher->mCallback);
UpdateDictionnaryHolder holder(this);
if (aFetcher->mGroup < mDictionaryFetcherGroup) {
// SetCurrentDictionary was called after the fetch started. Don't overwrite
// that dictionary with the fetched one.
return NS_OK;
}
mPreferredLang.Assign(aFetcher->mRootContentLang);
// If we successfully fetched a dictionary from content prefs, do not go
// further. Use this exact dictionary.
nsAutoString dictName;
rv = gDictionaryStore->FetchLastDictionary(mEditor, dictName);
if (NS_SUCCEEDED(rv) && !dictName.IsEmpty()) {
dictName.Assign(aFetcher->mDictionary);
if (!dictName.IsEmpty()) {
if (NS_FAILED(SetCurrentDictionary(dictName))) {
// may be dictionary was uninstalled ?
gDictionaryStore->ClearCurrentDictionary(mEditor);
ClearCurrentDictionary(mEditor);
}
return NS_OK;
}
if (mPreferredLang.IsEmpty()) {
nsCOMPtr<nsIDocument> doc = rootContent->GetCurrentDoc();
NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
doc->GetContentLanguage(mPreferredLang);
mPreferredLang.Assign(aFetcher->mRootDocContentLang);
}
// Then, try to use language computed from element
@ -781,8 +870,3 @@ nsEditorSpellCheck::UpdateCurrentDictionary()
return NS_OK;
}
void
nsEditorSpellCheck::ShutDown() {
delete gDictionaryStore;
}

View File

@ -25,10 +25,12 @@ class nsITextServicesFilter;
{ 0x93, 0x9a, 0xec, 0x63, 0x51, 0xee, 0xa0, 0xcc }\
}
class LastDictionary;
class DictionaryFetcher;
class nsEditorSpellCheck : public nsIEditorSpellCheck
{
friend class DictionaryFetcher;
public:
nsEditorSpellCheck();
virtual ~nsEditorSpellCheck();
@ -39,10 +41,6 @@ public:
/* Declare all methods in the nsIEditorSpellCheck interface */
NS_DECL_NSIEDITORSPELLCHECK
static LastDictionary* gDictionaryStore;
static void ShutDown();
protected:
nsCOMPtr<nsISpellChecker> mSpellChecker;
@ -61,8 +59,12 @@ protected:
nsString mPreferredLang;
uint32_t mDictionaryFetcherGroup;
bool mUpdateDictionaryRunning;
nsresult DictionaryFetched(DictionaryFetcher* aFetchState);
public:
void BeginUpdateDictionary() { mUpdateDictionaryRunning = true ;}
void EndUpdateDictionary() { mUpdateDictionaryRunning = false ;}

View File

@ -24,6 +24,7 @@ MOCHITEST_CHROME_FILES = \
test_bug434998.xul \
test_bug338427.html \
test_bug678842.html \
test_async_UpdateCurrentDictionary.html \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,72 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=856270
-->
<head>
<title>Test for Bug 856270 - Async UpdateCurrentDictionary</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=856270">Mozilla Bug 856270</a>
<p id="display"></p>
<div id="content">
<textarea id="editor" spellcheck="true"></textarea>
</div>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
addLoadEvent(start);
function start() {
Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
var textarea = document.getElementById("editor");
textarea.focus();
onSpellCheck(textarea, function () {
var isc = textarea.editor.getInlineSpellChecker(false);
ok(isc, "Inline spell checker should exist after focus and spell check");
var sc = isc.spellChecker;
isnot(sc.GetCurrentDictionary(), lang,
"Current dictionary should not be set yet.");
// First, set the lang attribute on the textarea, call Update, and make
// sure the spell checker's language was updated appropriately.
var lang = "en-US";
textarea.setAttribute("lang", lang);
sc.UpdateCurrentDictionary(function () {
is(sc.GetCurrentDictionary(), lang,
"UpdateCurrentDictionary should set the current dictionary.");
// Second, make some Update calls, but then do a Set. The Set should
// effectively cancel the Updates, but the Updates' callbacks should be
// called nonetheless.
var numCalls = 3;
for (var i = 0; i < numCalls; i++) {
sc.UpdateCurrentDictionary(function () {
is(sc.GetCurrentDictionary(), "",
"No dictionary should be active after Update.");
if (--numCalls == 0)
SimpleTest.finish();
});
}
try {
sc.SetCurrentDictionary("testing-XX");
}
catch (err) {
// Set throws NS_ERROR_NOT_AVAILABLE because "testing-XX" isn't really
// an available dictionary.
}
is(sc.GetCurrentDictionary(), "",
"No dictionary should be active after Set.");
});
});
}
</script>
</pre>
</body>
</html>

View File

@ -7,8 +7,9 @@
interface nsIEditor;
interface nsITextServicesFilter;
interface nsIEditorSpellCheckCallback;
[scriptable, uuid(334946c3-0e93-4aac-b662-e1b56f95d68b)]
[scriptable, uuid(dd32ef3b-a7d8-43d1-9617-5f2dddbe29eb)]
interface nsIEditorSpellCheck : nsISupports
{
@ -30,9 +31,11 @@ interface nsIEditorSpellCheck : nsISupports
* set means that we only want to check the current selection in the editor,
* (this controls the behavior of GetNextMisspelledWord). For spellchecking
* clients with no modal UI (such as inline spellcheckers), this flag doesn't
* matter
* matter. Initialization is asynchronous and is not complete until the given
* callback is called.
*/
void InitSpellChecker(in nsIEditor editor, in boolean enableSelectionChecking);
void InitSpellChecker(in nsIEditor editor, in boolean enableSelectionChecking,
[optional] in nsIEditorSpellCheckCallback callback);
/**
* When interactively spell checking the document, this will return the
@ -156,8 +159,15 @@ interface nsIEditorSpellCheck : nsISupports
/**
* Update the dictionary in use to be sure it corresponds to what the editor
* needs.
* needs. The update is asynchronous and is not complete until the given
* callback is called.
*/
void UpdateCurrentDictionary();
void UpdateCurrentDictionary([optional] in nsIEditorSpellCheckCallback callback);
};
[scriptable, function, uuid(5f0a4bab-8538-4074-89d3-2f0e866a1c0b)]
interface nsIEditorSpellCheckCallback : nsISupports
{
void editorSpellCheckDone();
};

View File

@ -10,8 +10,7 @@ interface nsISelection;
interface nsIEditor;
interface nsIEditorSpellCheck;
[scriptable, uuid(df635540-d073-47b8-8678-18776130691d)]
[scriptable, uuid(b7b7a77c-40c4-4196-b0b7-b0338243b3fe)]
interface nsIInlineSpellChecker : nsISupports
{
readonly attribute nsIEditorSpellCheck spellChecker;
@ -40,6 +39,8 @@ interface nsIInlineSpellChecker : nsISupports
void ignoreWord(in AString aWord);
void ignoreWords([array, size_is(aCount)] in wstring aWordsToIgnore, in unsigned long aCount);
void updateCurrentDictionary();
readonly attribute boolean spellCheckPending;
};
%{C++

View File

@ -62,6 +62,8 @@
#include "nsRange.h"
#include "nsContentUtils.h"
#include "nsEditor.h"
#include "mozilla/Services.h"
#include "nsIObserverService.h"
using namespace mozilla::dom;
@ -84,6 +86,10 @@ using namespace mozilla::dom;
// since this just controls how often we check the current time.
#define MISSPELLED_WORD_COUNT_PENALTY 4
// These notifications are broadcast when spell check starts and ends. STARTED
// must always be followed by ENDED.
#define INLINESPELL_STARTED_TOPIC "inlineSpellChecker-spellCheck-started"
#define INLINESPELL_ENDED_TOPIC "inlineSpellChecker-spellCheck-ended"
static bool ContentIsDescendantOf(nsINode* aPossibleDescendant,
nsINode* aPossibleAncestor);
@ -455,8 +461,10 @@ mozInlineSpellStatus::PositionToCollapsedRange(nsIDOMDocument* aDocument,
class mozInlineSpellResume : public nsRunnable
{
public:
mozInlineSpellResume(const mozInlineSpellStatus& aStatus) : mStatus(aStatus) {}
mozInlineSpellStatus mStatus;
mozInlineSpellResume(const mozInlineSpellStatus& aStatus,
uint32_t aDisabledAsyncToken)
: mDisabledAsyncToken(aDisabledAsyncToken), mStatus(aStatus) {}
nsresult Post()
{
return NS_DispatchToMainThread(this);
@ -464,9 +472,17 @@ public:
NS_IMETHOD Run()
{
// Discard the resumption if the spell checker was disabled after the
// resumption was scheduled.
if (mDisabledAsyncToken == mStatus.mSpellChecker->mDisabledAsyncToken) {
mStatus.mSpellChecker->ResumeCheck(&mStatus);
}
return NS_OK;
}
private:
uint32_t mDisabledAsyncToken;
mozInlineSpellStatus mStatus;
};
@ -494,6 +510,9 @@ mozInlineSpellChecker::SpellCheckingState
mozInlineSpellChecker::mozInlineSpellChecker() :
mNumWordsInSpellSelection(0),
mMaxNumWordsInSpellSelection(250),
mNumPendingSpellChecks(0),
mNumPendingUpdateCurrentDictionary(0),
mDisabledAsyncToken(0),
mNeedsCheckAfterNavigation(false),
mFullSpellCheckScheduled(false)
{
@ -650,10 +669,34 @@ NS_IMETHODIMP
mozInlineSpellChecker::GetEnableRealTimeSpell(bool* aEnabled)
{
NS_ENSURE_ARG_POINTER(aEnabled);
*aEnabled = mSpellCheck != nullptr;
*aEnabled = mSpellCheck != nullptr || mPendingSpellCheck != nullptr;
return NS_OK;
}
// Used as the nsIEditorSpellCheck::InitSpellChecker callback.
class InitEditorSpellCheckCallback MOZ_FINAL : public nsIEditorSpellCheckCallback
{
public:
NS_DECL_ISUPPORTS
explicit InitEditorSpellCheckCallback(mozInlineSpellChecker* aSpellChecker)
: mSpellChecker(aSpellChecker) {}
NS_IMETHOD EditorSpellCheckDone()
{
return mSpellChecker ? mSpellChecker->EditorSpellCheckInited() : NS_OK;
}
void Cancel()
{
mSpellChecker = nullptr;
}
private:
nsRefPtr<mozInlineSpellChecker> mSpellChecker;
};
NS_IMPL_ISUPPORTS1(InitEditorSpellCheckCallback, nsIEditorSpellCheckCallback)
// mozInlineSpellChecker::SetEnableRealTimeSpell
NS_IMETHODIMP
@ -661,26 +704,106 @@ mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
{
if (!aEnabled) {
mSpellCheck = nullptr;
return Cleanup(false);
// Hold on to mEditor since Cleanup nulls it out. See below.
nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
nsresult rv = Cleanup(false);
// Notify ENDED observers now. If we wait to notify as we normally do when
// these async operations finish, then in the meantime the editor may create
// another inline spell checker and cause more STARTED and ENDED
// notifications to be broadcast. Interleaved notifications for the same
// editor but different inline spell checkers could easily confuse
// observers. They may receive two consecutive STARTED notifications for
// example, which we guarantee will not happen. Plus, mEditor must always
// be passed to observers. If we wait to notify, we'd have to hold on to
// mEditor because Cleanup nulls it out.
if (mPendingSpellCheck) {
// Cancel the pending editor spell checker initialization.
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback->Cancel();
mPendingInitEditorSpellCheckCallback = nullptr;
ChangeNumPendingSpellChecks(-1, editor);
}
if (!mSpellCheck) {
nsresult res = NS_OK;
nsCOMPtr<nsIEditorSpellCheck> spellchecker = do_CreateInstance("@mozilla.org/editor/editorspellchecker;1", &res);
if (NS_SUCCEEDED(res) && spellchecker)
{
nsCOMPtr<nsITextServicesFilter> filter = do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1", &res);
spellchecker->SetFilter(filter);
nsCOMPtr<nsIEditor> editor (do_QueryReferent(mEditor));
res = spellchecker->InitSpellChecker(editor, false);
NS_ENSURE_SUCCESS(res, res);
// Increment this token so that pending UpdateCurrentDictionary calls and
// scheduled spell checks are discarded when they finish.
mDisabledAsyncToken++;
mSpellCheck = spellchecker;
if (mNumPendingUpdateCurrentDictionary > 0) {
// Account for pending UpdateCurrentDictionary calls.
ChangeNumPendingSpellChecks(-mNumPendingUpdateCurrentDictionary, editor);
mNumPendingUpdateCurrentDictionary = 0;
}
if (mNumPendingSpellChecks > 0) {
// If mNumPendingSpellChecks is still > 0 at this point, the remainder is
// pending scheduled spell checks.
ChangeNumPendingSpellChecks(-mNumPendingSpellChecks, editor);
}
return rv;
}
if (mSpellCheck) {
// spellcheck the current contents. SpellCheckRange doesn't supply a created
// range to DoSpellCheck, which in our case is the entire range. But this
// optimization doesn't matter because there is nothing in the spellcheck
// selection when starting, which triggers a better optimization.
return SpellCheckRange(nullptr);
}
if (mPendingSpellCheck) {
// The editor spell checker is already being initialized.
return NS_OK;
}
mPendingSpellCheck =
do_CreateInstance("@mozilla.org/editor/editorspellchecker;1");
NS_ENSURE_STATE(mPendingSpellCheck);
nsCOMPtr<nsITextServicesFilter> filter =
do_CreateInstance("@mozilla.org/editor/txtsrvfiltermail;1");
if (!filter) {
mPendingSpellCheck = nullptr;
NS_ENSURE_STATE(filter);
}
mPendingSpellCheck->SetFilter(filter);
mPendingInitEditorSpellCheckCallback = new InitEditorSpellCheckCallback(this);
if (!mPendingInitEditorSpellCheckCallback) {
mPendingSpellCheck = nullptr;
NS_ENSURE_STATE(mPendingInitEditorSpellCheckCallback);
}
nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditor);
nsresult rv = mPendingSpellCheck->InitSpellChecker(
editor, false, mPendingInitEditorSpellCheckCallback);
if (NS_FAILED(rv)) {
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback = nullptr;
NS_ENSURE_SUCCESS(rv, rv);
}
ChangeNumPendingSpellChecks(1);
return NS_OK;
}
// Called when nsIEditorSpellCheck::InitSpellChecker completes.
nsresult
mozInlineSpellChecker::EditorSpellCheckInited()
{
NS_ASSERTION(mPendingSpellCheck, "Spell check should be pending!");
// spell checking is enabled, register our event listeners to track navigation
RegisterEventListeners();
}
}
mSpellCheck = mPendingSpellCheck;
mPendingSpellCheck = nullptr;
mPendingInitEditorSpellCheckCallback = nullptr;
ChangeNumPendingSpellChecks(-1);
// spellcheck the current contents. SpellCheckRange doesn't supply a created
// range to DoSpellCheck, which in our case is the entire range. But this
@ -689,6 +812,39 @@ mozInlineSpellChecker::SetEnableRealTimeSpell(bool aEnabled)
return SpellCheckRange(nullptr);
}
// Changes the number of pending spell checks by the given delta. If the number
// becomes zero or nonzero, observers are notified. See NotifyObservers for
// info on the aEditor parameter.
void
mozInlineSpellChecker::ChangeNumPendingSpellChecks(int32_t aDelta,
nsIEditor* aEditor)
{
int8_t oldNumPending = mNumPendingSpellChecks;
mNumPendingSpellChecks += aDelta;
NS_ASSERTION(mNumPendingSpellChecks >= 0,
"Unbalanced ChangeNumPendingSpellChecks calls!");
if (oldNumPending == 0 && mNumPendingSpellChecks > 0) {
NotifyObservers(INLINESPELL_STARTED_TOPIC, aEditor);
} else if (oldNumPending > 0 && mNumPendingSpellChecks == 0) {
NotifyObservers(INLINESPELL_ENDED_TOPIC, aEditor);
}
}
// Broadcasts the given topic to observers. aEditor is passed to observers if
// nonnull; otherwise mEditor is passed.
void
mozInlineSpellChecker::NotifyObservers(const char* aTopic, nsIEditor* aEditor)
{
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (!os)
return;
nsCOMPtr<nsIEditor> editor = aEditor;
if (!editor) {
editor = do_QueryReferent(mEditor);
}
os->NotifyObservers(editor, aTopic, nullptr);
}
// mozInlineSpellChecker::SpellCheckAfterEditorChange
//
// Called by the editor when nearly anything happens to change the content.
@ -1127,17 +1283,21 @@ mozInlineSpellChecker::ScheduleSpellCheck(const mozInlineSpellStatus& aStatus)
return NS_OK;
}
mozInlineSpellResume* resume = new mozInlineSpellResume(aStatus);
mozInlineSpellResume* resume =
new mozInlineSpellResume(aStatus, mDisabledAsyncToken);
NS_ENSURE_TRUE(resume, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = resume->Post();
if (NS_FAILED(rv)) {
delete resume;
} else if (aStatus.IsFullSpellCheck()) {
} else {
if (aStatus.IsFullSpellCheck()) {
// We're going to check everything. Suppress further spell-check attempts
// until that happens.
mFullSpellCheckScheduled = true;
}
ChangeNumPendingSpellChecks(1);
}
return rv;
}
@ -1418,6 +1578,24 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
return NS_OK;
}
// An RAII helper that calls ChangeNumPendingSpellChecks on destruction.
class AutoChangeNumPendingSpellChecks
{
public:
AutoChangeNumPendingSpellChecks(mozInlineSpellChecker* aSpellChecker,
int32_t aDelta)
: mSpellChecker(aSpellChecker), mDelta(aDelta) {}
~AutoChangeNumPendingSpellChecks()
{
mSpellChecker->ChangeNumPendingSpellChecks(mDelta);
}
private:
nsRefPtr<mozInlineSpellChecker> mSpellChecker;
int32_t mDelta;
};
// mozInlineSpellChecker::ResumeCheck
//
// Called by the resume event when it fires. We will try to pick up where
@ -1426,6 +1604,12 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil,
nsresult
mozInlineSpellChecker::ResumeCheck(mozInlineSpellStatus* aStatus)
{
// Observers should be notified that spell check has ended only after spell
// check is done below, but since there are many early returns in this method
// and the number of pending spell checks must be decremented regardless of
// whether the spell check actually happens, use this RAII object.
AutoChangeNumPendingSpellChecks autoChangeNumPending(this, -1);
if (aStatus->IsFullSpellCheck()) {
// Allow posting new spellcheck resume events from inside
// ResumeCheck, now that we're actually firing.
@ -1761,19 +1945,63 @@ nsresult mozInlineSpellChecker::KeyPress(nsIDOMEvent* aKeyEvent)
return NS_OK;
}
// Used as the nsIEditorSpellCheck::UpdateCurrentDictionary callback.
class UpdateCurrentDictionaryCallback MOZ_FINAL : public nsIEditorSpellCheckCallback
{
public:
NS_DECL_ISUPPORTS
explicit UpdateCurrentDictionaryCallback(mozInlineSpellChecker* aSpellChecker,
uint32_t aDisabledAsyncToken)
: mSpellChecker(aSpellChecker), mDisabledAsyncToken(aDisabledAsyncToken) {}
NS_IMETHOD EditorSpellCheckDone()
{
// Ignore this callback if SetEnableRealTimeSpell(false) was called after
// the UpdateCurrentDictionary call that triggered it.
return mSpellChecker->mDisabledAsyncToken > mDisabledAsyncToken ?
NS_OK :
mSpellChecker->CurrentDictionaryUpdated();
}
private:
nsRefPtr<mozInlineSpellChecker> mSpellChecker;
uint32_t mDisabledAsyncToken;
};
NS_IMPL_ISUPPORTS1(UpdateCurrentDictionaryCallback, nsIEditorSpellCheckCallback)
NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
{
if (!mSpellCheck) {
return NS_OK;
}
nsAutoString previousDictionary;
if (NS_FAILED(mSpellCheck->GetCurrentDictionary(previousDictionary))) {
previousDictionary.Truncate();
if (NS_FAILED(mSpellCheck->GetCurrentDictionary(mPreviousDictionary))) {
mPreviousDictionary.Truncate();
}
// This might set mSpellCheck to null (bug 793866)
nsresult rv = mSpellCheck->UpdateCurrentDictionary();
nsRefPtr<UpdateCurrentDictionaryCallback> cb =
new UpdateCurrentDictionaryCallback(this, mDisabledAsyncToken);
NS_ENSURE_STATE(cb);
nsresult rv = mSpellCheck->UpdateCurrentDictionary(cb);
if (NS_FAILED(rv)) {
cb = nullptr;
NS_ENSURE_SUCCESS(rv, rv);
}
mNumPendingUpdateCurrentDictionary++;
ChangeNumPendingSpellChecks(1);
return NS_OK;
}
// Called when nsIEditorSpellCheck::UpdateCurrentDictionary completes.
nsresult mozInlineSpellChecker::CurrentDictionaryUpdated()
{
mNumPendingUpdateCurrentDictionary--;
NS_ASSERTION(mNumPendingUpdateCurrentDictionary >= 0,
"CurrentDictionaryUpdated called without corresponding "
"UpdateCurrentDictionary call!");
ChangeNumPendingSpellChecks(-1);
nsAutoString currentDictionary;
if (!mSpellCheck ||
@ -1781,9 +2009,17 @@ NS_IMETHODIMP mozInlineSpellChecker::UpdateCurrentDictionary()
currentDictionary.Truncate();
}
if (!previousDictionary.Equals(currentDictionary)) {
rv = SpellCheckRange(nullptr);
if (!mPreviousDictionary.Equals(currentDictionary)) {
nsresult rv = SpellCheckRange(nullptr);
NS_ENSURE_SUCCESS(rv, rv);
}
return rv;
return NS_OK;
}
NS_IMETHODIMP
mozInlineSpellChecker::GetSpellCheckPending(bool* aPending)
{
*aPending = mNumPendingSpellChecks > 0;
return NS_OK;
}

View File

@ -28,6 +28,9 @@ class nsIDOMMouseEventListener;
class mozInlineSpellWordUtil;
class mozInlineSpellChecker;
class mozInlineSpellResume;
class InitEditorSpellCheckCallback;
class UpdateCurrentDictionaryCallback;
class mozInlineSpellResume;
class mozInlineSpellStatus
{
@ -120,6 +123,10 @@ class mozInlineSpellChecker : public nsIInlineSpellChecker,
{
private:
friend class mozInlineSpellStatus;
friend class InitEditorSpellCheckCallback;
friend class UpdateCurrentDictionaryCallback;
friend class AutoChangeNumPendingSpellChecks;
friend class mozInlineSpellResume;
// Access with CanEnableInlineSpellChecking
enum SpellCheckingState { SpellCheck_Uninitialized = -1,
@ -129,6 +136,7 @@ private:
nsWeakPtr mEditor;
nsCOMPtr<nsIEditorSpellCheck> mSpellCheck;
nsCOMPtr<nsIEditorSpellCheck> mPendingSpellCheck;
nsCOMPtr<nsIDOMTreeWalker> mTreeWalker;
nsCOMPtr<mozISpellI18NUtil> mConverter;
@ -147,6 +155,24 @@ private:
nsCOMPtr<nsIDOMNode> mCurrentSelectionAnchorNode;
int32_t mCurrentSelectionOffset;
// Tracks the number of pending spell checks *and* async operations that may
// lead to spell checks, like updating the current dictionary. This is
// necessary so that observers can know when to wait for spell check to
// complete.
int32_t mNumPendingSpellChecks;
// The number of calls to UpdateCurrentDictionary that haven't finished yet.
int32_t mNumPendingUpdateCurrentDictionary;
// This number is incremented each time the spell checker is disabled so that
// pending scheduled spell checks and UpdateCurrentDictionary calls can be
// ignored when they finish.
uint32_t mDisabledAsyncToken;
// When mPendingSpellCheck is non-null, this is the callback passed when
// it was initialized.
nsCOMPtr<InitEditorSpellCheckCallback> mPendingInitEditorSpellCheckCallback;
// Set when we have spellchecked after the last edit operation. See the
// commment at the top of the .cpp file for more info.
bool mNeedsCheckAfterNavigation;
@ -155,6 +181,9 @@ private:
// the whole document.
bool mFullSpellCheckScheduled;
// Maintains state during the asynchronous UpdateCurrentDictionary call.
nsString mPreviousDictionary;
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
@ -228,6 +257,18 @@ public:
nsresult SaveCurrentSelectionPosition();
nsresult ResumeCheck(mozInlineSpellStatus* aStatus);
protected:
// called when async nsIEditorSpellCheck methods complete
nsresult EditorSpellCheckInited();
nsresult CurrentDictionaryUpdated();
// track the number of pending spell checks and async operations that may lead
// to spell checks, notifying observers accordingly
void ChangeNumPendingSpellChecks(int32_t aDelta,
nsIEditor* aEditor = nullptr);
void NotifyObservers(const char* aTopic, nsIEditor* aEditor);
};
#endif /* __mozinlinespellchecker_h__ */

View File

@ -380,7 +380,6 @@ nsLayoutStatics::Shutdown()
nsLayoutUtils::Shutdown();
nsHyphenationManager::Shutdown();
nsEditorSpellCheck::ShutDown();
nsDOMMutationObserver::Shutdown();
AudioChannelService::Shutdown();