Bug 352037 - Add an "Undo add to dictionary" item to spell checker's context menu item; r=ehsan
authorQuentin Headen <qheaden@phaseshiftsoftware.com>
Thu, 29 Dec 2011 16:06:56 -0500
changeset 84742 314f940ca86ea3bec225cbcd477e35573eb8fca8
parent 84741 972982711eb315c39a18a111d4109dfea03fcd84
child 84743 9cfa99d3807f4374cba6bb858381b1837d9a06b6
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs352037
milestone12.0a1
Bug 352037 - Add an "Undo add to dictionary" item to spell checker's context menu item; r=ehsan
browser/base/content/browser-context.inc
browser/base/content/nsContextMenu.js
browser/base/content/test/test_contextmenu.html
editor/txtsvc/public/nsIInlineSpellChecker.idl
extensions/spellcheck/src/mozInlineSpellChecker.cpp
toolkit/content/InlineSpellChecker.jsm
toolkit/content/tests/chrome/Makefile.in
toolkit/content/tests/chrome/test_textbox_dictionary.xul
toolkit/content/widgets/textbox.xml
toolkit/locales/en-US/chrome/global/textcontext.dtd
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -41,16 +41,20 @@
       <menuseparator id="page-menu-separator"/>
       <menuitem id="spell-no-suggestions"
                 disabled="true"
                 label="&spellNoSuggestions.label;"/>
       <menuitem id="spell-add-to-dictionary"
                 label="&spellAddToDictionary.label;"
                 accesskey="&spellAddToDictionary.accesskey;"
                 oncommand="InlineSpellCheckerUI.addToDictionary();"/>
+      <menuitem id="spell-undo-add-to-dictionary"
+                label="&spellUndoAddToDictionary.label;"
+                accesskey="&spellUndoAddToDictionary.accesskey;"
+                oncommand="InlineSpellCheckerUI.undoAddToDictionary();" />
       <menuseparator id="spell-suggestions-separator"/>
       <menuitem id="context-openlinkincurrent"
                 label="&openLinkCmdInCurrent.label;"
                 accesskey="&openLinkCmdInCurrent.accesskey;"
                 oncommand="gContextMenu.openLinkInCurrent();"/>
       <menuitem id="context-openlinkintab"
                 label="&openLinkCmdInTab.label;"
                 accesskey="&openLinkCmdInTab.accesskey;"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -332,16 +332,17 @@ nsContextMenu.prototype = {
     var canSpell = InlineSpellCheckerUI.canSpellCheck;
     var onMisspelling = InlineSpellCheckerUI.overMisspelling;
     this.showItem("spell-check-enabled", canSpell);
     this.showItem("spell-separator", canSpell || this.onEditableArea);
     document.getElementById("spell-check-enabled")
             .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
 
     this.showItem("spell-add-to-dictionary", onMisspelling);
+    this.showItem("spell-undo-add-to-dictionary", InlineSpellCheckerUI.canUndo());
 
     // suggestion list
     this.showItem("spell-suggestions-separator", onMisspelling);
     if (onMisspelling) {
       var suggestionsSeparator =
         document.getElementById("spell-add-to-dictionary");
       var numsug =
         InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
--- a/browser/base/content/test/test_contextmenu.html
+++ b/browser/base/content/test/test_contextmenu.html
@@ -14,16 +14,17 @@ Browser context menu tests.
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Login Manager: multiple login autocomplete. **/
 
 netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 function openContextMenuFor(element, shiftkey, shouldWaitForFocus) {
     // Context menu should be closed before we open it again.
     is(contextMenu.state, "closed", "checking if popup is closed");
 
@@ -509,22 +510,46 @@ function runTest(testNum) {
                           "context-selectall",   true,
                           "---",                 null,
                           "spell-check-enabled", true,
                           "spell-dictionaries",  true,
                               ["spell-check-dictionary-en-US", true,
                                "---",                          null,
                                "spell-add-dictionaries",       true], null,
                          ].concat(inspectItems));
-
+        contextMenu.ownerDocument.getElementById("spell-add-to-dictionary").doCommand(); // Add to dictionary
         closeContextMenu();
-        openContextMenuFor(contenteditable); // Invoke context menu for next test.
+        openContextMenuFor(textarea, false, true); // Invoke context menu for next test.
+        break;
+    
+    case 15:    
+        // Context menu for textarea after a word has been added
+        // to the dictionary
+        checkContextMenu(["spell-undo-add-to-dictionary", true,
+                          "context-undo",        false,
+                          "---",                 null,
+                          "context-cut",         false,
+                          "context-copy",        false,
+                          "context-paste",       null, // ignore clipboard state
+                          "context-delete",      false,
+                          "---",                 null,
+                          "context-selectall",   true,
+                          "---",                 null,
+                          "spell-check-enabled", true,
+                          "spell-dictionaries",  true,
+                              ["spell-check-dictionary-en-US", true,
+                               "---",                          null,
+                               "spell-add-dictionaries",       true], null,
+                         ].concat(inspectItems));
+        contextMenu.ownerDocument.getElementById("spell-undo-add-to-dictionary").doCommand(); // Undo add to dictionary
+        closeContextMenu();
+        openContextMenuFor(contenteditable);
         break;
 
-    case 15:
+    case 16:
         // Context menu for contenteditable
         checkContextMenu(["spell-no-suggestions", false,
                           "spell-add-to-dictionary", true,
                           "---",                 null,
                           "context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
@@ -539,17 +564,17 @@ function runTest(testNum) {
                                "---",                          null,
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
 
         closeContextMenu();
         openContextMenuFor(inputspell); // Invoke context menu for next test.
         break;
 
-    case 16:
+    case 17:
         // Context menu for spell-check input
         checkContextMenu(["*prodigality",        true, // spelling suggestion
                           "spell-add-to-dictionary", true,
                           "---",                 null,
                           "context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
@@ -564,23 +589,23 @@ function runTest(testNum) {
                                "---",                          null,
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
 
         closeContextMenu();
         openContextMenuFor(link); // Invoke context menu for next test.
         break;
 
-    case 17:
+    case 18:
         executeCopyCommand("cmd_copyLink", "http://mozilla.com/");
         closeContextMenu();
         openContextMenuFor(pagemenu); // Invoke context menu for next test.
         break;
 
-    case 18:
+    case 19:
         // Context menu for element with assigned content context menu
         checkContextMenu(["+Plain item",          {type: "", icon: "", checked: false, disabled: false},
                           "+Disabled item",       {type: "", icon: "", checked: false, disabled: true},
                           "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false},
                           "---",                  null,
                           "+Checkbox",            {type: "checkbox", icon: "", checked: true, disabled: false},
                           "---",                  null,
                           "+Radio1",              {type: "checkbox", icon: "", checked: true, disabled: false},
@@ -613,17 +638,17 @@ function runTest(testNum) {
                           "context-viewinfo",     true
                          ].concat(inspectItems));
 
         invokeItemAction("0");
         closeContextMenu();
         openContextMenuFor(pagemenu, true); // Invoke context menu for next test.
         break;
 
-    case 19:
+    case 20:
         // Context menu for element with assigned content context menu
         // The shift key should bypass content context menu processing
         checkContextMenu(["context-back",         false,
                           "context-forward",      false,
                           "context-reload",       true,
                           "context-stop",         false,
                           "---",                  null,
                           "context-bookmarkpage", true,
--- a/editor/txtsvc/public/nsIInlineSpellChecker.idl
+++ b/editor/txtsvc/public/nsIInlineSpellChecker.idl
@@ -38,17 +38,17 @@
 
 #include "nsISupports.idl"
 #include "domstubs.idl"
 
 interface nsISelection;
 interface nsIEditor;
 interface nsIEditorSpellCheck;
 
-[scriptable, uuid(f456dda1-965d-470c-8c55-e51b38e45212)]
+[scriptable, uuid(df635540-d073-47b8-8678-18776130691d)]
 
 interface nsIInlineSpellChecker : nsISupports
 {
   readonly attribute nsIEditorSpellCheck spellChecker;
 
   [noscript] void init(in nsIEditor aEditor);
   [noscript] void cleanup(in boolean aDestroyingFrames);
 
@@ -63,16 +63,17 @@ interface nsIInlineSpellChecker : nsISup
                                    in nsIDOMNode aEndNode,
                                    in long aEndOffset);
 
   void spellCheckRange(in nsIDOMRange aSelection);
 
   nsIDOMRange getMisspelledWord(in nsIDOMNode aNode, in long aOffset);
   void replaceWord(in nsIDOMNode aNode, in long aOffset, in AString aNewword);
   void addWordToDictionary(in AString aWord);
+  void removeWordFromDictionary(in AString aWord);
   
   void ignoreWord(in AString aWord);
   void ignoreWords([array, size_is(aCount)] in wstring aWordsToIgnore, in unsigned long aCount);
   void updateCurrentDictionary();
 };
 
 %{C++
 
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -864,16 +864,34 @@ mozInlineSpellChecker::AddWordToDictiona
   NS_ENSURE_SUCCESS(rv, rv); 
 
   mozInlineSpellStatus status(this);
   rv = status.InitForSelection();
   NS_ENSURE_SUCCESS(rv, rv);
   return ScheduleSpellCheck(status);
 }
 
+//  mozInlineSpellChecker::RemoveWordFromDictionary
+
+NS_IMETHODIMP
+mozInlineSpellChecker::RemoveWordFromDictionary(const nsAString &word)
+{
+  NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
+
+  nsAutoString wordstr(word);
+  nsresult rv = mSpellCheck->RemoveWordFromDictionary(wordstr.get());
+  NS_ENSURE_SUCCESS(rv, rv); 
+  
+  mozInlineSpellStatus status(this);
+  nsCOMPtr<nsIRange> range = do_QueryInterface(NULL); // Check everything
+  rv = status.InitForRange(range);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return ScheduleSpellCheck(status);
+}
+
 // mozInlineSpellChecker::IgnoreWord
 
 NS_IMETHODIMP
 mozInlineSpellChecker::IgnoreWord(const nsAString &word)
 {
   NS_ENSURE_TRUE(mSpellCheck, NS_ERROR_NOT_INITIALIZED);
 
   nsAutoString wordstr(word);
--- a/toolkit/content/InlineSpellChecker.jsm
+++ b/toolkit/content/InlineSpellChecker.jsm
@@ -33,19 +33,21 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 var EXPORTED_SYMBOLS = [ "InlineSpellChecker" ];
 var gLanguageBundle;
 var gRegionBundle;
+const MAX_UNDO_STACK_DEPTH = 1;
 
 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: function(aEditor)
   {
     this.uninit();
     this.mEditor = aEditor;
@@ -279,15 +281,34 @@ InlineSpellChecker.prototype = {
   toggleEnabled: function()
   {
     this.mEditor.setSpellcheckUserOverride(!this.mInlineSpellChecker.enableRealTimeSpell);
   },
 
   // callback for adding the current misspelling to the user-defined dictionary
   addToDictionary: function()
   {
+    // 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);
     this.mInlineSpellChecker.addWordToDictionary(this.mMisspelling);
   },
+  // callback for removing the last added word to the dictionary LIFO fashion
+  undoAddToDictionary: function()
+  {
+    if (this.mAddedWordStack.length > 0)
+    {
+      var word = this.mAddedWordStack.pop();
+      this.mInlineSpellChecker.removeWordFromDictionary(word);
+    }
+  },
+  canUndo : function()
+  {
+    // Return true if we have words on the stack
+    return (this.mAddedWordStack.length > 0);
+  },
   ignoreWord: function()
   {
     this.mInlineSpellChecker.ignoreWord(this.mMisspelling);
   }
 };
--- a/toolkit/content/tests/chrome/Makefile.in
+++ b/toolkit/content/tests/chrome/Makefile.in
@@ -139,16 +139,17 @@ include $(topsrcdir)/config/rules.mk
 		test_statusbar.xul \
 		test_timepicker.xul \
 		test_tree.xul \
 		test_tree_view.xul \
 		test_tree_single.xul \
 		test_textbox_emptytext.xul \
 		test_textbox_number.xul \
 		test_textbox_search.xul \
+		test_textbox_dictionary.xul\
 		test_toolbar.xul \
 		xul_selectcontrol.js \
 		test_popupincontent.xul \
 		test_panelfrommenu.xul \
 		test_hiddenitems.xul \
 		test_hiddenpaging.xul \
 		test_popup_tree.xul \
 		test_popup_keys.xul \
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_textbox_dictionary.xul
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+  XUL Widget Test for textbox with placeholder
+  -->
+<window title="Textbox Add and Undo Add Dictionary Test" width="500" height="600"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+  <hbox>
+    <textbox id="t1"  value="Hellop" oncontextmenu="runContextMenuTest()" spellcheck="true"/>
+  </hbox>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
+
+  <!-- test code goes here -->
+  <script type="application/javascript"><![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var textbox;
+var testNum;
+
+function bringUpContextMenu(element)
+{
+  synthesizeMouseAtCenter(element, { type: "contextmenu", button: 2});
+}
+
+function leftClickElement(element)
+{
+  synthesizeMouseAtCenter(element, { button: 0 });
+}
+
+function startTests() 
+{     
+  textbox = document.getElementById("t1");
+  textbox.focus();
+  testNum = 0;
+  
+  SimpleTest.executeSoon( function() {  bringUpContextMenu(textbox); });
+}
+
+function runContextMenuTest()
+{
+  SimpleTest.executeSoon( function() {
+    // The textbox has its children in an hbox XUL element, so get that first
+    var hbox = document.getAnonymousNodes(textbox).item(0);
+    
+    var contextMenu = document.getAnonymousElementByAttribute(hbox, "anonid", "input-box-contextmenu");
+   
+    switch(testNum)
+    {
+      case 0: // "Add to Dictionary" button
+        var addToDict = contextMenu.querySelector("[anonid=spell-add-to-dictionary]");
+        is(!addToDict.hidden, true, "Is Add to Dictionary visible?");
+        
+        addToDict.doCommand();
+        
+        contextMenu.hidePopup();
+        testNum++;
+        
+        SimpleTest.executeSoon( function() {bringUpContextMenu(textbox); }); // Bring up the menu again to invoke the next test
+        break;
+        
+      case 1: // "Undo Add to Dictionary" button
+        var undoAddDict = contextMenu.querySelector("[anonid=spell-undo-add-to-dictionary]");
+        is(!undoAddDict.hidden, true, "Is Undo Add to Dictioanry visible?");
+        
+        undoAddDict.doCommand();
+        
+        contextMenu.hidePopup();
+        SimpleTest.finish();
+        break;
+    }
+  });
+}
+
+SimpleTest.waitForFocus(startTests);
+
+  ]]></script>
+
+</window>
--- a/toolkit/content/widgets/textbox.xml
+++ b/toolkit/content/widgets/textbox.xml
@@ -510,16 +510,18 @@
                      onpopupshowing="if (document.commandDispatcher.focusedElement != this.parentNode.firstChild)
                                        this.parentNode.firstChild.focus();
                                      this.parentNode._doPopupItemEnablingSpell(this);"
                      onpopuphiding="this.parentNode._doPopupItemDisabling(this);"
                      oncommand="var cmd = event.originalTarget.getAttribute('cmd'); if(cmd) { this.parentNode.doCommand(cmd); event.stopPropagation(); }">
         <xul:menuitem label="&spellNoSuggestions.label;" anonid="spell-no-suggestions" disabled="true"/>
         <xul:menuitem label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;" anonid="spell-add-to-dictionary"
                       oncommand="this.parentNode.parentNode.spellCheckerUI.addToDictionary();"/>
+        <xul:menuitem label="&spellUndoAddToDictionary.label;" accesskey="&spellUndoAddToDictionary.accesskey;" anonid="spell-undo-add-to-dictionary"
+                      oncommand="this.parentNode.parentNode.spellCheckerUI.undoAddToDictionary();"/>
         <xul:menuseparator anonid="spell-suggestions-separator"/>
         <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"/>
         <xul:menuseparator/>
         <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"/>
         <xul:menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"/>
         <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"/>
         <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"/>
         <xul:menuseparator/>
@@ -575,29 +577,31 @@
         <body>
           <![CDATA[
             var spellui = this.spellCheckerUI;
             if (!spellui || !spellui.canSpellCheck) {
               this._setMenuItemVisibility("spell-no-suggestions", false);
               this._setMenuItemVisibility("spell-check-enabled", false);
               this._setMenuItemVisibility("spell-check-separator", false);
               this._setMenuItemVisibility("spell-add-to-dictionary", false);
+              this._setMenuItemVisibility("spell-undo-add-to-dictionary", false);
               this._setMenuItemVisibility("spell-suggestions-separator", false);
               this._setMenuItemVisibility("spell-dictionaries", false);
               return;
             }
 
             spellui.initFromEvent(document.popupRangeParent,
                                   document.popupRangeOffset);
 
             var enabled = spellui.enabled;
             this._enabledCheckbox.setAttribute("checked", enabled);
 
             var overMisspelling = spellui.overMisspelling;
             this._setMenuItemVisibility("spell-add-to-dictionary", overMisspelling);
+            this._setMenuItemVisibility("spell-undo-add-to-dictionary", spellui.canUndo());
             this._setMenuItemVisibility("spell-suggestions-separator", overMisspelling);
 
             // suggestion list
             var numsug = spellui.addSuggestionsToMenu(popupNode, this._suggestionsSeparator, 5);
             this._setMenuItemVisibility("spell-no-suggestions", overMisspelling && numsug == 0);
 
             // dictionary list
             var numdicts = spellui.addDictionaryListToMenu(this._dictionariesMenu, null);
--- a/toolkit/locales/en-US/chrome/global/textcontext.dtd
+++ b/toolkit/locales/en-US/chrome/global/textcontext.dtd
@@ -8,13 +8,15 @@
 <!ENTITY undoCmd.accesskey "u">
 <!ENTITY selectAllCmd.label "Select All">
 <!ENTITY selectAllCmd.accesskey "a">
 <!ENTITY deleteCmd.label "Delete">
 <!ENTITY deleteCmd.accesskey "d">
 
 <!ENTITY spellAddToDictionary.label "Add to Dictionary">
 <!ENTITY spellAddToDictionary.accesskey "o">
+<!ENTITY spellUndoAddToDictionary.label "Undo Add To Dictionary">
+<!ENTITY spellUndoAddToDictionary.accesskey "n">
 <!ENTITY spellCheckEnable.label "Check Spelling">
 <!ENTITY spellCheckEnable.accesskey "S">
 <!ENTITY spellNoSuggestions.label "(No Spelling Suggestions)">
 <!ENTITY spellDictionaries.label "Languages">
 <!ENTITY spellDictionaries.accesskey "l">