Bug 607224, add property that specifies if a menu was opened via the keyboard, r=neil,a=blocking
authorNeil Deakin <neil@mozilla.com>
Tue, 04 Jan 2011 12:24:51 -0500
changeset 59851 e1a695a101388811e12cf456d3b0cbcb6420cfc2
parent 59850 d0141c1310ff5127baf8bd236448e4000749a838
push id1
push usershaver@mozilla.com
push dateTue, 04 Jan 2011 17:58:04 +0000
reviewersneil, blocking
bugs607224
milestone2.0b9pre
Bug 607224, add property that specifies if a menu was opened via the keyboard, r=neil,a=blocking
layout/xul/base/public/nsIMenuBoxObject.idl
layout/xul/base/src/nsMenuBarFrame.cpp
layout/xul/base/src/nsMenuBarFrame.h
layout/xul/base/src/nsMenuBarListener.cpp
layout/xul/base/src/nsMenuBoxObject.cpp
toolkit/content/tests/widgets/window_menubar.xul
toolkit/content/widgets/menu.xml
--- a/layout/xul/base/public/nsIMenuBoxObject.idl
+++ b/layout/xul/base/public/nsIMenuBoxObject.idl
@@ -36,23 +36,26 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsIBoxObject.idl"
 
 interface nsIDOMElement;
 interface nsIDOMKeyEvent;
 
-[scriptable, uuid(F5099746-5049-4e81-A03E-945D5110FEE2)]
+[scriptable, uuid(3931F141-D640-48AB-A792-719D62CF1736)]
 interface nsIMenuBoxObject : nsISupports
 {
   void openMenu(in boolean openFlag);
 
   attribute nsIDOMElement activeChild;
 
   boolean handleKeyPress(in nsIDOMKeyEvent keyEvent);
+
+  // true if the menu or menubar was opened via a keypress.
+  readonly attribute boolean openedWithKey;
 };
 
 %{C++
 nsresult
 NS_NewMenuBoxObject(nsIBoxObject** aResult);
 
 %}
--- a/layout/xul/base/src/nsMenuBarFrame.cpp
+++ b/layout/xul/base/src/nsMenuBarFrame.cpp
@@ -146,16 +146,17 @@ nsMenuBarFrame::SetActive(PRBool aActive
       return NS_OK;
   }
 
   mIsActive = aActiveFlag;
   if (mIsActive) {
     InstallKeyboardNavigator();
   }
   else {
+    mActiveByKeyboard = PR_FALSE;
     RemoveKeyboardNavigator();
   }
 
   NS_NAMED_LITERAL_STRING(active, "DOMMenuBarActive");
   NS_NAMED_LITERAL_STRING(inactive, "DOMMenuBarInactive");
   
   FireDOMEvent(mIsActive ? active : inactive, mContent);
 
--- a/layout/xul/base/src/nsMenuBarFrame.h
+++ b/layout/xul/base/src/nsMenuBarFrame.h
@@ -98,16 +98,19 @@ public:
 
   void
   SetStayActive(PRBool aStayActive) { mStayActive = aStayActive; }
 
   // Called when a menu on the menu bar is clicked on. Returns a menu if one
   // needs to be closed.
   nsMenuFrame* ToggleMenuActiveState();
 
+  PRBool IsActiveByKeyboard() { return mActiveByKeyboard; }
+  void SetActiveByKeyboard() { mActiveByKeyboard = PR_TRUE; }
+
   // indicate that a menu on the menubar was closed. Returns true if the caller
   // may deselect the menuitem.
   virtual PRBool MenuClosed();
 
   // Called when Enter is pressed while the menubar is focused. If the current
   // menu is open, let the child handle the key.
   nsMenuFrame* Enter();
 
@@ -132,16 +135,20 @@ public:
 protected:
   nsMenuBarListener* mMenuBarListener; // The listener that tells us about key and mouse events.
 
   // flag that is temporarily set when switching from one menu on the menubar to another
   // to indicate that the menubar should not be deactivated.
   PRPackedBool mStayActive;
 
   PRPackedBool mIsActive; // Whether or not the menu bar is active (a menu item is highlighted or shown).
+
+  // whether the menubar was made active via the keyboard.
+  PRPackedBool mActiveByKeyboard;
+
   // The current menu that is active (highlighted), which may not be open. This will
   // be null if no menu is active.
   nsMenuFrame* mCurrentMenu;
 
   nsIDOMEventTarget* mTarget;
 
 }; // class nsMenuBarFrame
 
--- a/layout/xul/base/src/nsMenuBarListener.cpp
+++ b/layout/xul/base/src/nsMenuBarListener.cpp
@@ -174,16 +174,19 @@ nsMenuBarListener::KeyUp(nsIDOMEvent* aK
     PRUint32 theChar;
     keyEvent->GetKeyCode(&theChar);
 
     if (mAccessKeyDown && !mAccessKeyDownCanceled &&
         (PRInt32)theChar == mAccessKey)
     {
       // The access key was down and is now up, and no other
       // keys were pressed in between.
+      if (!mMenuBarFrame->IsActive()) {
+        mMenuBarFrame->SetActiveByKeyboard();
+      }
       ToggleMenuActiveState();
     }
     mAccessKeyDown = PR_FALSE;
     mAccessKeyDownCanceled = PR_FALSE;
 
     PRBool active = mMenuBarFrame->IsActive();
     if (active) {
       aKeyEvent->StopPropagation();
@@ -252,16 +255,17 @@ nsMenuBarListener::KeyPress(nsIDOMEvent*
       }
 
       if (IsAccessKeyPressed(keyEvent) && hasAccessKeyCandidates) {
         // Do shortcut navigation.
         // A letter was pressed. We want to see if a shortcut gets matched. If
         // so, we'll know the menu got activated.
         nsMenuFrame* result = mMenuBarFrame->FindMenuWithShortcut(keyEvent);
         if (result) {
+          mMenuBarFrame->SetActiveByKeyboard();
           mMenuBarFrame->SetActive(PR_TRUE);
           result->OpenMenu(PR_TRUE);
 
           // The opened menu will listen next keyup event.
           // Therefore, we should clear the keydown flags here.
           mAccessKeyDown = mAccessKeyDownCanceled = PR_FALSE;
 
           aKeyEvent->StopPropagation();
@@ -270,16 +274,17 @@ nsMenuBarListener::KeyPress(nsIDOMEvent*
         }
       }    
 #ifndef XP_MACOSX
       // Also need to handle F10 specially on Non-Mac platform.
       else if (keyCode == NS_VK_F10) {
         if ((GetModifiers(keyEvent) & ~MODIFIER_CONTROL) == 0) {
           // The F10 key just went down by itself or with ctrl pressed.
           // In Windows, both of these activate the menu bar.
+          mMenuBarFrame->SetActiveByKeyboard();
           ToggleMenuActiveState();
 
           aKeyEvent->StopPropagation();
           aKeyEvent->PreventDefault();
           return NS_OK; // consume the event
         }
       }
 #endif // !XP_MACOSX
--- a/layout/xul/base/src/nsMenuBoxObject.cpp
+++ b/layout/xul/base/src/nsMenuBoxObject.cpp
@@ -36,16 +36,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 #include "nsISupportsUtils.h"
 #include "nsIMenuBoxObject.h"
 #include "nsBoxObject.h"
 #include "nsIFrame.h"
 #include "nsGUIEvent.h"
 #include "nsIDOMNSUIEvent.h"
+#include "nsMenuBarFrame.h"
 #include "nsMenuBarListener.h"
 #include "nsMenuFrame.h"
 #include "nsMenuPopupFrame.h"
 
 class nsMenuBoxObject : public nsIMenuBoxObject, public nsBoxObject
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
@@ -152,16 +153,38 @@ NS_IMETHODIMP nsMenuBoxObject::HandleKey
       return NS_OK;
     }
     default:
       *aHandledFlag = pm->HandleShortcutNavigation(aKeyEvent, popupFrame);
       return NS_OK;
   }
 }
 
+NS_IMETHODIMP
+nsMenuBoxObject::GetOpenedWithKey(PRBool* aOpenedWithKey)
+{
+  *aOpenedWithKey = PR_FALSE;
+
+  nsIFrame* frame = GetFrame(PR_FALSE);
+  if (!frame || frame->GetType() != nsGkAtoms::menuFrame)
+    return NS_OK;
+
+  frame = frame->GetParent();
+  while (frame) {
+    if (frame->GetType() == nsGkAtoms::menuBarFrame) {
+      *aOpenedWithKey = (static_cast<nsMenuBarFrame *>(frame))->IsActiveByKeyboard();
+      return NS_OK;
+    }
+    frame = frame->GetParent();
+  }
+
+  return NS_OK;
+}
+
+
 // Creation Routine ///////////////////////////////////////////////////////////////////////
 
 nsresult
 NS_NewMenuBoxObject(nsIBoxObject** aResult)
 {
   *aResult = new nsMenuBoxObject;
   if (!*aResult)
     return NS_ERROR_OUT_OF_MEMORY;
--- a/toolkit/content/tests/widgets/window_menubar.xul
+++ b/toolkit/content/tests/widgets/window_menubar.xul
@@ -80,29 +80,32 @@
 </menubar>
 </hbox>
 
 <script class="testbody" type="application/javascript">
 <![CDATA[
 
 window.opener.SimpleTest.waitForFocus(function () {
   gFilePopup = document.getElementById("filepopup");
-  document.getElementById("filemenu").focus();
+  var filemenu = document.getElementById("filemenu");
+  filemenu.focus();
+  is(filemenu.openedWithKey, false, "initial openedWithKey");
   startPopupTests(popupTests);
 }, window);
 
 var popupTests = [
 {
   testname: "press on menu",
   events: [ "popupshowing filepopup", "DOMMenuBarActive menubar",
             "DOMMenuItemActive filemenu", "popupshown filepopup" ],
   test: function() { synthesizeMouse(document.getElementById("filemenu"), 8, 8, { }); },
   result: function (testname) {
     checkActive(gFilePopup, "", testname);
     checkOpen("filemenu", testname);
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
   }
 },
 {
   // check that pressing cursor down while there is no selection
   // highlights the first item
   testname: "cursor down no selection",
   events: [ "DOMMenuItemActive item1" ],
   test: function() { synthesizeKey("VK_DOWN", { }); },
@@ -163,16 +166,17 @@ var popupTests = [
     return elist;
   },
   test: function() { synthesizeKey("VK_RIGHT", { }); },
   result: function(testname) {
     var expected = (navigator.platform.indexOf("Win") == 0) ? "cut" : "copy";
     checkActive(document.getElementById("editpopup"), expected, testname);
     checkClosed("filemenu", testname);
     checkOpen("editmenu", testname);
+    is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey");
   }
 },
 {
   // on Windows, a disabled item is selected, so pressing ENTER should close
   // the menu but not fire a command event
   testname: "enter on disabled",
   events: function() {
     if (navigator.platform.indexOf("Win") == 0)
@@ -183,28 +187,34 @@ var popupTests = [
     else
       return [ "DOMMenuItemInactive copy", "DOMMenuInactive editpopup",
                "DOMMenuBarInactive menubar",
                "DOMMenuItemInactive editmenu", "DOMMenuItemInactive editmenu",
                "command copy", "popuphiding editpopup", "popuphidden editpopup",
                "DOMMenuItemInactive copy" ];
   },
   test: function() { synthesizeKey("VK_ENTER", { }); },
-  result: function(testname) { checkClosed("editmenu", testname); }
+  result: function(testname) {
+    checkClosed("editmenu", testname);
+    is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey");
+  }
 },
 {
   // pressing Alt + a key should open the corresponding menu
   testname: "open with accelerator",
   events: function() {
     return [ "DOMMenuBarActive menubar",
              "popupshowing viewpopup", "DOMMenuItemActive viewmenu",
              "DOMMenuItemActive toolbar", "popupshown viewpopup" ];
   },
   test: function() { synthesizeKey("V", { altKey: true }); },
-  result: function(testname) { checkOpen("viewmenu", testname); }
+  result: function(testname) {
+    checkOpen("viewmenu", testname);
+    is(document.getElementById("viewmenu").openedWithKey, true, testname + " openedWithKey");
+  }
 },
 {
   // open the submenu with the cursor right key
   testname: "open submenu with cursor right",
   events: function() {
     // on Windows, the disabled 'navigation' item can stll be highlihted
     if (navigator.platform.indexOf("Win") == 0)
       return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation",
@@ -284,17 +294,20 @@ var popupTests = [
   result: function(testname) { checkClosed("viewmenu", testname); },
 },
 {
   // on Windows, pressing Alt should highlight the first menu but not open it
   testname: "alt to activate menubar",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_ALT", { }); },
-  result: function(testname) { checkClosed("filemenu", testname); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey");
+    checkClosed("filemenu", testname);
+  },
 },
 {
   // pressing cursor left should select the previous menu but not open it
   testname: "cursor left on active menubar",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu" ],
   test: function() { synthesizeKey("VK_LEFT", { }); },
   result: function(testname) { checkClosed("helpmenu", testname); },
@@ -310,26 +323,32 @@ var popupTests = [
 {
   // pressing a character should act as an accelerator and open the menu
   testname: "accelerator on active menubar",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "popupshowing helppopup",
             "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu",
             "DOMMenuItemActive contents", "popupshown helppopup" ],
   test: function() { synthesizeKey("H", { }); },
-  result: function(testname) { checkOpen("helpmenu", testname); },
+  result: function(testname) {
+    checkOpen("helpmenu", testname);
+    is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey");
+  },
 },
 {
   testname: "open with accelerator again",
   condition: function() { return (navigator.platform.indexOf("Win") == -1) },
   events: [ "DOMMenuBarActive menubar", "popupshowing helppopup",
             "DOMMenuItemActive helpmenu", "DOMMenuItemActive contents",
             "popupshown helppopup" ],
   test: function() { synthesizeKey("H", { altKey: true }); },
-  result: function(testname) { checkOpen("helpmenu", testname); },
+  result: function(testname) {
+    checkOpen("helpmenu", testname);
+    is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey");
+  },
 },
 {
   // check that pressing cursor up skips non menuitems
   testname: "cursor up wrap",
   events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ],
   test: function() { synthesizeKey("VK_UP", { }); },
   result: function(testname) { }
 },
@@ -353,29 +372,35 @@ var popupTests = [
   test: function() { synthesizeKey("M", { }); },
   result: function(testname) { checkClosed("helpmenu", testname); }
 },
 {
   // pressing F10 should highlight the first menu but not open it
   testname: "F10 to activate menubar",
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_F10", { }); },
-  result: function(testname) { checkClosed("filemenu", testname); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey");
+    checkClosed("filemenu", testname);
+  },
 },
 {
   // pressing cursor down should open a menu
   testname: "cursor down on menu",
   events: [ 
             "popupshowing helppopup",
             "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu",
             // This is in a different order than the
             // "accelerator on active menubar" because menus opened from a
             // shortcut key are fired asynchronously
             "DOMMenuItemActive contents", "popupshown helppopup" ],
   test: function() { synthesizeKey("VK_LEFT", { }); synthesizeKey("VK_DOWN", { }); },
+  result: function(testname) {
+    is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey");
+  }
 },
 {
   // pressing a letter that doesn't correspond to an accelerator. The menu
   // should not close because there is more than one item corresponding to
   // that letter
   testname: "menuitem with no accelerator",
   events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive one" ],
   test: function() { synthesizeKey("O", { }); },
@@ -431,17 +456,20 @@ var popupTests = [
   result: function(testname) { checkClosed("filemenu", testname); },
 },
 {
   // pressing an accelerator for a disabled item should deactivate the menubar
   testname: "accelerator for disabled menu",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ],
   test: function() { synthesizeKey("S", { }); },
-  result: function(testname) { checkClosed("secretmenu", testname); },
+  result: function(testname) {
+    checkClosed("secretmenu", testname);
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
+  },
 },
 {
   testname: "press on disabled menu",
   test: function() {
     synthesizeMouse(document.getElementById("secretmenu"), 8, 8, { });
   },
   result: function (testname) {
     checkClosed("secretmenu", testname);
@@ -516,48 +544,60 @@ var popupTests = [
   testname: "F10 to activate menubar for tab deactivation",
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_F10", { }); },
 },
 {
   testname: "Deactivate menubar with tab key",
   events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ],
   test: function() { synthesizeKey("VK_TAB", { }); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
+  }
 },
 {
   testname: "F10 to activate menubar for escape deactivation",
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_F10", { }); },
 },
 {
   testname: "Deactivate menubar with escape key",
   events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ],
   test: function() { synthesizeKey("VK_ESCAPE", { }); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
+  }
 },
 {
   testname: "F10 to activate menubar for f10 deactivation",
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_F10", { }); },
 },
 {
   testname: "Deactivate menubar with f10 key",
   events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu"  ],
   test: function() { synthesizeKey("VK_F10", { }); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
+  }
 },
 {
   testname: "F10 to activate menubar for alt deactivation",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ],
   test: function() { synthesizeKey("VK_F10", { }); },
 },
 {
   testname: "Deactivate menubar with alt key",
   condition: function() { return (navigator.platform.indexOf("Win") == 0) },
   events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu"  ],
   test: function() { synthesizeKey("VK_ALT", { }); },
+  result: function(testname) {
+    is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey");
+  }
 },
 {
   testname: "Don't activate menubar with mousedown during alt key auto-repeat",
   test: function() {
     synthesizeKey("VK_ALT", { type: "keydown" });
     synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mousedown", altKey: true });
     synthesizeKey("VK_ALT", { type: "keydown" });
     synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mouseup", altKey: true });
@@ -623,10 +663,9 @@ var popupTests = [
   }
 }
 
 ];
 
 ]]>
 </script>
 
-
 </window>
--- a/toolkit/content/widgets/menu.xml
+++ b/toolkit/content/widgets/menu.xml
@@ -55,16 +55,22 @@
       <property name="open" onget="return this.hasAttribute('open');">
         <setter><![CDATA[
           this.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject)
               .openMenu(val);
           return val;
         ]]></setter>
       </property>
 
+      <property name="openedWithKey" readonly="true">
+        <getter><![CDATA[
+          return this.boxObject.QueryInterface(Components.interfaces.nsIMenuBoxObject).openedWithKey;
+        ]]></getter>
+      </property>
+
       <!-- nsIDOMXULContainerElement interface -->
       <method name="appendItem">
         <parameter name="aLabel"/>
         <parameter name="aValue"/>
         <body>
           return this.insertItemAt(-1, aLabel, aValue);
         </body>
       </method>