|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 #include "nsGkAtoms.h" |
|
7 #include "nsHTMLParts.h" |
|
8 #include "nsMenuFrame.h" |
|
9 #include "nsBoxFrame.h" |
|
10 #include "nsIContent.h" |
|
11 #include "nsIAtom.h" |
|
12 #include "nsPresContext.h" |
|
13 #include "nsIPresShell.h" |
|
14 #include "nsStyleContext.h" |
|
15 #include "nsCSSRendering.h" |
|
16 #include "nsNameSpaceManager.h" |
|
17 #include "nsMenuPopupFrame.h" |
|
18 #include "nsMenuBarFrame.h" |
|
19 #include "nsIDocument.h" |
|
20 #include "nsIDOMElement.h" |
|
21 #include "nsIComponentManager.h" |
|
22 #include "nsBoxLayoutState.h" |
|
23 #include "nsIScrollableFrame.h" |
|
24 #include "nsBindingManager.h" |
|
25 #include "nsIServiceManager.h" |
|
26 #include "nsCSSFrameConstructor.h" |
|
27 #include "nsIDOMKeyEvent.h" |
|
28 #include "nsXPIDLString.h" |
|
29 #include "nsReadableUtils.h" |
|
30 #include "nsUnicharUtils.h" |
|
31 #include "nsIStringBundle.h" |
|
32 #include "nsContentUtils.h" |
|
33 #include "nsDisplayList.h" |
|
34 #include "nsIReflowCallback.h" |
|
35 #include "nsISound.h" |
|
36 #include "nsIDOMXULMenuListElement.h" |
|
37 #include "mozilla/Attributes.h" |
|
38 #include "mozilla/EventDispatcher.h" |
|
39 #include "mozilla/EventStateManager.h" |
|
40 #include "mozilla/Likely.h" |
|
41 #include "mozilla/LookAndFeel.h" |
|
42 #include "mozilla/MouseEvents.h" |
|
43 #include "mozilla/Preferences.h" |
|
44 #include "mozilla/Services.h" |
|
45 #include "mozilla/TextEvents.h" |
|
46 #include "mozilla/dom/Element.h" |
|
47 #include <algorithm> |
|
48 |
|
49 using namespace mozilla; |
|
50 |
|
51 #define NS_MENU_POPUP_LIST_INDEX 0 |
|
52 |
|
53 #if defined(XP_WIN) |
|
54 #define NSCONTEXTMENUISMOUSEUP 1 |
|
55 #endif |
|
56 |
|
57 static void |
|
58 AssertNotCalled(void* aPropertyValue) |
|
59 { |
|
60 NS_ERROR("popup list should never be destroyed by the FramePropertyTable"); |
|
61 } |
|
62 NS_DECLARE_FRAME_PROPERTY(PopupListProperty, AssertNotCalled) |
|
63 |
|
64 static int32_t gEatMouseMove = false; |
|
65 |
|
66 const int32_t kBlinkDelay = 67; // milliseconds |
|
67 |
|
68 // this class is used for dispatching menu activation events asynchronously. |
|
69 class nsMenuActivateEvent : public nsRunnable |
|
70 { |
|
71 public: |
|
72 nsMenuActivateEvent(nsIContent *aMenu, |
|
73 nsPresContext* aPresContext, |
|
74 bool aIsActivate) |
|
75 : mMenu(aMenu), mPresContext(aPresContext), mIsActivate(aIsActivate) |
|
76 { |
|
77 } |
|
78 |
|
79 NS_IMETHOD Run() MOZ_OVERRIDE |
|
80 { |
|
81 nsAutoString domEventToFire; |
|
82 |
|
83 if (mIsActivate) { |
|
84 // Highlight the menu. |
|
85 mMenu->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, |
|
86 NS_LITERAL_STRING("true"), true); |
|
87 // The menuactivated event is used by accessibility to track the user's |
|
88 // movements through menus |
|
89 domEventToFire.AssignLiteral("DOMMenuItemActive"); |
|
90 } |
|
91 else { |
|
92 // Unhighlight the menu. |
|
93 mMenu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); |
|
94 domEventToFire.AssignLiteral("DOMMenuItemInactive"); |
|
95 } |
|
96 |
|
97 nsCOMPtr<nsIDOMEvent> event; |
|
98 if (NS_SUCCEEDED(EventDispatcher::CreateEvent(mMenu, mPresContext, nullptr, |
|
99 NS_LITERAL_STRING("Events"), |
|
100 getter_AddRefs(event)))) { |
|
101 event->InitEvent(domEventToFire, true, true); |
|
102 |
|
103 event->SetTrusted(true); |
|
104 |
|
105 EventDispatcher::DispatchDOMEvent(mMenu, nullptr, event, |
|
106 mPresContext, nullptr); |
|
107 } |
|
108 |
|
109 return NS_OK; |
|
110 } |
|
111 |
|
112 private: |
|
113 nsCOMPtr<nsIContent> mMenu; |
|
114 nsRefPtr<nsPresContext> mPresContext; |
|
115 bool mIsActivate; |
|
116 }; |
|
117 |
|
118 class nsMenuAttributeChangedEvent : public nsRunnable |
|
119 { |
|
120 public: |
|
121 nsMenuAttributeChangedEvent(nsIFrame* aFrame, nsIAtom* aAttr) |
|
122 : mFrame(aFrame), mAttr(aAttr) |
|
123 { |
|
124 } |
|
125 |
|
126 NS_IMETHOD Run() MOZ_OVERRIDE |
|
127 { |
|
128 nsMenuFrame* frame = static_cast<nsMenuFrame*>(mFrame.GetFrame()); |
|
129 NS_ENSURE_STATE(frame); |
|
130 if (mAttr == nsGkAtoms::checked) { |
|
131 frame->UpdateMenuSpecialState(frame->PresContext()); |
|
132 } else if (mAttr == nsGkAtoms::acceltext) { |
|
133 // someone reset the accelText attribute, |
|
134 // so clear the bit that says *we* set it |
|
135 frame->RemoveStateBits(NS_STATE_ACCELTEXT_IS_DERIVED); |
|
136 frame->BuildAcceleratorText(true); |
|
137 } |
|
138 else if (mAttr == nsGkAtoms::key) { |
|
139 frame->BuildAcceleratorText(true); |
|
140 } else if (mAttr == nsGkAtoms::type || mAttr == nsGkAtoms::name) { |
|
141 frame->UpdateMenuType(frame->PresContext()); |
|
142 } |
|
143 return NS_OK; |
|
144 } |
|
145 protected: |
|
146 nsWeakFrame mFrame; |
|
147 nsCOMPtr<nsIAtom> mAttr; |
|
148 }; |
|
149 |
|
150 // |
|
151 // NS_NewMenuFrame and NS_NewMenuItemFrame |
|
152 // |
|
153 // Wrappers for creating a new menu popup container |
|
154 // |
|
155 nsIFrame* |
|
156 NS_NewMenuFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) |
|
157 { |
|
158 nsMenuFrame* it = new (aPresShell) nsMenuFrame (aPresShell, aContext); |
|
159 it->SetIsMenu(true); |
|
160 return it; |
|
161 } |
|
162 |
|
163 nsIFrame* |
|
164 NS_NewMenuItemFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) |
|
165 { |
|
166 nsMenuFrame* it = new (aPresShell) nsMenuFrame (aPresShell, aContext); |
|
167 it->SetIsMenu(false); |
|
168 return it; |
|
169 } |
|
170 |
|
171 NS_IMPL_FRAMEARENA_HELPERS(nsMenuFrame) |
|
172 |
|
173 NS_QUERYFRAME_HEAD(nsMenuFrame) |
|
174 NS_QUERYFRAME_ENTRY(nsMenuFrame) |
|
175 NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame) |
|
176 |
|
177 nsMenuFrame::nsMenuFrame(nsIPresShell* aShell, nsStyleContext* aContext): |
|
178 nsBoxFrame(aShell, aContext), |
|
179 mIsMenu(false), |
|
180 mChecked(false), |
|
181 mIgnoreAccelTextChange(false), |
|
182 mType(eMenuType_Normal), |
|
183 mMenuParent(nullptr), |
|
184 mBlinkState(0) |
|
185 { |
|
186 } |
|
187 |
|
188 void |
|
189 nsMenuFrame::SetParent(nsIFrame* aParent) |
|
190 { |
|
191 nsBoxFrame::SetParent(aParent); |
|
192 InitMenuParent(aParent); |
|
193 } |
|
194 |
|
195 void |
|
196 nsMenuFrame::InitMenuParent(nsIFrame* aParent) |
|
197 { |
|
198 while (aParent) { |
|
199 nsMenuPopupFrame* popup = do_QueryFrame(aParent); |
|
200 if (popup) { |
|
201 mMenuParent = popup; |
|
202 break; |
|
203 } |
|
204 |
|
205 nsMenuBarFrame* menubar = do_QueryFrame(aParent); |
|
206 if (menubar) { |
|
207 mMenuParent = menubar; |
|
208 break; |
|
209 } |
|
210 |
|
211 aParent = aParent->GetParent(); |
|
212 } |
|
213 } |
|
214 |
|
215 class nsASyncMenuInitialization MOZ_FINAL : public nsIReflowCallback |
|
216 { |
|
217 public: |
|
218 nsASyncMenuInitialization(nsIFrame* aFrame) |
|
219 : mWeakFrame(aFrame) |
|
220 { |
|
221 } |
|
222 |
|
223 virtual bool ReflowFinished() MOZ_OVERRIDE |
|
224 { |
|
225 bool shouldFlush = false; |
|
226 nsMenuFrame* menu = do_QueryFrame(mWeakFrame.GetFrame()); |
|
227 if (menu) { |
|
228 menu->UpdateMenuType(menu->PresContext()); |
|
229 shouldFlush = true; |
|
230 } |
|
231 delete this; |
|
232 return shouldFlush; |
|
233 } |
|
234 |
|
235 virtual void ReflowCallbackCanceled() MOZ_OVERRIDE |
|
236 { |
|
237 delete this; |
|
238 } |
|
239 |
|
240 nsWeakFrame mWeakFrame; |
|
241 }; |
|
242 |
|
243 void |
|
244 nsMenuFrame::Init(nsIContent* aContent, |
|
245 nsIFrame* aParent, |
|
246 nsIFrame* aPrevInFlow) |
|
247 { |
|
248 nsBoxFrame::Init(aContent, aParent, aPrevInFlow); |
|
249 |
|
250 // Set up a mediator which can be used for callbacks on this frame. |
|
251 mTimerMediator = new nsMenuTimerMediator(this); |
|
252 |
|
253 InitMenuParent(aParent); |
|
254 |
|
255 BuildAcceleratorText(false); |
|
256 nsIReflowCallback* cb = new nsASyncMenuInitialization(this); |
|
257 PresContext()->PresShell()->PostReflowCallback(cb); |
|
258 } |
|
259 |
|
260 const nsFrameList& |
|
261 nsMenuFrame::GetChildList(ChildListID aListID) const |
|
262 { |
|
263 if (kPopupList == aListID) { |
|
264 nsFrameList* list = GetPopupList(); |
|
265 return list ? *list : nsFrameList::EmptyList(); |
|
266 } |
|
267 return nsBoxFrame::GetChildList(aListID); |
|
268 } |
|
269 |
|
270 void |
|
271 nsMenuFrame::GetChildLists(nsTArray<ChildList>* aLists) const |
|
272 { |
|
273 nsBoxFrame::GetChildLists(aLists); |
|
274 nsFrameList* list = GetPopupList(); |
|
275 if (list) { |
|
276 list->AppendIfNonempty(aLists, kPopupList); |
|
277 } |
|
278 } |
|
279 |
|
280 nsMenuPopupFrame* |
|
281 nsMenuFrame::GetPopup() |
|
282 { |
|
283 nsFrameList* popupList = GetPopupList(); |
|
284 return popupList ? static_cast<nsMenuPopupFrame*>(popupList->FirstChild()) : |
|
285 nullptr; |
|
286 } |
|
287 |
|
288 nsFrameList* |
|
289 nsMenuFrame::GetPopupList() const |
|
290 { |
|
291 if (!HasPopup()) { |
|
292 return nullptr; |
|
293 } |
|
294 nsFrameList* prop = |
|
295 static_cast<nsFrameList*>(Properties().Get(PopupListProperty())); |
|
296 NS_ASSERTION(prop && prop->GetLength() == 1 && |
|
297 prop->FirstChild()->GetType() == nsGkAtoms::menuPopupFrame, |
|
298 "popup list should have exactly one nsMenuPopupFrame"); |
|
299 return prop; |
|
300 } |
|
301 |
|
302 void |
|
303 nsMenuFrame::DestroyPopupList() |
|
304 { |
|
305 NS_ASSERTION(HasPopup(), "huh?"); |
|
306 nsFrameList* prop = |
|
307 static_cast<nsFrameList*>(Properties().Remove(PopupListProperty())); |
|
308 NS_ASSERTION(prop && prop->IsEmpty(), |
|
309 "popup list must exist and be empty when destroying"); |
|
310 RemoveStateBits(NS_STATE_MENU_HAS_POPUP_LIST); |
|
311 prop->Delete(PresContext()->PresShell()); |
|
312 } |
|
313 |
|
314 void |
|
315 nsMenuFrame::SetPopupFrame(nsFrameList& aFrameList) |
|
316 { |
|
317 for (nsFrameList::Enumerator e(aFrameList); !e.AtEnd(); e.Next()) { |
|
318 nsMenuPopupFrame* popupFrame = do_QueryFrame(e.get()); |
|
319 if (popupFrame) { |
|
320 // Remove the frame from the list and store it in a nsFrameList* property. |
|
321 aFrameList.RemoveFrame(popupFrame); |
|
322 nsFrameList* popupList = new (PresContext()->PresShell()) nsFrameList(popupFrame, popupFrame); |
|
323 Properties().Set(PopupListProperty(), popupList); |
|
324 AddStateBits(NS_STATE_MENU_HAS_POPUP_LIST); |
|
325 break; |
|
326 } |
|
327 } |
|
328 } |
|
329 |
|
330 nsresult |
|
331 nsMenuFrame::SetInitialChildList(ChildListID aListID, |
|
332 nsFrameList& aChildList) |
|
333 { |
|
334 NS_ASSERTION(!HasPopup(), "SetInitialChildList called twice?"); |
|
335 if (aListID == kPrincipalList || aListID == kPopupList) { |
|
336 SetPopupFrame(aChildList); |
|
337 } |
|
338 return nsBoxFrame::SetInitialChildList(aListID, aChildList); |
|
339 } |
|
340 |
|
341 void |
|
342 nsMenuFrame::DestroyFrom(nsIFrame* aDestructRoot) |
|
343 { |
|
344 // Kill our timer if one is active. This is not strictly necessary as |
|
345 // the pointer to this frame will be cleared from the mediator, but |
|
346 // this is done for added safety. |
|
347 if (mOpenTimer) { |
|
348 mOpenTimer->Cancel(); |
|
349 } |
|
350 |
|
351 StopBlinking(); |
|
352 |
|
353 // Null out the pointer to this frame in the mediator wrapper so that it |
|
354 // doesn't try to interact with a deallocated frame. |
|
355 mTimerMediator->ClearFrame(); |
|
356 |
|
357 // if the menu content is just being hidden, it may be made visible again |
|
358 // later, so make sure to clear the highlighting. |
|
359 mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, false); |
|
360 |
|
361 // are we our menu parent's current menu item? |
|
362 if (mMenuParent && mMenuParent->GetCurrentMenuItem() == this) { |
|
363 // yes; tell it that we're going away |
|
364 mMenuParent->CurrentMenuIsBeingDestroyed(); |
|
365 } |
|
366 |
|
367 nsFrameList* popupList = GetPopupList(); |
|
368 if (popupList) { |
|
369 popupList->DestroyFramesFrom(aDestructRoot); |
|
370 DestroyPopupList(); |
|
371 } |
|
372 |
|
373 nsBoxFrame::DestroyFrom(aDestructRoot); |
|
374 } |
|
375 |
|
376 void |
|
377 nsMenuFrame::BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder, |
|
378 const nsRect& aDirtyRect, |
|
379 const nsDisplayListSet& aLists) |
|
380 { |
|
381 if (!aBuilder->IsForEventDelivery()) { |
|
382 nsBoxFrame::BuildDisplayListForChildren(aBuilder, aDirtyRect, aLists); |
|
383 return; |
|
384 } |
|
385 |
|
386 nsDisplayListCollection set; |
|
387 nsBoxFrame::BuildDisplayListForChildren(aBuilder, aDirtyRect, set); |
|
388 |
|
389 WrapListsInRedirector(aBuilder, set, aLists); |
|
390 } |
|
391 |
|
392 nsresult |
|
393 nsMenuFrame::HandleEvent(nsPresContext* aPresContext, |
|
394 WidgetGUIEvent* aEvent, |
|
395 nsEventStatus* aEventStatus) |
|
396 { |
|
397 NS_ENSURE_ARG_POINTER(aEventStatus); |
|
398 if (nsEventStatus_eConsumeNoDefault == *aEventStatus || |
|
399 (mMenuParent && mMenuParent->IsMenuLocked())) { |
|
400 return NS_OK; |
|
401 } |
|
402 |
|
403 nsWeakFrame weakFrame(this); |
|
404 if (*aEventStatus == nsEventStatus_eIgnore) |
|
405 *aEventStatus = nsEventStatus_eConsumeDoDefault; |
|
406 |
|
407 bool onmenu = IsOnMenu(); |
|
408 |
|
409 if (aEvent->message == NS_KEY_PRESS && !IsDisabled()) { |
|
410 WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); |
|
411 uint32_t keyCode = keyEvent->keyCode; |
|
412 #ifdef XP_MACOSX |
|
413 // On mac, open menulist on either up/down arrow or space (w/o Cmd pressed) |
|
414 if (!IsOpen() && ((keyEvent->charCode == NS_VK_SPACE && !keyEvent->IsMeta()) || |
|
415 (keyCode == NS_VK_UP || keyCode == NS_VK_DOWN))) { |
|
416 *aEventStatus = nsEventStatus_eConsumeNoDefault; |
|
417 OpenMenu(false); |
|
418 } |
|
419 #else |
|
420 // On other platforms, toggle menulist on unmodified F4 or Alt arrow |
|
421 if ((keyCode == NS_VK_F4 && !keyEvent->IsAlt()) || |
|
422 ((keyCode == NS_VK_UP || keyCode == NS_VK_DOWN) && keyEvent->IsAlt())) { |
|
423 *aEventStatus = nsEventStatus_eConsumeNoDefault; |
|
424 ToggleMenuState(); |
|
425 } |
|
426 #endif |
|
427 } |
|
428 else if (aEvent->message == NS_MOUSE_BUTTON_DOWN && |
|
429 aEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton && |
|
430 !IsDisabled() && IsMenu()) { |
|
431 // The menu item was selected. Bring up the menu. |
|
432 // We have children. |
|
433 // Don't prevent the default action here, since that will also cancel |
|
434 // potential drag starts. |
|
435 if (!mMenuParent || mMenuParent->IsMenuBar()) { |
|
436 ToggleMenuState(); |
|
437 } |
|
438 else { |
|
439 if (!IsOpen()) { |
|
440 OpenMenu(false); |
|
441 } |
|
442 } |
|
443 } |
|
444 else if ( |
|
445 #ifndef NSCONTEXTMENUISMOUSEUP |
|
446 (aEvent->message == NS_MOUSE_BUTTON_UP && |
|
447 aEvent->AsMouseEvent()->button == WidgetMouseEvent::eRightButton) && |
|
448 #else |
|
449 aEvent->message == NS_CONTEXTMENU && |
|
450 #endif |
|
451 onmenu && !IsMenu() && !IsDisabled()) { |
|
452 // if this menu is a context menu it accepts right-clicks...fire away! |
|
453 // Make sure we cancel default processing of the context menu event so |
|
454 // that it doesn't bubble and get seen again by the popuplistener and show |
|
455 // another context menu. |
|
456 // |
|
457 // Furthermore (there's always more, isn't there?), on some platforms (win32 |
|
458 // being one of them) we get the context menu event on a mouse up while |
|
459 // on others we get it on a mouse down. For the ones where we get it on a |
|
460 // mouse down, we must continue listening for the right button up event to |
|
461 // dismiss the menu. |
|
462 if (mMenuParent->IsContextMenu()) { |
|
463 *aEventStatus = nsEventStatus_eConsumeNoDefault; |
|
464 Execute(aEvent); |
|
465 } |
|
466 } |
|
467 else if (aEvent->message == NS_MOUSE_BUTTON_UP && |
|
468 aEvent->AsMouseEvent()->button == WidgetMouseEvent::eLeftButton && |
|
469 !IsMenu() && !IsDisabled()) { |
|
470 // Execute the execute event handler. |
|
471 *aEventStatus = nsEventStatus_eConsumeNoDefault; |
|
472 Execute(aEvent); |
|
473 } |
|
474 else if (aEvent->message == NS_MOUSE_EXIT_SYNTH) { |
|
475 // Kill our timer if one is active. |
|
476 if (mOpenTimer) { |
|
477 mOpenTimer->Cancel(); |
|
478 mOpenTimer = nullptr; |
|
479 } |
|
480 |
|
481 // Deactivate the menu. |
|
482 if (mMenuParent) { |
|
483 bool onmenubar = mMenuParent->IsMenuBar(); |
|
484 if (!(onmenubar && mMenuParent->IsActive())) { |
|
485 if (IsMenu() && !onmenubar && IsOpen()) { |
|
486 // Submenus don't get closed up immediately. |
|
487 } |
|
488 else if (this == mMenuParent->GetCurrentMenuItem()) { |
|
489 mMenuParent->ChangeMenuItem(nullptr, false); |
|
490 } |
|
491 } |
|
492 } |
|
493 } |
|
494 else if (aEvent->message == NS_MOUSE_MOVE && |
|
495 (onmenu || (mMenuParent && mMenuParent->IsMenuBar()))) { |
|
496 if (gEatMouseMove) { |
|
497 gEatMouseMove = false; |
|
498 return NS_OK; |
|
499 } |
|
500 |
|
501 // Let the menu parent know we're the new item. |
|
502 mMenuParent->ChangeMenuItem(this, false); |
|
503 NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK); |
|
504 NS_ENSURE_TRUE(mMenuParent, NS_OK); |
|
505 |
|
506 // we need to check if we really became the current menu |
|
507 // item or not |
|
508 nsMenuFrame *realCurrentItem = mMenuParent->GetCurrentMenuItem(); |
|
509 if (realCurrentItem != this) { |
|
510 // we didn't (presumably because a context menu was active) |
|
511 return NS_OK; |
|
512 } |
|
513 |
|
514 // Hovering over a menu in a popup should open it without a need for a click. |
|
515 // A timer is used so that it doesn't open if the user moves the mouse quickly |
|
516 // past the menu. This conditional check ensures that only menus have this |
|
517 // behaviour |
|
518 if (!IsDisabled() && IsMenu() && !IsOpen() && !mOpenTimer && !mMenuParent->IsMenuBar()) { |
|
519 int32_t menuDelay = |
|
520 LookAndFeel::GetInt(LookAndFeel::eIntID_SubmenuDelay, 300); // ms |
|
521 |
|
522 // We're a menu, we're built, we're closed, and no timer has been kicked off. |
|
523 mOpenTimer = do_CreateInstance("@mozilla.org/timer;1"); |
|
524 mOpenTimer->InitWithCallback(mTimerMediator, menuDelay, nsITimer::TYPE_ONE_SHOT); |
|
525 } |
|
526 } |
|
527 |
|
528 return NS_OK; |
|
529 } |
|
530 |
|
531 void |
|
532 nsMenuFrame::ToggleMenuState() |
|
533 { |
|
534 if (IsOpen()) |
|
535 CloseMenu(false); |
|
536 else |
|
537 OpenMenu(false); |
|
538 } |
|
539 |
|
540 void |
|
541 nsMenuFrame::PopupOpened() |
|
542 { |
|
543 nsWeakFrame weakFrame(this); |
|
544 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, |
|
545 NS_LITERAL_STRING("true"), true); |
|
546 if (!weakFrame.IsAlive()) |
|
547 return; |
|
548 |
|
549 if (mMenuParent) { |
|
550 mMenuParent->SetActive(true); |
|
551 // Make sure the current menu which is being toggled on |
|
552 // the menubar is highlighted |
|
553 mMenuParent->SetCurrentMenuItem(this); |
|
554 } |
|
555 } |
|
556 |
|
557 void |
|
558 nsMenuFrame::PopupClosed(bool aDeselectMenu) |
|
559 { |
|
560 nsWeakFrame weakFrame(this); |
|
561 nsContentUtils::AddScriptRunner( |
|
562 new nsUnsetAttrRunnable(mContent, nsGkAtoms::open)); |
|
563 if (!weakFrame.IsAlive()) |
|
564 return; |
|
565 |
|
566 // if the popup is for a menu on a menubar, inform menubar to deactivate |
|
567 if (mMenuParent && mMenuParent->MenuClosed()) { |
|
568 if (aDeselectMenu) { |
|
569 SelectMenu(false); |
|
570 } else { |
|
571 // We are not deselecting the parent menu while closing the popup, so send |
|
572 // a DOMMenuItemActive event to the menu to indicate that the menu is |
|
573 // becoming active again. |
|
574 nsMenuFrame *current = mMenuParent->GetCurrentMenuItem(); |
|
575 if (current) { |
|
576 // However, if the menu is a descendant on a menubar, and the menubar |
|
577 // has the 'stay active' flag set, it means that the menubar is switching |
|
578 // to another toplevel menu entirely (for example from Edit to View), so |
|
579 // don't fire the DOMMenuItemActive event or else we'll send extraneous |
|
580 // events for submenus. nsMenuBarFrame::ChangeMenuItem has already deselected |
|
581 // the old menu, so it doesn't need to happen again here, and the new |
|
582 // menu can be selected right away. |
|
583 nsIFrame* parent = current; |
|
584 while (parent) { |
|
585 nsMenuBarFrame* menubar = do_QueryFrame(parent); |
|
586 if (menubar && menubar->GetStayActive()) |
|
587 return; |
|
588 |
|
589 parent = parent->GetParent(); |
|
590 } |
|
591 |
|
592 nsCOMPtr<nsIRunnable> event = |
|
593 new nsMenuActivateEvent(current->GetContent(), |
|
594 PresContext(), true); |
|
595 NS_DispatchToCurrentThread(event); |
|
596 } |
|
597 } |
|
598 } |
|
599 } |
|
600 |
|
601 NS_IMETHODIMP |
|
602 nsMenuFrame::SelectMenu(bool aActivateFlag) |
|
603 { |
|
604 if (mContent) { |
|
605 // When a menu opens a submenu, the mouse will often be moved onto a |
|
606 // sibling before moving onto an item within the submenu, causing the |
|
607 // parent to become deselected. We need to ensure that the parent menu |
|
608 // is reselected when an item in the submenu is selected, so navigate up |
|
609 // from the item to its popup, and then to the popup above that. |
|
610 if (aActivateFlag) { |
|
611 nsIFrame* parent = GetParent(); |
|
612 while (parent) { |
|
613 nsMenuPopupFrame* menupopup = do_QueryFrame(parent); |
|
614 if (menupopup) { |
|
615 // a menu is always the direct parent of a menupopup |
|
616 nsMenuFrame* menu = do_QueryFrame(menupopup->GetParent()); |
|
617 if (menu) { |
|
618 // a popup however is not necessarily the direct parent of a menu |
|
619 nsIFrame* popupParent = menu->GetParent(); |
|
620 while (popupParent) { |
|
621 menupopup = do_QueryFrame(popupParent); |
|
622 if (menupopup) { |
|
623 menupopup->SetCurrentMenuItem(menu); |
|
624 break; |
|
625 } |
|
626 popupParent = popupParent->GetParent(); |
|
627 } |
|
628 } |
|
629 break; |
|
630 } |
|
631 parent = parent->GetParent(); |
|
632 } |
|
633 } |
|
634 |
|
635 // cancel the close timer if selecting a menu within the popup to be closed |
|
636 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
637 if (pm) |
|
638 pm->CancelMenuTimer(mMenuParent); |
|
639 |
|
640 nsCOMPtr<nsIRunnable> event = |
|
641 new nsMenuActivateEvent(mContent, PresContext(), aActivateFlag); |
|
642 NS_DispatchToCurrentThread(event); |
|
643 } |
|
644 |
|
645 return NS_OK; |
|
646 } |
|
647 |
|
648 nsresult |
|
649 nsMenuFrame::AttributeChanged(int32_t aNameSpaceID, |
|
650 nsIAtom* aAttribute, |
|
651 int32_t aModType) |
|
652 { |
|
653 if (aAttribute == nsGkAtoms::acceltext && mIgnoreAccelTextChange) { |
|
654 // Reset the flag so that only one change is ignored. |
|
655 mIgnoreAccelTextChange = false; |
|
656 return NS_OK; |
|
657 } |
|
658 |
|
659 if (aAttribute == nsGkAtoms::checked || |
|
660 aAttribute == nsGkAtoms::acceltext || |
|
661 aAttribute == nsGkAtoms::key || |
|
662 aAttribute == nsGkAtoms::type || |
|
663 aAttribute == nsGkAtoms::name) { |
|
664 nsCOMPtr<nsIRunnable> event = |
|
665 new nsMenuAttributeChangedEvent(this, aAttribute); |
|
666 nsContentUtils::AddScriptRunner(event); |
|
667 } |
|
668 return NS_OK; |
|
669 } |
|
670 |
|
671 nsIContent* |
|
672 nsMenuFrame::GetAnchor() |
|
673 { |
|
674 mozilla::dom::Element* anchor = nullptr; |
|
675 |
|
676 nsAutoString id; |
|
677 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::anchor, id); |
|
678 if (!id.IsEmpty()) { |
|
679 nsIDocument* doc = mContent->OwnerDoc(); |
|
680 |
|
681 anchor = |
|
682 doc->GetAnonymousElementByAttribute(mContent, nsGkAtoms::anonid, id); |
|
683 if (!anchor) { |
|
684 anchor = doc->GetElementById(id); |
|
685 } |
|
686 } |
|
687 |
|
688 // Always return the menu's content if the anchor wasn't set or wasn't found. |
|
689 return anchor && anchor->GetPrimaryFrame() ? anchor : mContent; |
|
690 } |
|
691 |
|
692 void |
|
693 nsMenuFrame::OpenMenu(bool aSelectFirstItem) |
|
694 { |
|
695 if (!mContent) |
|
696 return; |
|
697 |
|
698 gEatMouseMove = true; |
|
699 |
|
700 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
701 if (pm) { |
|
702 pm->KillMenuTimer(); |
|
703 // This opens the menu asynchronously |
|
704 pm->ShowMenu(mContent, aSelectFirstItem, true); |
|
705 } |
|
706 } |
|
707 |
|
708 void |
|
709 nsMenuFrame::CloseMenu(bool aDeselectMenu) |
|
710 { |
|
711 gEatMouseMove = true; |
|
712 |
|
713 // Close the menu asynchronously |
|
714 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
715 if (pm && HasPopup()) |
|
716 pm->HidePopup(GetPopup()->GetContent(), false, aDeselectMenu, true, false); |
|
717 } |
|
718 |
|
719 bool |
|
720 nsMenuFrame::IsSizedToPopup(nsIContent* aContent, bool aRequireAlways) |
|
721 { |
|
722 nsAutoString sizedToPopup; |
|
723 aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::sizetopopup, sizedToPopup); |
|
724 return sizedToPopup.EqualsLiteral("always") || |
|
725 (!aRequireAlways && sizedToPopup.EqualsLiteral("pref")); |
|
726 } |
|
727 |
|
728 nsSize |
|
729 nsMenuFrame::GetMinSize(nsBoxLayoutState& aBoxLayoutState) |
|
730 { |
|
731 nsSize size = nsBoxFrame::GetMinSize(aBoxLayoutState); |
|
732 DISPLAY_MIN_SIZE(this, size); |
|
733 |
|
734 if (IsSizedToPopup(mContent, true)) |
|
735 SizeToPopup(aBoxLayoutState, size); |
|
736 |
|
737 return size; |
|
738 } |
|
739 |
|
740 NS_IMETHODIMP |
|
741 nsMenuFrame::DoLayout(nsBoxLayoutState& aState) |
|
742 { |
|
743 // lay us out |
|
744 nsresult rv = nsBoxFrame::DoLayout(aState); |
|
745 |
|
746 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
747 if (popupFrame) { |
|
748 bool sizeToPopup = IsSizedToPopup(mContent, false); |
|
749 popupFrame->LayoutPopup(aState, this, GetAnchor()->GetPrimaryFrame(), sizeToPopup); |
|
750 } |
|
751 |
|
752 return rv; |
|
753 } |
|
754 |
|
755 #ifdef DEBUG_LAYOUT |
|
756 nsresult |
|
757 nsMenuFrame::SetDebug(nsBoxLayoutState& aState, bool aDebug) |
|
758 { |
|
759 // see if our state matches the given debug state |
|
760 bool debugSet = mState & NS_STATE_CURRENTLY_IN_DEBUG; |
|
761 bool debugChanged = (!aDebug && debugSet) || (aDebug && !debugSet); |
|
762 |
|
763 // if it doesn't then tell each child below us the new debug state |
|
764 if (debugChanged) |
|
765 { |
|
766 nsBoxFrame::SetDebug(aState, aDebug); |
|
767 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
768 if (popupFrame) |
|
769 SetDebug(aState, popupFrame, aDebug); |
|
770 } |
|
771 |
|
772 return NS_OK; |
|
773 } |
|
774 |
|
775 nsresult |
|
776 nsMenuFrame::SetDebug(nsBoxLayoutState& aState, nsIFrame* aList, bool aDebug) |
|
777 { |
|
778 if (!aList) |
|
779 return NS_OK; |
|
780 |
|
781 while (aList) { |
|
782 if (aList->IsBoxFrame()) |
|
783 aList->SetDebug(aState, aDebug); |
|
784 |
|
785 aList = aList->GetNextSibling(); |
|
786 } |
|
787 |
|
788 return NS_OK; |
|
789 } |
|
790 #endif |
|
791 |
|
792 // |
|
793 // Enter |
|
794 // |
|
795 // Called when the user hits the <Enter>/<Return> keys or presses the |
|
796 // shortcut key. If this is a leaf item, the item's action will be executed. |
|
797 // In either case, do nothing if the item is disabled. |
|
798 // |
|
799 nsMenuFrame* |
|
800 nsMenuFrame::Enter(WidgetGUIEvent* aEvent) |
|
801 { |
|
802 if (IsDisabled()) { |
|
803 #ifdef XP_WIN |
|
804 // behavior on Windows - close the popup chain |
|
805 if (mMenuParent) { |
|
806 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
807 if (pm) { |
|
808 nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny); |
|
809 if (popup) |
|
810 pm->HidePopup(popup->GetContent(), true, true, true, false); |
|
811 } |
|
812 } |
|
813 #endif // #ifdef XP_WIN |
|
814 // this menu item was disabled - exit |
|
815 return nullptr; |
|
816 } |
|
817 |
|
818 if (!IsOpen()) { |
|
819 // The enter key press applies to us. |
|
820 if (!IsMenu() && mMenuParent) |
|
821 Execute(aEvent); // Execute our event handler |
|
822 else |
|
823 return this; |
|
824 } |
|
825 |
|
826 return nullptr; |
|
827 } |
|
828 |
|
829 bool |
|
830 nsMenuFrame::IsOpen() |
|
831 { |
|
832 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
833 return popupFrame && popupFrame->IsOpen(); |
|
834 } |
|
835 |
|
836 bool |
|
837 nsMenuFrame::IsMenu() |
|
838 { |
|
839 return mIsMenu; |
|
840 } |
|
841 |
|
842 nsMenuListType |
|
843 nsMenuFrame::GetParentMenuListType() |
|
844 { |
|
845 if (mMenuParent && mMenuParent->IsMenu()) { |
|
846 nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(mMenuParent); |
|
847 nsIFrame* parentMenu = popupFrame->GetParent(); |
|
848 if (parentMenu) { |
|
849 nsCOMPtr<nsIDOMXULMenuListElement> menulist = do_QueryInterface(parentMenu->GetContent()); |
|
850 if (menulist) { |
|
851 bool isEditable = false; |
|
852 menulist->GetEditable(&isEditable); |
|
853 return isEditable ? eEditableMenuList : eReadonlyMenuList; |
|
854 } |
|
855 } |
|
856 } |
|
857 return eNotMenuList; |
|
858 } |
|
859 |
|
860 nsresult |
|
861 nsMenuFrame::Notify(nsITimer* aTimer) |
|
862 { |
|
863 // Our timer has fired. |
|
864 if (aTimer == mOpenTimer.get()) { |
|
865 mOpenTimer = nullptr; |
|
866 |
|
867 if (!IsOpen() && mMenuParent) { |
|
868 // make sure we didn't open a context menu in the meantime |
|
869 // (i.e. the user right-clicked while hovering over a submenu). |
|
870 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
871 if (pm) { |
|
872 if ((!pm->HasContextMenu(nullptr) || mMenuParent->IsContextMenu()) && |
|
873 mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::menuactive, |
|
874 nsGkAtoms::_true, eCaseMatters)) { |
|
875 OpenMenu(false); |
|
876 } |
|
877 } |
|
878 } |
|
879 } else if (aTimer == mBlinkTimer) { |
|
880 switch (mBlinkState++) { |
|
881 case 0: |
|
882 NS_ASSERTION(false, "Blink timer fired while not blinking"); |
|
883 StopBlinking(); |
|
884 break; |
|
885 case 1: |
|
886 { |
|
887 // Turn the highlight back on and wait for a while before closing the menu. |
|
888 nsWeakFrame weakFrame(this); |
|
889 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, |
|
890 NS_LITERAL_STRING("true"), true); |
|
891 if (weakFrame.IsAlive()) { |
|
892 aTimer->InitWithCallback(mTimerMediator, kBlinkDelay, nsITimer::TYPE_ONE_SHOT); |
|
893 } |
|
894 } |
|
895 break; |
|
896 default: |
|
897 if (mMenuParent) { |
|
898 mMenuParent->LockMenuUntilClosed(false); |
|
899 } |
|
900 PassMenuCommandEventToPopupManager(); |
|
901 StopBlinking(); |
|
902 break; |
|
903 } |
|
904 } |
|
905 |
|
906 return NS_OK; |
|
907 } |
|
908 |
|
909 bool |
|
910 nsMenuFrame::IsDisabled() |
|
911 { |
|
912 return mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, |
|
913 nsGkAtoms::_true, eCaseMatters); |
|
914 } |
|
915 |
|
916 void |
|
917 nsMenuFrame::UpdateMenuType(nsPresContext* aPresContext) |
|
918 { |
|
919 static nsIContent::AttrValuesArray strings[] = |
|
920 {&nsGkAtoms::checkbox, &nsGkAtoms::radio, nullptr}; |
|
921 switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, |
|
922 strings, eCaseMatters)) { |
|
923 case 0: mType = eMenuType_Checkbox; break; |
|
924 case 1: |
|
925 mType = eMenuType_Radio; |
|
926 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::name, mGroupName); |
|
927 break; |
|
928 |
|
929 default: |
|
930 if (mType != eMenuType_Normal) { |
|
931 nsWeakFrame weakFrame(this); |
|
932 mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, |
|
933 true); |
|
934 ENSURE_TRUE(weakFrame.IsAlive()); |
|
935 } |
|
936 mType = eMenuType_Normal; |
|
937 break; |
|
938 } |
|
939 UpdateMenuSpecialState(aPresContext); |
|
940 } |
|
941 |
|
942 /* update checked-ness for type="checkbox" and type="radio" */ |
|
943 void |
|
944 nsMenuFrame::UpdateMenuSpecialState(nsPresContext* aPresContext) |
|
945 { |
|
946 bool newChecked = |
|
947 mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::checked, |
|
948 nsGkAtoms::_true, eCaseMatters); |
|
949 if (newChecked == mChecked) { |
|
950 /* checked state didn't change */ |
|
951 |
|
952 if (mType != eMenuType_Radio) |
|
953 return; // only Radio possibly cares about other kinds of change |
|
954 |
|
955 if (!mChecked || mGroupName.IsEmpty()) |
|
956 return; // no interesting change |
|
957 } else { |
|
958 mChecked = newChecked; |
|
959 if (mType != eMenuType_Radio || !mChecked) |
|
960 /* |
|
961 * Unchecking something requires no further changes, and only |
|
962 * menuRadio has to do additional work when checked. |
|
963 */ |
|
964 return; |
|
965 } |
|
966 |
|
967 /* |
|
968 * If we get this far, we're type=radio, and: |
|
969 * - our name= changed, or |
|
970 * - we went from checked="false" to checked="true" |
|
971 */ |
|
972 |
|
973 /* |
|
974 * Behavioural note: |
|
975 * If we're checked and renamed _into_ an existing radio group, we are |
|
976 * made the new checked item, and we unselect the previous one. |
|
977 * |
|
978 * The only other reasonable behaviour would be to check for another selected |
|
979 * item in that group. If found, unselect ourselves, otherwise we're the |
|
980 * selected item. That, however, would be a lot more work, and I don't think |
|
981 * it's better at all. |
|
982 */ |
|
983 |
|
984 /* walk siblings, looking for the other checked item with the same name */ |
|
985 // get the first sibling in this menu popup. This frame may be it, and if we're |
|
986 // being called at creation time, this frame isn't yet in the parent's child list. |
|
987 // All I'm saying is that this may fail, but it's most likely alright. |
|
988 nsIFrame* sib = GetParent()->GetFirstPrincipalChild(); |
|
989 |
|
990 while (sib) { |
|
991 if (sib != this) { |
|
992 nsMenuFrame* menu = do_QueryFrame(sib); |
|
993 if (menu && menu->GetMenuType() == eMenuType_Radio && |
|
994 menu->IsChecked() && menu->GetRadioGroupName() == mGroupName) { |
|
995 /* uncheck the old item */ |
|
996 sib->GetContent()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, |
|
997 true); |
|
998 /* XXX in DEBUG, check to make sure that there aren't two checked items */ |
|
999 return; |
|
1000 } |
|
1001 } |
|
1002 |
|
1003 sib = sib->GetNextSibling(); |
|
1004 } |
|
1005 } |
|
1006 |
|
1007 void |
|
1008 nsMenuFrame::BuildAcceleratorText(bool aNotify) |
|
1009 { |
|
1010 nsAutoString accelText; |
|
1011 |
|
1012 if ((GetStateBits() & NS_STATE_ACCELTEXT_IS_DERIVED) == 0) { |
|
1013 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, accelText); |
|
1014 if (!accelText.IsEmpty()) |
|
1015 return; |
|
1016 } |
|
1017 // accelText is definitely empty here. |
|
1018 |
|
1019 // Now we're going to compute the accelerator text, so remember that we did. |
|
1020 AddStateBits(NS_STATE_ACCELTEXT_IS_DERIVED); |
|
1021 |
|
1022 // If anything below fails, just leave the accelerator text blank. |
|
1023 nsWeakFrame weakFrame(this); |
|
1024 mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, aNotify); |
|
1025 ENSURE_TRUE(weakFrame.IsAlive()); |
|
1026 |
|
1027 // See if we have a key node and use that instead. |
|
1028 nsAutoString keyValue; |
|
1029 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyValue); |
|
1030 if (keyValue.IsEmpty()) |
|
1031 return; |
|
1032 |
|
1033 // Turn the document into a DOM document so we can use getElementById |
|
1034 nsIDocument *document = mContent->GetDocument(); |
|
1035 if (!document) |
|
1036 return; |
|
1037 |
|
1038 nsIContent *keyElement = document->GetElementById(keyValue); |
|
1039 if (!keyElement) { |
|
1040 #ifdef DEBUG |
|
1041 nsAutoString label; |
|
1042 mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label); |
|
1043 nsAutoString msg = NS_LITERAL_STRING("Key '") + |
|
1044 keyValue + |
|
1045 NS_LITERAL_STRING("' of menu item '") + |
|
1046 label + |
|
1047 NS_LITERAL_STRING("' could not be found"); |
|
1048 NS_WARNING(NS_ConvertUTF16toUTF8(msg).get()); |
|
1049 #endif |
|
1050 return; |
|
1051 } |
|
1052 |
|
1053 // get the string to display as accelerator text |
|
1054 // check the key element's attributes in this order: |
|
1055 // |keytext|, |key|, |keycode| |
|
1056 nsAutoString accelString; |
|
1057 keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keytext, accelString); |
|
1058 |
|
1059 if (accelString.IsEmpty()) { |
|
1060 keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, accelString); |
|
1061 |
|
1062 if (!accelString.IsEmpty()) { |
|
1063 ToUpperCase(accelString); |
|
1064 } else { |
|
1065 nsAutoString keyCode; |
|
1066 keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, keyCode); |
|
1067 ToUpperCase(keyCode); |
|
1068 |
|
1069 nsresult rv; |
|
1070 nsCOMPtr<nsIStringBundleService> bundleService = |
|
1071 mozilla::services::GetStringBundleService(); |
|
1072 if (bundleService) { |
|
1073 nsCOMPtr<nsIStringBundle> bundle; |
|
1074 rv = bundleService->CreateBundle("chrome://global/locale/keys.properties", |
|
1075 getter_AddRefs(bundle)); |
|
1076 |
|
1077 if (NS_SUCCEEDED(rv) && bundle) { |
|
1078 nsXPIDLString keyName; |
|
1079 rv = bundle->GetStringFromName(keyCode.get(), getter_Copies(keyName)); |
|
1080 if (keyName) |
|
1081 accelString = keyName; |
|
1082 } |
|
1083 } |
|
1084 |
|
1085 // nothing usable found, bail |
|
1086 if (accelString.IsEmpty()) |
|
1087 return; |
|
1088 } |
|
1089 } |
|
1090 |
|
1091 static int32_t accelKey = 0; |
|
1092 |
|
1093 if (!accelKey) |
|
1094 { |
|
1095 // Compiled-in defaults, in case we can't get LookAndFeel -- |
|
1096 // command for mac, control for all other platforms. |
|
1097 #ifdef XP_MACOSX |
|
1098 accelKey = nsIDOMKeyEvent::DOM_VK_META; |
|
1099 #else |
|
1100 accelKey = nsIDOMKeyEvent::DOM_VK_CONTROL; |
|
1101 #endif |
|
1102 |
|
1103 // Get the accelerator key value from prefs, overriding the default: |
|
1104 accelKey = Preferences::GetInt("ui.key.accelKey", accelKey); |
|
1105 } |
|
1106 |
|
1107 nsAutoString modifiers; |
|
1108 keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiers); |
|
1109 |
|
1110 char* str = ToNewCString(modifiers); |
|
1111 char* newStr; |
|
1112 char* token = nsCRT::strtok(str, ", \t", &newStr); |
|
1113 |
|
1114 nsAutoString shiftText; |
|
1115 nsAutoString altText; |
|
1116 nsAutoString metaText; |
|
1117 nsAutoString controlText; |
|
1118 nsAutoString osText; |
|
1119 nsAutoString modifierSeparator; |
|
1120 |
|
1121 nsContentUtils::GetShiftText(shiftText); |
|
1122 nsContentUtils::GetAltText(altText); |
|
1123 nsContentUtils::GetMetaText(metaText); |
|
1124 nsContentUtils::GetControlText(controlText); |
|
1125 nsContentUtils::GetOSText(osText); |
|
1126 nsContentUtils::GetModifierSeparatorText(modifierSeparator); |
|
1127 |
|
1128 while (token) { |
|
1129 |
|
1130 if (PL_strcmp(token, "shift") == 0) |
|
1131 accelText += shiftText; |
|
1132 else if (PL_strcmp(token, "alt") == 0) |
|
1133 accelText += altText; |
|
1134 else if (PL_strcmp(token, "meta") == 0) |
|
1135 accelText += metaText; |
|
1136 else if (PL_strcmp(token, "os") == 0) |
|
1137 accelText += osText; |
|
1138 else if (PL_strcmp(token, "control") == 0) |
|
1139 accelText += controlText; |
|
1140 else if (PL_strcmp(token, "accel") == 0) { |
|
1141 switch (accelKey) |
|
1142 { |
|
1143 case nsIDOMKeyEvent::DOM_VK_META: |
|
1144 accelText += metaText; |
|
1145 break; |
|
1146 |
|
1147 case nsIDOMKeyEvent::DOM_VK_WIN: |
|
1148 accelText += osText; |
|
1149 break; |
|
1150 |
|
1151 case nsIDOMKeyEvent::DOM_VK_ALT: |
|
1152 accelText += altText; |
|
1153 break; |
|
1154 |
|
1155 case nsIDOMKeyEvent::DOM_VK_CONTROL: |
|
1156 default: |
|
1157 accelText += controlText; |
|
1158 break; |
|
1159 } |
|
1160 } |
|
1161 |
|
1162 accelText += modifierSeparator; |
|
1163 |
|
1164 token = nsCRT::strtok(newStr, ", \t", &newStr); |
|
1165 } |
|
1166 |
|
1167 nsMemory::Free(str); |
|
1168 |
|
1169 accelText += accelString; |
|
1170 |
|
1171 mIgnoreAccelTextChange = true; |
|
1172 mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::acceltext, accelText, aNotify); |
|
1173 ENSURE_TRUE(weakFrame.IsAlive()); |
|
1174 |
|
1175 mIgnoreAccelTextChange = false; |
|
1176 } |
|
1177 |
|
1178 void |
|
1179 nsMenuFrame::Execute(WidgetGUIEvent* aEvent) |
|
1180 { |
|
1181 // flip "checked" state if we're a checkbox menu, or an un-checked radio menu |
|
1182 bool needToFlipChecked = false; |
|
1183 if (mType == eMenuType_Checkbox || (mType == eMenuType_Radio && !mChecked)) { |
|
1184 needToFlipChecked = !mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::autocheck, |
|
1185 nsGkAtoms::_false, eCaseMatters); |
|
1186 } |
|
1187 |
|
1188 nsCOMPtr<nsISound> sound(do_CreateInstance("@mozilla.org/sound;1")); |
|
1189 if (sound) |
|
1190 sound->PlayEventSound(nsISound::EVENT_MENU_EXECUTE); |
|
1191 |
|
1192 StartBlinking(aEvent, needToFlipChecked); |
|
1193 } |
|
1194 |
|
1195 bool |
|
1196 nsMenuFrame::ShouldBlink() |
|
1197 { |
|
1198 int32_t shouldBlink = |
|
1199 LookAndFeel::GetInt(LookAndFeel::eIntID_ChosenMenuItemsShouldBlink, 0); |
|
1200 if (!shouldBlink) |
|
1201 return false; |
|
1202 |
|
1203 // Don't blink in editable menulists. |
|
1204 if (GetParentMenuListType() == eEditableMenuList) |
|
1205 return false; |
|
1206 |
|
1207 return true; |
|
1208 } |
|
1209 |
|
1210 void |
|
1211 nsMenuFrame::StartBlinking(WidgetGUIEvent* aEvent, bool aFlipChecked) |
|
1212 { |
|
1213 StopBlinking(); |
|
1214 CreateMenuCommandEvent(aEvent, aFlipChecked); |
|
1215 |
|
1216 if (!ShouldBlink()) { |
|
1217 PassMenuCommandEventToPopupManager(); |
|
1218 return; |
|
1219 } |
|
1220 |
|
1221 // Blink off. |
|
1222 nsWeakFrame weakFrame(this); |
|
1223 mContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, true); |
|
1224 if (!weakFrame.IsAlive()) |
|
1225 return; |
|
1226 |
|
1227 if (mMenuParent) { |
|
1228 // Make this menu ignore events from now on. |
|
1229 mMenuParent->LockMenuUntilClosed(true); |
|
1230 } |
|
1231 |
|
1232 // Set up a timer to blink back on. |
|
1233 mBlinkTimer = do_CreateInstance("@mozilla.org/timer;1"); |
|
1234 mBlinkTimer->InitWithCallback(mTimerMediator, kBlinkDelay, nsITimer::TYPE_ONE_SHOT); |
|
1235 mBlinkState = 1; |
|
1236 } |
|
1237 |
|
1238 void |
|
1239 nsMenuFrame::StopBlinking() |
|
1240 { |
|
1241 mBlinkState = 0; |
|
1242 if (mBlinkTimer) { |
|
1243 mBlinkTimer->Cancel(); |
|
1244 mBlinkTimer = nullptr; |
|
1245 } |
|
1246 mDelayedMenuCommandEvent = nullptr; |
|
1247 } |
|
1248 |
|
1249 void |
|
1250 nsMenuFrame::CreateMenuCommandEvent(WidgetGUIEvent* aEvent, bool aFlipChecked) |
|
1251 { |
|
1252 // Create a trusted event if the triggering event was trusted, or if |
|
1253 // we're called from chrome code (since at least one of our caller |
|
1254 // passes in a null event). |
|
1255 bool isTrusted = aEvent ? aEvent->mFlags.mIsTrusted : |
|
1256 nsContentUtils::IsCallerChrome(); |
|
1257 |
|
1258 bool shift = false, control = false, alt = false, meta = false; |
|
1259 WidgetInputEvent* inputEvent = aEvent ? aEvent->AsInputEvent() : nullptr; |
|
1260 if (inputEvent) { |
|
1261 shift = inputEvent->IsShift(); |
|
1262 control = inputEvent->IsControl(); |
|
1263 alt = inputEvent->IsAlt(); |
|
1264 meta = inputEvent->IsMeta(); |
|
1265 } |
|
1266 |
|
1267 // Because the command event is firing asynchronously, a flag is needed to |
|
1268 // indicate whether user input is being handled. This ensures that a popup |
|
1269 // window won't get blocked. |
|
1270 bool userinput = EventStateManager::IsHandlingUserInput(); |
|
1271 |
|
1272 mDelayedMenuCommandEvent = |
|
1273 new nsXULMenuCommandEvent(mContent, isTrusted, shift, control, alt, meta, |
|
1274 userinput, aFlipChecked); |
|
1275 } |
|
1276 |
|
1277 void |
|
1278 nsMenuFrame::PassMenuCommandEventToPopupManager() |
|
1279 { |
|
1280 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); |
|
1281 if (pm && mMenuParent && mDelayedMenuCommandEvent) { |
|
1282 pm->ExecuteMenu(mContent, mDelayedMenuCommandEvent); |
|
1283 } |
|
1284 mDelayedMenuCommandEvent = nullptr; |
|
1285 } |
|
1286 |
|
1287 nsresult |
|
1288 nsMenuFrame::RemoveFrame(ChildListID aListID, |
|
1289 nsIFrame* aOldFrame) |
|
1290 { |
|
1291 nsFrameList* popupList = GetPopupList(); |
|
1292 if (popupList && popupList->FirstChild() == aOldFrame) { |
|
1293 popupList->RemoveFirstChild(); |
|
1294 aOldFrame->Destroy(); |
|
1295 DestroyPopupList(); |
|
1296 PresContext()->PresShell()-> |
|
1297 FrameNeedsReflow(this, nsIPresShell::eTreeChange, |
|
1298 NS_FRAME_HAS_DIRTY_CHILDREN); |
|
1299 return NS_OK; |
|
1300 } |
|
1301 return nsBoxFrame::RemoveFrame(aListID, aOldFrame); |
|
1302 } |
|
1303 |
|
1304 nsresult |
|
1305 nsMenuFrame::InsertFrames(ChildListID aListID, |
|
1306 nsIFrame* aPrevFrame, |
|
1307 nsFrameList& aFrameList) |
|
1308 { |
|
1309 if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) { |
|
1310 SetPopupFrame(aFrameList); |
|
1311 if (HasPopup()) { |
|
1312 #ifdef DEBUG_LAYOUT |
|
1313 nsBoxLayoutState state(PresContext()); |
|
1314 SetDebug(state, aFrameList, mState & NS_STATE_CURRENTLY_IN_DEBUG); |
|
1315 #endif |
|
1316 |
|
1317 PresContext()->PresShell()-> |
|
1318 FrameNeedsReflow(this, nsIPresShell::eTreeChange, |
|
1319 NS_FRAME_HAS_DIRTY_CHILDREN); |
|
1320 } |
|
1321 } |
|
1322 |
|
1323 if (aFrameList.IsEmpty()) |
|
1324 return NS_OK; |
|
1325 |
|
1326 if (MOZ_UNLIKELY(aPrevFrame && aPrevFrame == GetPopup())) { |
|
1327 aPrevFrame = nullptr; |
|
1328 } |
|
1329 |
|
1330 return nsBoxFrame::InsertFrames(aListID, aPrevFrame, aFrameList); |
|
1331 } |
|
1332 |
|
1333 nsresult |
|
1334 nsMenuFrame::AppendFrames(ChildListID aListID, |
|
1335 nsFrameList& aFrameList) |
|
1336 { |
|
1337 if (!HasPopup() && (aListID == kPrincipalList || aListID == kPopupList)) { |
|
1338 SetPopupFrame(aFrameList); |
|
1339 if (HasPopup()) { |
|
1340 |
|
1341 #ifdef DEBUG_LAYOUT |
|
1342 nsBoxLayoutState state(PresContext()); |
|
1343 SetDebug(state, aFrameList, mState & NS_STATE_CURRENTLY_IN_DEBUG); |
|
1344 #endif |
|
1345 PresContext()->PresShell()-> |
|
1346 FrameNeedsReflow(this, nsIPresShell::eTreeChange, |
|
1347 NS_FRAME_HAS_DIRTY_CHILDREN); |
|
1348 } |
|
1349 } |
|
1350 |
|
1351 if (aFrameList.IsEmpty()) |
|
1352 return NS_OK; |
|
1353 |
|
1354 return nsBoxFrame::AppendFrames(aListID, aFrameList); |
|
1355 } |
|
1356 |
|
1357 bool |
|
1358 nsMenuFrame::SizeToPopup(nsBoxLayoutState& aState, nsSize& aSize) |
|
1359 { |
|
1360 if (!IsCollapsed()) { |
|
1361 bool widthSet, heightSet; |
|
1362 nsSize tmpSize(-1, 0); |
|
1363 nsIFrame::AddCSSPrefSize(this, tmpSize, widthSet, heightSet); |
|
1364 if (!widthSet && GetFlex(aState) == 0) { |
|
1365 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
1366 if (!popupFrame) |
|
1367 return false; |
|
1368 tmpSize = popupFrame->GetPrefSize(aState); |
|
1369 |
|
1370 // Produce a size such that: |
|
1371 // (1) the menu and its popup can be the same width |
|
1372 // (2) there's enough room in the menu for the content and its |
|
1373 // border-padding |
|
1374 // (3) there's enough room in the popup for the content and its |
|
1375 // scrollbar |
|
1376 nsMargin borderPadding; |
|
1377 GetBorderAndPadding(borderPadding); |
|
1378 |
|
1379 // if there is a scroll frame, add the desired width of the scrollbar as well |
|
1380 nsIScrollableFrame* scrollFrame = do_QueryFrame(popupFrame->GetFirstPrincipalChild()); |
|
1381 nscoord scrollbarWidth = 0; |
|
1382 if (scrollFrame) { |
|
1383 scrollbarWidth = |
|
1384 scrollFrame->GetDesiredScrollbarSizes(&aState).LeftRight(); |
|
1385 } |
|
1386 |
|
1387 aSize.width = |
|
1388 tmpSize.width + std::max(borderPadding.LeftRight(), scrollbarWidth); |
|
1389 |
|
1390 return true; |
|
1391 } |
|
1392 } |
|
1393 |
|
1394 return false; |
|
1395 } |
|
1396 |
|
1397 nsSize |
|
1398 nsMenuFrame::GetPrefSize(nsBoxLayoutState& aState) |
|
1399 { |
|
1400 nsSize size = nsBoxFrame::GetPrefSize(aState); |
|
1401 DISPLAY_PREF_SIZE(this, size); |
|
1402 |
|
1403 // If we are using sizetopopup="always" then |
|
1404 // nsBoxFrame will already have enforced the minimum size |
|
1405 if (!IsSizedToPopup(mContent, true) && |
|
1406 IsSizedToPopup(mContent, false) && |
|
1407 SizeToPopup(aState, size)) { |
|
1408 // We now need to ensure that size is within the min - max range. |
|
1409 nsSize minSize = nsBoxFrame::GetMinSize(aState); |
|
1410 nsSize maxSize = GetMaxSize(aState); |
|
1411 size = BoundsCheck(minSize, size, maxSize); |
|
1412 } |
|
1413 |
|
1414 return size; |
|
1415 } |
|
1416 |
|
1417 NS_IMETHODIMP |
|
1418 nsMenuFrame::GetActiveChild(nsIDOMElement** aResult) |
|
1419 { |
|
1420 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
1421 if (!popupFrame) |
|
1422 return NS_ERROR_FAILURE; |
|
1423 |
|
1424 nsMenuFrame* menuFrame = popupFrame->GetCurrentMenuItem(); |
|
1425 if (!menuFrame) { |
|
1426 *aResult = nullptr; |
|
1427 } |
|
1428 else { |
|
1429 nsCOMPtr<nsIDOMElement> elt(do_QueryInterface(menuFrame->GetContent())); |
|
1430 *aResult = elt; |
|
1431 NS_IF_ADDREF(*aResult); |
|
1432 } |
|
1433 |
|
1434 return NS_OK; |
|
1435 } |
|
1436 |
|
1437 NS_IMETHODIMP |
|
1438 nsMenuFrame::SetActiveChild(nsIDOMElement* aChild) |
|
1439 { |
|
1440 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
1441 if (!popupFrame) |
|
1442 return NS_ERROR_FAILURE; |
|
1443 |
|
1444 if (!aChild) { |
|
1445 // Remove the current selection |
|
1446 popupFrame->ChangeMenuItem(nullptr, false); |
|
1447 return NS_OK; |
|
1448 } |
|
1449 |
|
1450 nsCOMPtr<nsIContent> child(do_QueryInterface(aChild)); |
|
1451 |
|
1452 nsMenuFrame* menu = do_QueryFrame(child->GetPrimaryFrame()); |
|
1453 if (menu) |
|
1454 popupFrame->ChangeMenuItem(menu, false); |
|
1455 return NS_OK; |
|
1456 } |
|
1457 |
|
1458 nsIScrollableFrame* nsMenuFrame::GetScrollTargetFrame() |
|
1459 { |
|
1460 nsMenuPopupFrame* popupFrame = GetPopup(); |
|
1461 if (!popupFrame) |
|
1462 return nullptr; |
|
1463 nsIFrame* childFrame = popupFrame->GetFirstPrincipalChild(); |
|
1464 if (childFrame) |
|
1465 return popupFrame->GetScrollFrame(childFrame); |
|
1466 return nullptr; |
|
1467 } |
|
1468 |
|
1469 // nsMenuTimerMediator implementation. |
|
1470 NS_IMPL_ISUPPORTS(nsMenuTimerMediator, nsITimerCallback) |
|
1471 |
|
1472 /** |
|
1473 * Constructs a wrapper around an nsMenuFrame. |
|
1474 * @param aFrame nsMenuFrame to create a wrapper around. |
|
1475 */ |
|
1476 nsMenuTimerMediator::nsMenuTimerMediator(nsMenuFrame *aFrame) : |
|
1477 mFrame(aFrame) |
|
1478 { |
|
1479 NS_ASSERTION(mFrame, "Must have frame"); |
|
1480 } |
|
1481 |
|
1482 nsMenuTimerMediator::~nsMenuTimerMediator() |
|
1483 { |
|
1484 } |
|
1485 |
|
1486 /** |
|
1487 * Delegates the notification to the contained frame if it has not been destroyed. |
|
1488 * @param aTimer Timer which initiated the callback. |
|
1489 * @return NS_ERROR_FAILURE if the frame has been destroyed. |
|
1490 */ |
|
1491 NS_IMETHODIMP nsMenuTimerMediator::Notify(nsITimer* aTimer) |
|
1492 { |
|
1493 if (!mFrame) |
|
1494 return NS_ERROR_FAILURE; |
|
1495 |
|
1496 return mFrame->Notify(aTimer); |
|
1497 } |
|
1498 |
|
1499 /** |
|
1500 * Clear the pointer to the contained nsMenuFrame. This should be called |
|
1501 * when the contained nsMenuFrame is destroyed. |
|
1502 */ |
|
1503 void nsMenuTimerMediator::ClearFrame() |
|
1504 { |
|
1505 mFrame = nullptr; |
|
1506 } |