Bug 355375 - Make context menuitems blink when chosen. r=enn

--HG--
extra : rebase_source : 87b93bda596b4af4c1adf294ecc91510047ca0f1
This commit is contained in:
Markus Stange 2010-04-19 16:12:58 +02:00
parent 6cdac57e7f
commit c34c8948f8
12 changed files with 330 additions and 55 deletions

View File

@ -271,7 +271,7 @@ public:
PRBool aAlt,
PRBool aMeta,
PRBool aUserInput,
CloseMenuMode aCloseMenuMode)
PRBool aFlipChecked)
: mMenu(aMenu),
mIsTrusted(aIsTrusted),
mShift(aShift),
@ -279,13 +279,16 @@ public:
mAlt(aAlt),
mMeta(aMeta),
mUserInput(aUserInput),
mCloseMenuMode(aCloseMenuMode)
mFlipChecked(aFlipChecked),
mCloseMenuMode(CloseMenuMode_Auto)
{
NS_ASSERTION(aMenu, "null menu supplied to nsXULMenuCommandEvent constructor");
}
NS_IMETHOD Run();
void SetCloseMenuMode(CloseMenuMode aCloseMenuMode) { mCloseMenuMode = aCloseMenuMode; }
private:
nsCOMPtr<nsIContent> mMenu;
PRBool mIsTrusted;
@ -294,6 +297,7 @@ private:
PRBool mAlt;
PRBool mMeta;
PRBool mUserInput;
PRBool mFlipChecked;
CloseMenuMode mCloseMenuMode;
};
@ -480,10 +484,10 @@ public:
* Execute a menu command from the triggering event aEvent.
*
* aMenu - a menuitem to execute
* aEvent - the mouse event which triggered the menu to be executed,
* may be null
* aEvent - an nsXULMenuCommandEvent that contains all the info from the mouse
* event which triggered the menu to be executed, may not be null
*/
void ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent);
void ExecuteMenu(nsIContent* aMenu, nsXULMenuCommandEvent* aEvent);
/**
* Return true if the popup for the supplied content node is open.

View File

@ -91,6 +91,9 @@ public:
virtual nsIAtom* GetType() const { return nsGkAtoms::menuBarFrame; }
virtual void LockMenuUntilClosed(PRBool aLock) {}
virtual PRBool IsMenuLocked() { return PR_FALSE; }
// Non-interface helpers
void

View File

@ -80,6 +80,8 @@
#include "nsDisplayList.h"
#include "nsIReflowCallback.h"
#include "nsISound.h"
#include "nsEventStateManager.h"
#include "nsIDOMXULMenuListElement.h"
#define NS_MENU_POPUP_LIST_INDEX 0
@ -97,6 +99,7 @@ nsString *nsMenuFrame::gControlText = nsnull;
nsString *nsMenuFrame::gMetaText = nsnull;
nsString *nsMenuFrame::gAltText = nsnull;
nsString *nsMenuFrame::gModifierSeparator = nsnull;
const PRInt32 kBlinkDelay = 67; // milliseconds
// this class is used for dispatching menu activation events asynchronously.
class nsMenuActivateEvent : public nsRunnable
@ -191,7 +194,8 @@ nsMenuFrame::nsMenuFrame(nsIPresShell* aShell, nsStyleContext* aContext):
mChecked(PR_FALSE),
mType(eMenuType_Normal),
mMenuParent(nsnull),
mPopupFrame(nsnull)
mPopupFrame(nsnull),
mBlinkState(0)
{
} // cntr
@ -376,6 +380,8 @@ nsMenuFrame::DestroyFrom(nsIFrame* aDestructRoot)
mOpenTimer->Cancel();
}
StopBlinking();
// Null out the pointer to this frame in the mediator wrapper so that it
// doesn't try to interact with a deallocated frame.
mTimerMediator->ClearFrame();
@ -417,7 +423,8 @@ nsMenuFrame::HandleEvent(nsPresContext* aPresContext,
nsEventStatus* aEventStatus)
{
NS_ENSURE_ARG_POINTER(aEventStatus);
if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
if (nsEventStatus_eConsumeNoDefault == *aEventStatus ||
(mMenuParent && mMenuParent->IsMenuLocked())) {
return NS_OK;
}
@ -897,6 +904,31 @@ nsMenuFrame::Notify(nsITimer* aTimer)
}
}
}
} else if (aTimer == mBlinkTimer) {
switch (mBlinkState++) {
case 0:
NS_ASSERTION(false, "Blink timer fired while not blinking");
StopBlinking();
break;
case 1:
{
// Turn the highlight back on and wait for a while before closing the menu.
nsWeakFrame weakFrame(this);
mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive,
NS_LITERAL_STRING("true"), PR_TRUE);
if (weakFrame.IsAlive()) {
aTimer->InitWithCallback(mTimerMediator, kBlinkDelay, nsITimer::TYPE_ONE_SHOT);
}
}
break;
default:
if (mMenuParent) {
mMenuParent->LockMenuUntilClosed(PR_FALSE);
}
PassMenuCommandEventToPopupManager();
StopBlinking();
break;
}
}
return NS_OK;
@ -1154,31 +1186,123 @@ nsMenuFrame::BuildAcceleratorText()
void
nsMenuFrame::Execute(nsGUIEvent *aEvent)
{
nsWeakFrame weakFrame(this);
// flip "checked" state if we're a checkbox menu, or an un-checked radio menu
PRBool needToFlipChecked = PR_FALSE;
if (mType == eMenuType_Checkbox || (mType == eMenuType_Radio && !mChecked)) {
if (!mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
nsGkAtoms::_false, eCaseMatters)) {
if (mChecked) {
mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked,
PR_TRUE);
ENSURE_TRUE(weakFrame.IsAlive());
}
else {
mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, NS_LITERAL_STRING("true"),
PR_TRUE);
ENSURE_TRUE(weakFrame.IsAlive());
}
}
needToFlipChecked = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck,
nsGkAtoms::_false, eCaseMatters);
}
nsCOMPtr<nsISound> sound(do_CreateInstance("@mozilla.org/sound;1"));
if (sound)
sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE);
StartBlinking(aEvent, needToFlipChecked);
}
PRBool
nsMenuFrame::ShouldBlink()
{
PRInt32 shouldBlink = 0;
nsCOMPtr<nsILookAndFeel> lookAndFeel(do_GetService(kLookAndFeelCID));
if (lookAndFeel) {
lookAndFeel->GetMetric(nsILookAndFeel::eMetric_ChosenMenuItemsShouldBlink, shouldBlink);
}
if (!shouldBlink)
return PR_FALSE;
// Don't blink in editable menulists.
if (mMenuParent && mMenuParent->IsMenu()) {
nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(mMenuParent);
nsIFrame* parentMenu = popupFrame->GetParent();
if (parentMenu) {
nsCOMPtr<nsIDOMXULMenuListElement> menulist = do_QueryInterface(parentMenu->GetContent());
if (menulist) {
PRBool isEditable = PR_FALSE;
menulist->GetEditable(&isEditable);
return !isEditable;
}
}
}
return PR_TRUE;
}
void
nsMenuFrame::StartBlinking(nsGUIEvent *aEvent, PRBool aFlipChecked)
{
StopBlinking();
CreateMenuCommandEvent(aEvent, aFlipChecked);
if (!ShouldBlink()) {
PassMenuCommandEventToPopupManager();
return;
}
// Blink off.
nsWeakFrame weakFrame(this);
mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, PR_TRUE);
if (!weakFrame.IsAlive())
return;
if (mMenuParent) {
// Make this menu ignore events from now on.
mMenuParent->LockMenuUntilClosed(PR_TRUE);
}
// Set up a timer to blink back on.
mBlinkTimer = do_CreateInstance("@mozilla.org/timer;1");
mBlinkTimer->InitWithCallback(mTimerMediator, kBlinkDelay, nsITimer::TYPE_ONE_SHOT);
mBlinkState = 1;
}
void
nsMenuFrame::StopBlinking()
{
mBlinkState = 0;
if (mBlinkTimer) {
mBlinkTimer->Cancel();
mBlinkTimer = nsnull;
}
mDelayedMenuCommandEvent = nsnull;
}
void
nsMenuFrame::CreateMenuCommandEvent(nsGUIEvent *aEvent, PRBool aFlipChecked)
{
// Create a trusted event if the triggering event was trusted, or if
// we're called from chrome code (since at least one of our caller
// passes in a null event).
PRBool isTrusted = aEvent ? NS_IS_TRUSTED_EVENT(aEvent) :
nsContentUtils::IsCallerChrome();
PRBool shift = PR_FALSE, control = PR_FALSE, alt = PR_FALSE, meta = PR_FALSE;
if (aEvent && (aEvent->eventStructType == NS_MOUSE_EVENT ||
aEvent->eventStructType == NS_KEY_EVENT ||
aEvent->eventStructType == NS_ACCESSIBLE_EVENT)) {
shift = static_cast<nsInputEvent *>(aEvent)->isShift;
control = static_cast<nsInputEvent *>(aEvent)->isControl;
alt = static_cast<nsInputEvent *>(aEvent)->isAlt;
meta = static_cast<nsInputEvent *>(aEvent)->isMeta;
}
// Because the command event is firing asynchronously, a flag is needed to
// indicate whether user input is being handled. This ensures that a popup
// window won't get blocked.
PRBool userinput = nsEventStateManager::IsHandlingUserInput();
mDelayedMenuCommandEvent =
new nsXULMenuCommandEvent(mContent, isTrusted, shift, control, alt, meta,
userinput, aFlipChecked);
}
void
nsMenuFrame::PassMenuCommandEventToPopupManager()
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm && mMenuParent)
pm->ExecuteMenu(mContent, aEvent);
if (pm && mMenuParent && mDelayedMenuCommandEvent) {
pm->ExecuteMenu(mContent, mDelayedMenuCommandEvent);
}
mDelayedMenuCommandEvent = nsnull;
}
NS_IMETHODIMP

View File

@ -253,6 +253,12 @@ protected:
PRBool SizeToPopup(nsBoxLayoutState& aState, nsSize& aSize);
PRBool ShouldBlink();
void StartBlinking(nsGUIEvent *aEvent, PRBool aFlipChecked);
void StopBlinking();
void CreateMenuCommandEvent(nsGUIEvent *aEvent, PRBool aFlipChecked);
void PassMenuCommandEventToPopupManager();
protected:
#ifdef DEBUG_LAYOUT
nsresult SetDebug(nsBoxLayoutState& aState, nsIFrame* aList, PRBool aDebug);
@ -272,6 +278,10 @@ protected:
nsRefPtr<nsMenuTimerMediator> mTimerMediator;
nsCOMPtr<nsITimer> mOpenTimer;
nsCOMPtr<nsITimer> mBlinkTimer;
PRUint8 mBlinkState; // 0: not blinking, 1: off, 2: on
nsRefPtr<nsXULMenuCommandEvent> mDelayedMenuCommandEvent;
nsString mGroupName;

View File

@ -85,6 +85,14 @@ public:
// cleared. This should return true if the menu should be deselected
// by the caller.
virtual PRBool MenuClosed() = 0;
// Lock this menu and its parents until they're closed or unlocked.
// A menu being "locked" means that all events inside it that would change the
// selected menu item should be ignored.
// This is used when closing the popup is delayed because of a blink or fade
// animation.
virtual void LockMenuUntilClosed(PRBool aLock) = 0;
virtual PRBool IsMenuLocked() = 0;
};
#endif

View File

@ -121,6 +121,7 @@ nsMenuPopupFrame::nsMenuPopupFrame(nsIPresShell* aShell, nsStyleContext* aContex
mShouldAutoPosition(PR_TRUE),
mConsumeRollupEvent(nsIPopupBoxObject::ROLLUP_DEFAULT),
mInContentShell(PR_TRUE),
mIsMenuLocked(PR_FALSE),
mHFlip(PR_FALSE),
mVFlip(PR_FALSE)
{
@ -690,6 +691,8 @@ nsMenuPopupFrame::HidePopup(PRBool aDeselectMenu, nsPopupState aNewState)
mIncrementalString.Truncate();
LockMenuUntilClosed(PR_FALSE);
mIsOpenChanged = PR_FALSE;
mCurrentMenu = nsnull; // make sure no current menu is set
mHFlip = mVFlip = PR_FALSE;
@ -1504,6 +1507,21 @@ nsMenuPopupFrame::FindMenuWithShortcut(nsIDOMKeyEvent* aKeyEvent, PRBool& doActi
return nsnull;
}
void
nsMenuPopupFrame::LockMenuUntilClosed(PRBool aLock)
{
mIsMenuLocked = aLock;
// Lock / unlock the parent, too.
nsIFrame* parent = GetParent();
if (parent && parent->GetType() == nsGkAtoms::menuFrame) {
nsMenuParent* parentParent = static_cast<nsMenuFrame*>(parent)->GetMenuParent();
if (parentParent) {
parentParent->LockMenuUntilClosed(aLock);
}
}
}
NS_IMETHODIMP
nsMenuPopupFrame::GetWidget(nsIWidget **aWidget)
{

View File

@ -158,6 +158,9 @@ public:
virtual PRBool MenuClosed() { return PR_TRUE; }
virtual void LockMenuUntilClosed(PRBool aLock);
virtual PRBool IsMenuLocked() { return mIsMenuLocked; }
NS_IMETHOD GetWidget(nsIWidget **aWidget);
// The dismissal listener gets created and attached to the window.
@ -395,6 +398,7 @@ protected:
PRPackedBool mShouldAutoPosition; // Should SetPopupPosition be allowed to auto position popup?
PRPackedBool mConsumeRollupEvent; // Should the rollup event be consumed?
PRPackedBool mInContentShell; // True if the popup is in a content shell
PRPackedBool mIsMenuLocked; // Should events inside this menu be ignored?
// the flip modes that were used when the popup was opened
PRPackedBool mHFlip;

View File

@ -931,7 +931,7 @@ nsXULPopupManager::HidePopupsInDocShell(nsIDocShellTreeItem* aDocShellToHide)
}
void
nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent)
nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsXULMenuCommandEvent* aEvent)
{
CloseMenuMode cmm = CloseMenuMode_Auto;
@ -974,30 +974,8 @@ nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsEvent* aEvent)
HidePopupsInList(popupsToHide, cmm == CloseMenuMode_Auto);
}
// Create a trusted event if the triggering event was trusted, or if
// we're called from chrome code (since at least one of our caller
// passes in a null event).
PRBool isTrusted = aEvent ? NS_IS_TRUSTED_EVENT(aEvent) :
nsContentUtils::IsCallerChrome();
PRBool shift = PR_FALSE, control = PR_FALSE, alt = PR_FALSE, meta = PR_FALSE;
if (aEvent && (aEvent->eventStructType == NS_MOUSE_EVENT ||
aEvent->eventStructType == NS_KEY_EVENT ||
aEvent->eventStructType == NS_ACCESSIBLE_EVENT)) {
shift = static_cast<nsInputEvent *>(aEvent)->isShift;
control = static_cast<nsInputEvent *>(aEvent)->isControl;
alt = static_cast<nsInputEvent *>(aEvent)->isAlt;
meta = static_cast<nsInputEvent *>(aEvent)->isMeta;
}
// Because the command event is firing asynchronously, a flag is needed to
// indicate whether user input is being handled. This ensures that a popup
// window won't get blocked.
PRBool userinput = nsEventStateManager::IsHandlingUserInput();
nsCOMPtr<nsIRunnable> event =
new nsXULMenuCommandEvent(aMenu, isTrusted, shift, control,
alt, meta, userinput, cmm);
aEvent->SetCloseMenuMode(cmm);
nsCOMPtr<nsIRunnable> event = aEvent;
NS_DispatchToCurrentThread(event);
}
@ -1898,12 +1876,13 @@ nsXULPopupManager::KeyUp(nsIDOMEvent* aKeyEvent)
nsresult
nsXULPopupManager::KeyDown(nsIDOMEvent* aKeyEvent)
{
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && item->Frame()->IsMenuLocked())
return NS_OK;
// don't do anything if a menu isn't open or a menubar isn't active
if (!mActiveMenuBar) {
nsMenuChainItem* item = GetTopVisibleMenu();
if (!item || item->PopupType() != ePopupTypeMenu)
return NS_OK;
}
if (!mActiveMenuBar && (!item || item->PopupType() != ePopupTypeMenu))
return NS_OK;
PRInt32 menuAccessKey = -1;
@ -1954,6 +1933,10 @@ nsXULPopupManager::KeyPress(nsIDOMEvent* aKeyEvent)
// When a menu is open, the prevent default flag on a keypress is always set, so
// that no one else uses the key event.
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && item->Frame()->IsMenuLocked())
return NS_OK;
//handlers shouldn't be triggered by non-trusted events.
nsCOMPtr<nsIDOMNSEvent> domNSEvent = do_QueryInterface(aKeyEvent);
PRBool trustedEvent = PR_FALSE;
@ -1970,7 +1953,6 @@ nsXULPopupManager::KeyPress(nsIDOMEvent* aKeyEvent)
keyEvent->GetKeyCode(&theChar);
// Escape should close panels, but the other keys should have no effect.
nsMenuChainItem* item = GetTopVisibleMenu();
if (item && item->PopupType() != ePopupTypeMenu) {
if (theChar == NS_VK_ESCAPE) {
HidePopup(item->Content(), PR_FALSE, PR_FALSE, PR_FALSE);
@ -2094,7 +2076,17 @@ nsXULMenuCommandEvent::Run()
nsCOMPtr<nsIContent> popup;
nsMenuFrame* menuFrame = pm->GetMenuFrameForContent(mMenu);
if (menuFrame) {
nsWeakFrame weakFrame(menuFrame);
if (menuFrame && mFlipChecked) {
if (menuFrame->IsChecked()) {
mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, PR_TRUE);
} else {
mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
NS_LITERAL_STRING("true"), PR_TRUE);
}
}
if (menuFrame && weakFrame.IsAlive()) {
// Find the popup that the menu is inside. Below, this popup will
// need to be hidden.
nsIFrame* popupFrame = menuFrame->GetParent();

View File

@ -56,6 +56,7 @@ _TEST_FILES = test_bug360220.xul \
test_closemenu_attribute.xul \
test_colorpicker_popup.xul \
test_deck.xul \
test_menuitem_blink.xul \
test_menulist_keynav.xul \
test_menulist_null_value.xul \
test_popup_coords.xul \

View File

@ -0,0 +1,107 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
<window title="Blinking Context Menu Item Tests"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<menulist id="menulist">
<menupopup id="menupopup">
<menuitem label="Menu Item" id="menuitem"/>
</menupopup>
</menulist>
<script class="testbody" type="application/javascript">
<![CDATA[
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(startTest);
function startTest() {
if (!/Mac/.test(navigator.platform)) {
ok(true, "Nothing to test on non-Mac.");
SimpleTest.finish();
return;
}
// Destroy frame while removing the _moz-menuactive attribute.
test_crash("REMOVAL", test2);
}
function test2() {
// Destroy frame while adding the _moz-menuactive attribute.
test_crash("ADDITION", test3);
}
function test3() {
// Don't mess with the frame, just test whether we've blinked.
test_crash("", SimpleTest.finish);
}
function test_crash(when, andThen) {
var menupopup = document.getElementById("menupopup");
var menuitem = document.getElementById("menuitem");
var attrChanges = { "REMOVAL": 0, "ADDITION": 0 };
var storedEvent = null;
menupopup.addEventListener("popupshown", function () {
menupopup.removeEventListener("popupshown", arguments.callee, false);
menuitem.addEventListener("mouseup", function (e) {
menuitem.removeEventListener("mouseup", arguments.callee, true);
menuitem.addEventListener("DOMAttrModified", function (e) {
if (e.attrName == "_moz-menuactive") {
if (!attrChanges[e.attrChange])
attrChanges[e.attrChange] = 1;
else
attrChanges[e.attrChange]++;
storedEvent = e;
if (e.attrChange == e[when]) {
menuitem.hidden = true;
menuitem.getBoundingClientRect();
ok(true, "Didn't crash on _moz-menuactive " + when.toLowerCase() + " during blinking")
menuitem.hidden = false;
menuitem.removeEventListener("DOMAttrModified", arguments.callee, false);
SimpleTest.executeSoon(function () {
menupopup.hidePopup();
});
}
}
}, false);
}, true);
menupopup.addEventListener("popuphidden", function() {
menupopup.removeEventListener("popuphidden", arguments.callee, false);
if (!when) {
// Test whether we've blinked at all.
var shouldBlink = navigator.platform.match(/Mac/);
var expectedNumRemoval = shouldBlink ? 2 : 1;
var expectedNumAddition = shouldBlink ? 1 : 0;
ok(storedEvent, "got DOMAttrModified events after clicking menuitem")
is(attrChanges[storedEvent.REMOVAL], expectedNumRemoval, "blinking unset attributes correctly");
is(attrChanges[storedEvent.ADDITION], expectedNumAddition, "blinking set attributes correctly");
}
SimpleTest.executeSoon(andThen);
}, false);
synthesizeMouse(menuitem, 10, 5, { type : "mousemove" });
synthesizeMouse(menuitem, 10, 5, { type : "mousemove" });
synthesizeMouse(menuitem, 10, 5, { type : "mousedown" });
SimpleTest.executeSoon(function () {
synthesizeMouse(menuitem, 10, 5, { type : "mouseup" });
});
}, false);
document.getElementById("menulist").open = true;
}
]]>
</script>
<body xmlns="http://www.w3.org/1999/xhtml">
<p id="display">
</p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</window>

View File

@ -225,6 +225,7 @@ public:
eMetric_TreeScrollDelay, // delay for scrolling the tree
eMetric_TreeScrollLinesMax, // the maximum number of lines to be scrolled at ones
eMetric_TabFocusModel, // What type of tab-order to use
eMetric_ChosenMenuItemsShouldBlink, // Should menu items blink when they're chosen?
/*
* A Boolean value to determine whether the Windows default theme is

View File

@ -467,6 +467,9 @@ NS_IMETHODIMP nsLookAndFeel::GetMetric(const nsMetricID aID, PRInt32 & aMetric)
aMetric = [[NSUserDefaults standardUserDefaults] boolForKey:@"AppleScrollerPagingBehavior"];
}
break;
case eMetric_ChosenMenuItemsShouldBlink:
aMetric = 1;
break;
case eMetric_IMERawInputUnderlineStyle:
case eMetric_IMEConvertedTextUnderlineStyle:
case eMetric_IMESelectedRawTextUnderlineStyle: