From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- toolkit/modules/InlineSpellChecker.sys.mjs | 626 +++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 toolkit/modules/InlineSpellChecker.sys.mjs (limited to 'toolkit/modules/InlineSpellChecker.sys.mjs') diff --git a/toolkit/modules/InlineSpellChecker.sys.mjs b/toolkit/modules/InlineSpellChecker.sys.mjs new file mode 100644 index 0000000000..7d70c6a89a --- /dev/null +++ b/toolkit/modules/InlineSpellChecker.sys.mjs @@ -0,0 +1,626 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const MAX_UNDO_STACK_DEPTH = 1; + +export function InlineSpellChecker(aEditor) { + this.init(aEditor); + this.mAddedWordStack = []; // We init this here to preserve it between init/uninit calls +} + +InlineSpellChecker.prototype = { + // Call this function to initialize for a given editor + init(aEditor) { + this.uninit(); + this.mEditor = aEditor; + try { + this.mInlineSpellChecker = this.mEditor.getInlineSpellChecker(true); + // note: this might have been NULL if there is no chance we can spellcheck + } catch (e) { + this.mInlineSpellChecker = null; + } + }, + + initFromRemote(aSpellInfo, aWindowGlobalParent) { + if (this.mRemote) { + // We shouldn't get here, but let's just recover instead of bricking the + // menu by throwing exceptions: + console.error(new Error("Unexpected remote spellchecker present!")); + try { + this.mRemote.uninit(); + } catch (ex) { + console.error(ex); + } + this.mRemote = null; + } + this.uninit(); + + if (!aSpellInfo) { + return; + } + this.mInlineSpellChecker = this.mRemote = new RemoteSpellChecker( + aSpellInfo, + aWindowGlobalParent + ); + this.mOverMisspelling = aSpellInfo.overMisspelling; + this.mMisspelling = aSpellInfo.misspelling; + }, + + // call this to clear state + uninit() { + if (this.mRemote) { + this.mRemote.uninit(); + this.mRemote = null; + } + + this.mEditor = null; + this.mInlineSpellChecker = null; + this.mOverMisspelling = false; + this.mMisspelling = ""; + this.mMenu = null; + this.mSuggestionItems = []; + this.mDictionaryMenu = null; + this.mDictionaryItems = []; + this.mWordNode = null; + }, + + // for each UI event, you must call this function, it will compute the + // word the cursor is over + initFromEvent(rangeParent, rangeOffset) { + this.mOverMisspelling = false; + + if (!rangeParent || !this.mInlineSpellChecker) { + return; + } + + var selcon = this.mEditor.selectionController; + var spellsel = selcon.getSelection(selcon.SELECTION_SPELLCHECK); + if (spellsel.rangeCount == 0) { + return; + } // easy case - no misspellings + + var range = this.mInlineSpellChecker.getMisspelledWord( + rangeParent, + rangeOffset + ); + if (!range) { + return; + } // not over a misspelled word + + this.mMisspelling = range.toString(); + this.mOverMisspelling = true; + this.mWordNode = rangeParent; + this.mWordOffset = rangeOffset; + }, + + // returns false if there should be no spellchecking UI enabled at all, true + // means that you can at least give the user the ability to turn it on. + get canSpellCheck() { + // inline spell checker objects will be created only if there are actual + // dictionaries available + if (this.mRemote) { + return this.mRemote.canSpellCheck; + } + return this.mInlineSpellChecker != null; + }, + + get initialSpellCheckPending() { + if (this.mRemote) { + return this.mRemote.spellCheckPending; + } + return !!( + this.mInlineSpellChecker && + !this.mInlineSpellChecker.spellChecker && + this.mInlineSpellChecker.spellCheckPending + ); + }, + + // Whether spellchecking is enabled in the current box + get enabled() { + if (this.mRemote) { + return this.mRemote.enableRealTimeSpell; + } + return ( + this.mInlineSpellChecker && this.mInlineSpellChecker.enableRealTimeSpell + ); + }, + set enabled(isEnabled) { + if (this.mRemote) { + this.mRemote.setSpellcheckUserOverride(isEnabled); + } else if (this.mInlineSpellChecker) { + this.mEditor.setSpellcheckUserOverride(isEnabled); + } + }, + + // returns true if the given event is over a misspelled word + get overMisspelling() { + return this.mOverMisspelling; + }, + + // this prepends up to "maxNumber" suggestions at the given menu position + // for the word under the cursor. Returns the number of suggestions inserted. + addSuggestionsToMenuOnParent(menu, insertBefore, maxNumber) { + if (this.mRemote) { + // This is used on parent process only. + // If you want to add suggestions to context menu, get suggestions then + // use addSuggestionsToMenu instead. + return 0; + } + if (!this.mInlineSpellChecker || !this.mOverMisspelling) { + return 0; + } + + let spellchecker = this.mInlineSpellChecker.spellChecker; + let spellSuggestions = []; + + try { + if (!spellchecker.CheckCurrentWord(this.mMisspelling)) { + return 0; + } + + for (let i = 0; i < maxNumber; i++) { + let suggestion = spellchecker.GetSuggestedWord(); + if (!suggestion.length) { + // no more data + break; + } + spellSuggestions.push(suggestion); + } + } catch (e) { + return 0; + } + return this._addSuggestionsToMenu(menu, insertBefore, spellSuggestions); + }, + + addSuggestionsToMenu(menu, insertBefore, spellSuggestions) { + if ( + !this.mRemote && + (!this.mInlineSpellChecker || !this.mOverMisspelling) + ) { + return 0; + } // nothing to do + + if (!spellSuggestions?.length) { + return 0; + } + + return this._addSuggestionsToMenu(menu, insertBefore, spellSuggestions); + }, + + _addSuggestionsToMenu(menu, insertBefore, spellSuggestions) { + this.mMenu = menu; + this.mSuggestionItems = []; + + for (let suggestion of spellSuggestions) { + var item = menu.ownerDocument.createXULElement("menuitem"); + this.mSuggestionItems.push(item); + item.setAttribute("label", suggestion); + item.setAttribute("value", suggestion); + item.addEventListener( + "command", + this.replaceMisspelling.bind(this, suggestion), + true + ); + item.setAttribute("class", "spell-suggestion"); + menu.insertBefore(item, insertBefore); + } + return spellSuggestions.length; + }, + + // undoes the work of addSuggestionsToMenu for the same menu + // (call from popup hiding) + clearSuggestionsFromMenu() { + for (var i = 0; i < this.mSuggestionItems.length; i++) { + this.mMenu.removeChild(this.mSuggestionItems[i]); + } + this.mSuggestionItems = []; + }, + + sortDictionaryList(list) { + var sortedList = []; + var names = Services.intl.getLocaleDisplayNames(undefined, list); + for (var i = 0; i < list.length; i++) { + sortedList.push({ localeCode: list[i], displayName: names[i] }); + } + let comparer = new Services.intl.Collator().compare; + sortedList.sort((a, b) => comparer(a.displayName, b.displayName)); + return sortedList; + }, + + async languageMenuListener(evt) { + let curlangs = new Set(); + if (this.mRemote) { + curlangs = new Set(this.mRemote.currentDictionaries); + } else if (this.mInlineSpellChecker) { + let spellchecker = this.mInlineSpellChecker.spellChecker; + try { + curlangs = new Set(spellchecker.getCurrentDictionaries()); + } catch (e) {} + } + + let localeCodes = new Set(curlangs); + let localeCode = evt.target.dataset.localeCode; + if (localeCodes.has(localeCode)) { + localeCodes.delete(localeCode); + } else { + localeCodes.add(localeCode); + } + let dictionaries = Array.from(localeCodes); + await this.selectDictionaries(dictionaries); + if (this.mRemote) { + // Store the new set in case the menu doesn't close. + this.mRemote.currentDictionaries = dictionaries; + } + // Notify change of dictionary, especially for Thunderbird, + // which is otherwise not notified any more. + let view = this.mDictionaryMenu.ownerGlobal; + let spellcheckChangeEvent = new view.CustomEvent("spellcheck-changed", { + detail: { dictionaries }, + }); + this.mDictionaryMenu.ownerDocument.dispatchEvent(spellcheckChangeEvent); + }, + + // returns the number of dictionary languages. If insertBefore is NULL, this + // does an append to the given menu + addDictionaryListToMenu(menu, insertBefore) { + this.mDictionaryMenu = menu; + this.mDictionaryItems = []; + + if (!this.enabled) { + return 0; + } + + let list; + let curlangs = new Set(); + if (this.mRemote) { + list = this.mRemote.dictionaryList; + curlangs = new Set(this.mRemote.currentDictionaries); + } else if (this.mInlineSpellChecker) { + let spellchecker = this.mInlineSpellChecker.spellChecker; + list = spellchecker.GetDictionaryList(); + try { + curlangs = new Set(spellchecker.getCurrentDictionaries()); + } catch (e) {} + } + + let sortedList = this.sortDictionaryList(list); + this.languageMenuListenerBind = this.languageMenuListener.bind(this); + menu.addEventListener("command", this.languageMenuListenerBind, true); + + for (let i = 0; i < sortedList.length; i++) { + let item = menu.ownerDocument.createXULElement("menuitem"); + + item.setAttribute( + "id", + "spell-check-dictionary-" + sortedList[i].localeCode + ); + // XXX: Once Fluent has dynamic references, we could also lazily + // inject regionNames/languageNames FTL and localize using + // `l10n-id` here. + item.setAttribute("label", sortedList[i].displayName); + item.setAttribute("type", "checkbox"); + item.setAttribute("selection-type", "multiple"); + if (sortedList.length > 1) { + item.setAttribute("closemenu", "none"); + } + this.mDictionaryItems.push(item); + item.dataset.localeCode = sortedList[i].localeCode; + if (curlangs.has(sortedList[i].localeCode)) { + item.setAttribute("checked", "true"); + } + if (insertBefore) { + menu.insertBefore(item, insertBefore); + } else { + menu.appendChild(item); + } + } + return list.length; + }, + + // undoes the work of addDictionaryListToMenu for the menu + // (call on popup hiding) + clearDictionaryListFromMenu() { + this.mDictionaryMenu?.removeEventListener( + "command", + this.languageMenuListenerBind, + true + ); + for (var i = 0; i < this.mDictionaryItems.length; i++) { + this.mDictionaryMenu.removeChild(this.mDictionaryItems[i]); + } + this.mDictionaryItems = []; + }, + + // callback for selecting a dictionary + async selectDictionaries(localeCodes) { + if (this.mRemote) { + this.mRemote.selectDictionaries(localeCodes); + return; + } + if (!this.mInlineSpellChecker) { + return; + } + var spellchecker = this.mInlineSpellChecker.spellChecker; + await spellchecker.setCurrentDictionaries(localeCodes); + this.mInlineSpellChecker.spellCheckRange(null); // causes recheck + }, + + // callback for selecting a suggested replacement + replaceMisspelling(suggestion) { + if (this.mRemote) { + this.mRemote.replaceMisspelling(suggestion); + return; + } + if (!this.mInlineSpellChecker || !this.mOverMisspelling) { + return; + } + this.mInlineSpellChecker.replaceWord( + this.mWordNode, + this.mWordOffset, + suggestion + ); + }, + + // callback for enabling or disabling spellchecking + toggleEnabled() { + if (this.mRemote) { + this.mRemote.toggleEnabled(); + } else { + this.mEditor.setSpellcheckUserOverride( + !this.mInlineSpellChecker.enableRealTimeSpell + ); + } + }, + + // callback for adding the current misspelling to the user-defined dictionary + addToDictionary() { + // Prevent the undo stack from growing over the max depth + if (this.mAddedWordStack.length == MAX_UNDO_STACK_DEPTH) { + this.mAddedWordStack.shift(); + } + + this.mAddedWordStack.push(this.mMisspelling); + if (this.mRemote) { + this.mRemote.addToDictionary(); + } else { + this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling); + } + }, + // callback for removing the last added word to the dictionary LIFO fashion + undoAddToDictionary() { + if (this.mAddedWordStack.length) { + var word = this.mAddedWordStack.pop(); + if (this.mRemote) { + this.mRemote.undoAddToDictionary(word); + } else { + this.mInlineSpellChecker.removeWordFromDictionary(word); + } + } + }, + canUndo() { + // Return true if we have words on the stack + return !!this.mAddedWordStack.length; + }, + ignoreWord() { + if (this.mRemote) { + this.mRemote.ignoreWord(); + } else { + this.mInlineSpellChecker.ignoreWord(this.mMisspelling); + } + }, +}; + +export var SpellCheckHelper = { + // Set when over a non-read-only