1/*
2 * Copyright 2001-2018 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT license.
4 *
5 * Authors:
6 *		Stephan A��mus, superstippi@gmx.de
7 *		Stefano Ceccherini, stefano.ceccherini@gmail.com
8 *		Adrien Destugues, pulkomandy@pulkomandy.tk
9 *		Marc Flerackers, mflerackers@androme.be
10 *		Rene Gollent, anevilyak@gmail.com
11 *		John Scipione, jscipione@gmail.com
12 */
13
14
15#include <Menu.h>
16
17#include <algorithm>
18#include <new>
19#include <set>
20
21#include <ctype.h>
22#include <string.h>
23
24#include <Application.h>
25#include <Bitmap.h>
26#include <ControlLook.h>
27#include <Debug.h>
28#include <File.h>
29#include <FindDirectory.h>
30#include <Layout.h>
31#include <LayoutUtils.h>
32#include <MenuBar.h>
33#include <MenuItem.h>
34#include <Messenger.h>
35#include <Path.h>
36#include <PropertyInfo.h>
37#include <Screen.h>
38#include <ScrollBar.h>
39#include <SystemCatalog.h>
40#include <UnicodeChar.h>
41#include <Window.h>
42
43#include <AppServerLink.h>
44#include <binary_compatibility/Interface.h>
45#include <BMCPrivate.h>
46#include <MenuPrivate.h>
47#include <MenuWindow.h>
48#include <ServerProtocol.h>
49
50#include "utf8_functions.h"
51
52
53#define USE_CACHED_MENUWINDOW 1
54
55using BPrivate::gSystemCatalog;
56
57#undef B_TRANSLATION_CONTEXT
58#define B_TRANSLATION_CONTEXT "Menu"
59
60#undef B_TRANSLATE
61#define B_TRANSLATE(str) \
62	gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "Menu")
63
64
65using std::nothrow;
66using BPrivate::BMenuWindow;
67
68namespace BPrivate {
69
70class TriggerList {
71public:
72	TriggerList() {}
73	~TriggerList() {}
74
75	bool HasTrigger(uint32 c)
76		{ return fList.find(BUnicodeChar::ToLower(c)) != fList.end(); }
77
78	bool AddTrigger(uint32 c)
79	{
80		fList.insert(BUnicodeChar::ToLower(c));
81		return true;
82	}
83
84private:
85	std::set<uint32> fList;
86};
87
88
89class ExtraMenuData {
90public:
91	menu_tracking_hook	trackingHook;
92	void*				trackingState;
93
94	// Used to track when the menu would be drawn offscreen and instead gets
95	// shifted back on the screen towards the left. This information
96	// allows us to draw submenus in the same direction as their parents.
97	bool				frameShiftedLeft;
98
99	ExtraMenuData()
100	{
101		trackingHook = NULL;
102		trackingState = NULL;
103		frameShiftedLeft = false;
104	}
105};
106
107
108}	// namespace BPrivate
109
110
111menu_info BMenu::sMenuInfo;
112
113uint32 BMenu::sShiftKey;
114uint32 BMenu::sControlKey;
115uint32 BMenu::sOptionKey;
116uint32 BMenu::sCommandKey;
117uint32 BMenu::sMenuKey;
118
119static property_info sPropList[] = {
120	{ "Enabled", { B_GET_PROPERTY, 0 },
121		{ B_DIRECT_SPECIFIER, 0 }, "Returns true if menu or menu item is "
122		"enabled; false otherwise.",
123		0, { B_BOOL_TYPE }
124	},
125
126	{ "Enabled", { B_SET_PROPERTY, 0 },
127		{ B_DIRECT_SPECIFIER, 0 }, "Enables or disables menu or menu item.",
128		0, { B_BOOL_TYPE }
129	},
130
131	{ "Label", { B_GET_PROPERTY, 0 },
132		{ B_DIRECT_SPECIFIER, 0 }, "Returns the string label of the menu or "
133		"menu item.",
134		0, { B_STRING_TYPE }
135	},
136
137	{ "Label", { B_SET_PROPERTY, 0 },
138		{ B_DIRECT_SPECIFIER, 0 }, "Sets the string label of the menu or menu "
139		"item.",
140		0, { B_STRING_TYPE }
141	},
142
143	{ "Mark", { B_GET_PROPERTY, 0 },
144		{ B_DIRECT_SPECIFIER, 0 }, "Returns true if the menu item or the "
145		"menu's superitem is marked; false otherwise.",
146		0, { B_BOOL_TYPE }
147	},
148
149	{ "Mark", { B_SET_PROPERTY, 0 },
150		{ B_DIRECT_SPECIFIER, 0 }, "Marks or unmarks the menu item or the "
151		"menu's superitem.",
152		0, { B_BOOL_TYPE }
153	},
154
155	{ "Menu", { B_CREATE_PROPERTY, 0 },
156		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
157		"Adds a new menu item at the specified index with the text label "
158		"found in \"data\" and the int32 command found in \"what\" (used as "
159		"the what field in the BMessage sent by the item)." , 0, {},
160		{ 	{{{"data", B_STRING_TYPE}}}
161		}
162	},
163
164	{ "Menu", { B_DELETE_PROPERTY, 0 },
165		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
166		"Removes the selected menu or menus.", 0, {}
167	},
168
169	{ "Menu", { },
170		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
171		"Directs scripting message to the specified menu, first popping the "
172		"current specifier off the stack.", 0, {}
173	},
174
175	{ "MenuItem", { B_COUNT_PROPERTIES, 0 },
176		{ B_DIRECT_SPECIFIER, 0 }, "Counts the number of menu items in the "
177		"specified menu.",
178		0, { B_INT32_TYPE }
179	},
180
181	{ "MenuItem", { B_CREATE_PROPERTY, 0 },
182		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
183		"Adds a new menu item at the specified index with the text label "
184		"found in \"data\" and the int32 command found in \"what\" (used as "
185		"the what field in the BMessage sent by the item).", 0, {},
186		{	{ {{"data", B_STRING_TYPE },
187			{"be:invoke_message", B_MESSAGE_TYPE},
188			{"what", B_INT32_TYPE},
189			{"be:target", B_MESSENGER_TYPE}} }
190		}
191	},
192
193	{ "MenuItem", { B_DELETE_PROPERTY, 0 },
194		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
195		"Removes the specified menu item from its parent menu."
196	},
197
198	{ "MenuItem", { B_EXECUTE_PROPERTY, 0 },
199		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
200		"Invokes the specified menu item."
201	},
202
203	{ "MenuItem", { },
204		{ B_NAME_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, 0 },
205		"Directs scripting message to the specified menu, first popping the "
206		"current specifier off the stack."
207	},
208
209	{ 0 }
210};
211
212
213// note: this is redefined to localized one in BMenu::_InitData
214const char* BPrivate::kEmptyMenuLabel = "<empty>";
215
216
217struct BMenu::LayoutData {
218	BSize	preferred;
219	uint32	lastResizingMode;
220};
221
222
223// #pragma mark - BMenu
224
225
226BMenu::BMenu(const char* name, menu_layout layout)
227	:
228	BView(BRect(0, 0, 0, 0), name, 0, B_WILL_DRAW),
229	fChosenItem(NULL),
230	fPad(std::max(14.0f, be_plain_font->Size() + 2.0f), 2.0f, 20.0f, 0.0f),
231	fSelected(NULL),
232	fCachedMenuWindow(NULL),
233	fSuper(NULL),
234	fSuperitem(NULL),
235	fAscent(-1.0f),
236	fDescent(-1.0f),
237	fFontHeight(-1.0f),
238	fState(MENU_STATE_CLOSED),
239	fLayout(layout),
240	fExtraRect(NULL),
241	fMaxContentWidth(0.0f),
242	fInitMatrixSize(NULL),
243	fExtraMenuData(NULL),
244	fTrigger(0),
245	fResizeToFit(true),
246	fUseCachedMenuLayout(false),
247	fEnabled(true),
248	fDynamicName(false),
249	fRadioMode(false),
250	fTrackNewBounds(false),
251	fStickyMode(false),
252	fIgnoreHidden(true),
253	fTriggerEnabled(true),
254	fRedrawAfterSticky(false),
255	fAttachAborted(false)
256{
257	_InitData(NULL);
258}
259
260
261BMenu::BMenu(const char* name, float width, float height)
262	:
263	BView(BRect(0.0f, 0.0f, 0.0f, 0.0f), name, 0, B_WILL_DRAW),
264	fChosenItem(NULL),
265	fPad(14.0f, 2.0f, 20.0f, 0.0f),
266	fSelected(NULL),
267	fCachedMenuWindow(NULL),
268	fSuper(NULL),
269	fSuperitem(NULL),
270	fAscent(-1.0f),
271	fDescent(-1.0f),
272	fFontHeight(-1.0f),
273	fState(0),
274	fLayout(B_ITEMS_IN_MATRIX),
275	fExtraRect(NULL),
276	fMaxContentWidth(0.0f),
277	fInitMatrixSize(NULL),
278	fExtraMenuData(NULL),
279	fTrigger(0),
280	fResizeToFit(true),
281	fUseCachedMenuLayout(false),
282	fEnabled(true),
283	fDynamicName(false),
284	fRadioMode(false),
285	fTrackNewBounds(false),
286	fStickyMode(false),
287	fIgnoreHidden(true),
288	fTriggerEnabled(true),
289	fRedrawAfterSticky(false),
290	fAttachAborted(false)
291{
292	_InitData(NULL);
293}
294
295
296BMenu::BMenu(BMessage* archive)
297	:
298	BView(archive),
299	fChosenItem(NULL),
300	fPad(14.0f, 2.0f, 20.0f, 0.0f),
301	fSelected(NULL),
302	fCachedMenuWindow(NULL),
303	fSuper(NULL),
304	fSuperitem(NULL),
305	fAscent(-1.0f),
306	fDescent(-1.0f),
307	fFontHeight(-1.0f),
308	fState(MENU_STATE_CLOSED),
309	fLayout(B_ITEMS_IN_ROW),
310	fExtraRect(NULL),
311	fMaxContentWidth(0.0f),
312	fInitMatrixSize(NULL),
313	fExtraMenuData(NULL),
314	fTrigger(0),
315	fResizeToFit(true),
316	fUseCachedMenuLayout(false),
317	fEnabled(true),
318	fDynamicName(false),
319	fRadioMode(false),
320	fTrackNewBounds(false),
321	fStickyMode(false),
322	fIgnoreHidden(true),
323	fTriggerEnabled(true),
324	fRedrawAfterSticky(false),
325	fAttachAborted(false)
326{
327	_InitData(archive);
328}
329
330
331BMenu::~BMenu()
332{
333	_DeleteMenuWindow();
334
335	RemoveItems(0, CountItems(), true);
336
337	delete fInitMatrixSize;
338	delete fExtraMenuData;
339	delete fLayoutData;
340}
341
342
343BArchivable*
344BMenu::Instantiate(BMessage* archive)
345{
346	if (validate_instantiation(archive, "BMenu"))
347		return new (nothrow) BMenu(archive);
348
349	return NULL;
350}
351
352
353status_t
354BMenu::Archive(BMessage* data, bool deep) const
355{
356	status_t err = BView::Archive(data, deep);
357
358	if (err == B_OK && Layout() != B_ITEMS_IN_ROW)
359		err = data->AddInt32("_layout", Layout());
360	if (err == B_OK)
361		err = data->AddBool("_rsize_to_fit", fResizeToFit);
362	if (err == B_OK)
363		err = data->AddBool("_disable", !IsEnabled());
364	if (err ==  B_OK)
365		err = data->AddBool("_radio", IsRadioMode());
366	if (err == B_OK)
367		err = data->AddBool("_trig_disabled", AreTriggersEnabled());
368	if (err == B_OK)
369		err = data->AddBool("_dyn_label", fDynamicName);
370	if (err == B_OK)
371		err = data->AddFloat("_maxwidth", fMaxContentWidth);
372	if (err == B_OK && deep) {
373		BMenuItem* item = NULL;
374		int32 index = 0;
375		while ((item = ItemAt(index++)) != NULL) {
376			BMessage itemData;
377			item->Archive(&itemData, deep);
378			err = data->AddMessage("_items", &itemData);
379			if (err != B_OK)
380				break;
381			if (fLayout == B_ITEMS_IN_MATRIX) {
382				err = data->AddRect("_i_frames", item->fBounds);
383			}
384		}
385	}
386
387	return err;
388}
389
390
391void
392BMenu::AttachedToWindow()
393{
394	BView::AttachedToWindow();
395
396	_GetShiftKey(sShiftKey);
397	_GetControlKey(sControlKey);
398	_GetCommandKey(sCommandKey);
399	_GetOptionKey(sOptionKey);
400	_GetMenuKey(sMenuKey);
401
402	// The menu should be added to the menu hierarchy and made visible if:
403	// * the mouse is over the menu,
404	// * the user has requested the menu via the keyboard.
405	// So if we don't pass keydown in here, keyboard navigation breaks since
406	// fAttachAborted will return false if the mouse isn't over the menu
407	bool keyDown = Supermenu() != NULL
408		? Supermenu()->fState == MENU_STATE_KEY_TO_SUBMENU : false;
409	fAttachAborted = _AddDynamicItems(keyDown);
410
411	if (!fAttachAborted) {
412		_CacheFontInfo();
413		_LayoutItems(0);
414		_UpdateWindowViewSize(false);
415	}
416}
417
418
419void
420BMenu::DetachedFromWindow()
421{
422	BView::DetachedFromWindow();
423}
424
425
426void
427BMenu::AllAttached()
428{
429	BView::AllAttached();
430}
431
432
433void
434BMenu::AllDetached()
435{
436	BView::AllDetached();
437}
438
439
440void
441BMenu::Draw(BRect updateRect)
442{
443	if (_RelayoutIfNeeded()) {
444		Invalidate();
445		return;
446	}
447
448	DrawBackground(updateRect);
449	DrawItems(updateRect);
450}
451
452
453void
454BMenu::MessageReceived(BMessage* message)
455{
456	switch (message->what) {
457		case B_MOUSE_WHEEL_CHANGED:
458		{
459			float deltaY = 0;
460			message->FindFloat("be:wheel_delta_y", &deltaY);
461			if (deltaY == 0)
462				return;
463
464			BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
465			if (window == NULL)
466				return;
467
468			float largeStep;
469			float smallStep;
470			window->GetSteps(&smallStep, &largeStep);
471
472			// pressing the shift key scrolls faster
473			if ((modifiers() & B_SHIFT_KEY) != 0)
474				deltaY *= largeStep;
475			else
476				deltaY *= smallStep;
477
478			window->TryScrollBy(deltaY);
479			break;
480		}
481
482		default:
483			BView::MessageReceived(message);
484			break;
485	}
486}
487
488
489void
490BMenu::KeyDown(const char* bytes, int32 numBytes)
491{
492	// TODO: Test how it works on BeOS R5 and implement this correctly
493	switch (bytes[0]) {
494		case B_UP_ARROW:
495			if (fLayout == B_ITEMS_IN_COLUMN)
496				_SelectNextItem(fSelected, false);
497			break;
498
499		case B_DOWN_ARROW:
500		{
501			BMenuBar* bar = dynamic_cast<BMenuBar*>(Supermenu());
502			if (bar != NULL && fState == MENU_STATE_CLOSED) {
503				// tell MenuBar's _Track:
504				bar->fState = MENU_STATE_KEY_TO_SUBMENU;
505			}
506			if (fLayout == B_ITEMS_IN_COLUMN)
507				_SelectNextItem(fSelected, true);
508			break;
509		}
510
511		case B_LEFT_ARROW:
512			if (fLayout == B_ITEMS_IN_ROW)
513				_SelectNextItem(fSelected, false);
514			else {
515				// this case has to be handled a bit specially.
516				BMenuItem* item = Superitem();
517				if (item) {
518					if (dynamic_cast<BMenuBar*>(Supermenu())) {
519						// If we're at the top menu below the menu bar, pass
520						// the keypress to the menu bar so we can move to
521						// another top level menu.
522						BMessenger messenger(Supermenu());
523						messenger.SendMessage(Window()->CurrentMessage());
524					} else {
525						// tell _Track
526						fState = MENU_STATE_KEY_LEAVE_SUBMENU;
527					}
528				}
529			}
530			break;
531
532		case B_RIGHT_ARROW:
533			if (fLayout == B_ITEMS_IN_ROW)
534				_SelectNextItem(fSelected, true);
535			else {
536				if (fSelected != NULL && fSelected->Submenu() != NULL) {
537					fSelected->Submenu()->_SetStickyMode(true);
538						// fix me: this shouldn't be needed but dynamic menus
539						// aren't getting it set correctly when keyboard
540						// navigating, which aborts the attach
541					fState = MENU_STATE_KEY_TO_SUBMENU;
542					_SelectItem(fSelected, true, true, true);
543				} else if (dynamic_cast<BMenuBar*>(Supermenu())) {
544					// if we have no submenu and we're an
545					// item in the top menu below the menubar,
546					// pass the keypress to the menubar
547					// so you can use the keypress to switch menus.
548					BMessenger messenger(Supermenu());
549					messenger.SendMessage(Window()->CurrentMessage());
550				}
551			}
552			break;
553
554		case B_PAGE_UP:
555		case B_PAGE_DOWN:
556		{
557			BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
558			if (window == NULL || !window->HasScrollers())
559				break;
560
561			int32 deltaY = bytes[0] == B_PAGE_UP ? -1 : 1;
562
563			float largeStep;
564			window->GetSteps(NULL, &largeStep);
565			window->TryScrollBy(deltaY * largeStep);
566			break;
567		}
568
569		case B_ENTER:
570		case B_SPACE:
571			if (fSelected != NULL) {
572				fChosenItem = fSelected;
573					// preserve for exit handling
574				_QuitTracking(false);
575			}
576			break;
577
578		case B_ESCAPE:
579			_SelectItem(NULL);
580			if (fState == MENU_STATE_CLOSED
581				&& dynamic_cast<BMenuBar*>(Supermenu())) {
582				// Keyboard may show menu without tracking it
583				BMessenger messenger(Supermenu());
584				messenger.SendMessage(Window()->CurrentMessage());
585			} else
586				_QuitTracking(false);
587			break;
588
589		default:
590		{
591			uint32 trigger = BUnicodeChar::FromUTF8(&bytes);
592
593			for (uint32 i = CountItems(); i-- > 0;) {
594				BMenuItem* item = ItemAt(i);
595				if (item->fTriggerIndex < 0 || item->fTrigger != trigger)
596					continue;
597
598				_InvokeItem(item);
599				_QuitTracking(false);
600				break;
601			}
602			break;
603		}
604	}
605}
606
607
608BSize
609BMenu::MinSize()
610{
611	_ValidatePreferredSize();
612
613	BSize size = (GetLayout() != NULL ? GetLayout()->MinSize()
614		: fLayoutData->preferred);
615
616	return BLayoutUtils::ComposeSize(ExplicitMinSize(), size);
617}
618
619
620BSize
621BMenu::MaxSize()
622{
623	_ValidatePreferredSize();
624
625	BSize size = (GetLayout() != NULL ? GetLayout()->MaxSize()
626		: fLayoutData->preferred);
627
628	return BLayoutUtils::ComposeSize(ExplicitMaxSize(), size);
629}
630
631
632BSize
633BMenu::PreferredSize()
634{
635	_ValidatePreferredSize();
636
637	BSize size = (GetLayout() != NULL ? GetLayout()->PreferredSize()
638		: fLayoutData->preferred);
639
640	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), size);
641}
642
643
644void
645BMenu::GetPreferredSize(float* _width, float* _height)
646{
647	_ValidatePreferredSize();
648
649	if (_width)
650		*_width = fLayoutData->preferred.width;
651
652	if (_height)
653		*_height = fLayoutData->preferred.height;
654}
655
656
657void
658BMenu::ResizeToPreferred()
659{
660	BView::ResizeToPreferred();
661}
662
663
664void
665BMenu::DoLayout()
666{
667	// If the user set a layout, we let the base class version call its
668	// hook.
669	if (GetLayout() != NULL) {
670		BView::DoLayout();
671		return;
672	}
673
674	if (_RelayoutIfNeeded())
675		Invalidate();
676}
677
678
679void
680BMenu::FrameMoved(BPoint new_position)
681{
682	BView::FrameMoved(new_position);
683}
684
685
686void
687BMenu::FrameResized(float new_width, float new_height)
688{
689	BView::FrameResized(new_width, new_height);
690}
691
692
693void
694BMenu::InvalidateLayout()
695{
696	fUseCachedMenuLayout = false;
697	// This method exits for backwards compatibility reasons, it is used to
698	// invalidate the menu layout, but we also use call
699	// BView::InvalidateLayout() for good measure. Don't delete this method!
700	BView::InvalidateLayout(false);
701}
702
703
704void
705BMenu::MakeFocus(bool focused)
706{
707	BView::MakeFocus(focused);
708}
709
710
711bool
712BMenu::AddItem(BMenuItem* item)
713{
714	return AddItem(item, CountItems());
715}
716
717
718bool
719BMenu::AddItem(BMenuItem* item, int32 index)
720{
721	if (fLayout == B_ITEMS_IN_MATRIX) {
722		debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
723			"be called if the menu layout is not B_ITEMS_IN_MATRIX");
724	}
725
726	if (item == NULL || !_AddItem(item, index))
727		return false;
728
729	InvalidateLayout();
730	if (LockLooper()) {
731		if (!Window()->IsHidden()) {
732			_LayoutItems(index);
733			_UpdateWindowViewSize(false);
734			Invalidate();
735		}
736		UnlockLooper();
737	}
738
739	return true;
740}
741
742
743bool
744BMenu::AddItem(BMenuItem* item, BRect frame)
745{
746	if (fLayout != B_ITEMS_IN_MATRIX) {
747		debugger("BMenu::AddItem(BMenuItem*, BRect) this method can only "
748			"be called if the menu layout is B_ITEMS_IN_MATRIX");
749	}
750
751	if (item == NULL)
752		return false;
753
754	item->fBounds = frame;
755
756	int32 index = CountItems();
757	if (!_AddItem(item, index))
758		return false;
759
760	if (LockLooper()) {
761		if (!Window()->IsHidden()) {
762			_LayoutItems(index);
763			Invalidate();
764		}
765		UnlockLooper();
766	}
767
768	return true;
769}
770
771
772bool
773BMenu::AddItem(BMenu* submenu)
774{
775	BMenuItem* item = new (nothrow) BMenuItem(submenu);
776	if (item == NULL)
777		return false;
778
779	if (!AddItem(item, CountItems())) {
780		item->fSubmenu = NULL;
781		delete item;
782		return false;
783	}
784
785	return true;
786}
787
788
789bool
790BMenu::AddItem(BMenu* submenu, int32 index)
791{
792	if (fLayout == B_ITEMS_IN_MATRIX) {
793		debugger("BMenu::AddItem(BMenuItem*, int32) this method can only "
794			"be called if the menu layout is not B_ITEMS_IN_MATRIX");
795	}
796
797	BMenuItem* item = new (nothrow) BMenuItem(submenu);
798	if (item == NULL)
799		return false;
800
801	if (!AddItem(item, index)) {
802		item->fSubmenu = NULL;
803		delete item;
804		return false;
805	}
806
807	return true;
808}
809
810
811bool
812BMenu::AddItem(BMenu* submenu, BRect frame)
813{
814	if (fLayout != B_ITEMS_IN_MATRIX) {
815		debugger("BMenu::AddItem(BMenu*, BRect) this method can only "
816			"be called if the menu layout is B_ITEMS_IN_MATRIX");
817	}
818
819	BMenuItem* item = new (nothrow) BMenuItem(submenu);
820	if (item == NULL)
821		return false;
822
823	if (!AddItem(item, frame)) {
824		item->fSubmenu = NULL;
825		delete item;
826		return false;
827	}
828
829	return true;
830}
831
832
833bool
834BMenu::AddList(BList* list, int32 index)
835{
836	// TODO: test this function, it's not documented in the bebook.
837	if (list == NULL)
838		return false;
839
840	bool locked = LockLooper();
841
842	int32 numItems = list->CountItems();
843	for (int32 i = 0; i < numItems; i++) {
844		BMenuItem* item = static_cast<BMenuItem*>(list->ItemAt(i));
845		if (item != NULL) {
846			if (!_AddItem(item, index + i))
847				break;
848		}
849	}
850
851	InvalidateLayout();
852	if (locked && Window() != NULL && !Window()->IsHidden()) {
853		// Make sure we update the layout if needed.
854		_LayoutItems(index);
855		_UpdateWindowViewSize(false);
856		Invalidate();
857	}
858
859	if (locked)
860		UnlockLooper();
861
862	return true;
863}
864
865
866bool
867BMenu::AddSeparatorItem()
868{
869	BMenuItem* item = new (nothrow) BSeparatorItem();
870	if (!item || !AddItem(item, CountItems())) {
871		delete item;
872		return false;
873	}
874
875	return true;
876}
877
878
879bool
880BMenu::RemoveItem(BMenuItem* item)
881{
882	return _RemoveItems(0, 0, item, false);
883}
884
885
886BMenuItem*
887BMenu::RemoveItem(int32 index)
888{
889	BMenuItem* item = ItemAt(index);
890	if (item != NULL)
891		_RemoveItems(0, 0, item, false);
892	return item;
893}
894
895
896bool
897BMenu::RemoveItems(int32 index, int32 count, bool deleteItems)
898{
899	return _RemoveItems(index, count, NULL, deleteItems);
900}
901
902
903bool
904BMenu::RemoveItem(BMenu* submenu)
905{
906	for (int32 i = 0; i < fItems.CountItems(); i++) {
907		if (static_cast<BMenuItem*>(fItems.ItemAtFast(i))->Submenu()
908				== submenu) {
909			return _RemoveItems(i, 1, NULL, false);
910		}
911	}
912
913	return false;
914}
915
916
917int32
918BMenu::CountItems() const
919{
920	return fItems.CountItems();
921}
922
923
924BMenuItem*
925BMenu::ItemAt(int32 index) const
926{
927	return static_cast<BMenuItem*>(fItems.ItemAt(index));
928}
929
930
931BMenu*
932BMenu::SubmenuAt(int32 index) const
933{
934	BMenuItem* item = static_cast<BMenuItem*>(fItems.ItemAt(index));
935	return item != NULL ? item->Submenu() : NULL;
936}
937
938
939int32
940BMenu::IndexOf(BMenuItem* item) const
941{
942	return fItems.IndexOf(item);
943}
944
945
946int32
947BMenu::IndexOf(BMenu* submenu) const
948{
949	for (int32 i = 0; i < fItems.CountItems(); i++) {
950		if (ItemAt(i)->Submenu() == submenu)
951			return i;
952	}
953
954	return -1;
955}
956
957
958BMenuItem*
959BMenu::FindItem(const char* label) const
960{
961	BMenuItem* item = NULL;
962
963	for (int32 i = 0; i < CountItems(); i++) {
964		item = ItemAt(i);
965
966		if (item->Label() && strcmp(item->Label(), label) == 0)
967			return item;
968
969		if (item->Submenu() != NULL) {
970			item = item->Submenu()->FindItem(label);
971			if (item != NULL)
972				return item;
973		}
974	}
975
976	return NULL;
977}
978
979
980BMenuItem*
981BMenu::FindItem(uint32 command) const
982{
983	BMenuItem* item = NULL;
984
985	for (int32 i = 0; i < CountItems(); i++) {
986		item = ItemAt(i);
987
988		if (item->Command() == command)
989			return item;
990
991		if (item->Submenu() != NULL) {
992			item = item->Submenu()->FindItem(command);
993			if (item != NULL)
994				return item;
995		}
996	}
997
998	return NULL;
999}
1000
1001
1002status_t
1003BMenu::SetTargetForItems(BHandler* handler)
1004{
1005	status_t status = B_OK;
1006	for (int32 i = 0; i < fItems.CountItems(); i++) {
1007		status = ItemAt(i)->SetTarget(handler);
1008		if (status < B_OK)
1009			break;
1010	}
1011
1012	return status;
1013}
1014
1015
1016status_t
1017BMenu::SetTargetForItems(BMessenger messenger)
1018{
1019	status_t status = B_OK;
1020	for (int32 i = 0; i < fItems.CountItems(); i++) {
1021		status = ItemAt(i)->SetTarget(messenger);
1022		if (status < B_OK)
1023			break;
1024	}
1025
1026	return status;
1027}
1028
1029
1030void
1031BMenu::SetEnabled(bool enable)
1032{
1033	if (fEnabled == enable)
1034		return;
1035
1036	fEnabled = enable;
1037
1038	if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL)
1039		Supermenu()->SetEnabled(enable);
1040
1041	if (fSuperitem)
1042		fSuperitem->SetEnabled(enable);
1043}
1044
1045
1046void
1047BMenu::SetRadioMode(bool on)
1048{
1049	fRadioMode = on;
1050	if (!on)
1051		SetLabelFromMarked(false);
1052}
1053
1054
1055void
1056BMenu::SetTriggersEnabled(bool enable)
1057{
1058	fTriggerEnabled = enable;
1059}
1060
1061
1062void
1063BMenu::SetMaxContentWidth(float width)
1064{
1065	fMaxContentWidth = width;
1066}
1067
1068
1069void
1070BMenu::SetLabelFromMarked(bool on)
1071{
1072	fDynamicName = on;
1073	if (on)
1074		SetRadioMode(true);
1075}
1076
1077
1078bool
1079BMenu::IsLabelFromMarked()
1080{
1081	return fDynamicName;
1082}
1083
1084
1085bool
1086BMenu::IsEnabled() const
1087{
1088	if (!fEnabled)
1089		return false;
1090
1091	return fSuper ? fSuper->IsEnabled() : true ;
1092}
1093
1094
1095bool
1096BMenu::IsRadioMode() const
1097{
1098	return fRadioMode;
1099}
1100
1101
1102bool
1103BMenu::AreTriggersEnabled() const
1104{
1105	return fTriggerEnabled;
1106}
1107
1108
1109bool
1110BMenu::IsRedrawAfterSticky() const
1111{
1112	return fRedrawAfterSticky;
1113}
1114
1115
1116float
1117BMenu::MaxContentWidth() const
1118{
1119	return fMaxContentWidth;
1120}
1121
1122
1123BMenuItem*
1124BMenu::FindMarked()
1125{
1126	for (int32 i = 0; i < fItems.CountItems(); i++) {
1127		BMenuItem* item = ItemAt(i);
1128
1129		if (item->IsMarked())
1130			return item;
1131	}
1132
1133	return NULL;
1134}
1135
1136
1137int32
1138BMenu::FindMarkedIndex()
1139{
1140	for (int32 i = 0; i < fItems.CountItems(); i++) {
1141		BMenuItem* item = ItemAt(i);
1142
1143		if (item->IsMarked())
1144			return i;
1145	}
1146
1147	return -1;
1148}
1149
1150
1151BMenu*
1152BMenu::Supermenu() const
1153{
1154	return fSuper;
1155}
1156
1157
1158BMenuItem*
1159BMenu::Superitem() const
1160{
1161	return fSuperitem;
1162}
1163
1164
1165BHandler*
1166BMenu::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
1167	int32 form, const char* property)
1168{
1169	BPropertyInfo propInfo(sPropList);
1170	BHandler* target = NULL;
1171
1172	switch (propInfo.FindMatch(msg, 0, specifier, form, property)) {
1173		case B_ERROR:
1174			break;
1175
1176		case 0:
1177		case 1:
1178		case 2:
1179		case 3:
1180		case 4:
1181		case 5:
1182		case 6:
1183		case 7:
1184			target = this;
1185			break;
1186		case 8:
1187			// TODO: redirect to menu
1188			target = this;
1189			break;
1190		case 9:
1191		case 10:
1192		case 11:
1193		case 12:
1194			target = this;
1195			break;
1196		case 13:
1197			// TODO: redirect to menuitem
1198			target = this;
1199			break;
1200	}
1201
1202	if (!target)
1203		target = BView::ResolveSpecifier(msg, index, specifier, form,
1204		property);
1205
1206	return target;
1207}
1208
1209
1210status_t
1211BMenu::GetSupportedSuites(BMessage* data)
1212{
1213	if (data == NULL)
1214		return B_BAD_VALUE;
1215
1216	status_t err = data->AddString("suites", "suite/vnd.Be-menu");
1217
1218	if (err < B_OK)
1219		return err;
1220
1221	BPropertyInfo propertyInfo(sPropList);
1222	err = data->AddFlat("messages", &propertyInfo);
1223
1224	if (err < B_OK)
1225		return err;
1226
1227	return BView::GetSupportedSuites(data);
1228}
1229
1230
1231status_t
1232BMenu::Perform(perform_code code, void* _data)
1233{
1234	switch (code) {
1235		case PERFORM_CODE_MIN_SIZE:
1236			((perform_data_min_size*)_data)->return_value
1237				= BMenu::MinSize();
1238			return B_OK;
1239
1240		case PERFORM_CODE_MAX_SIZE:
1241			((perform_data_max_size*)_data)->return_value
1242				= BMenu::MaxSize();
1243			return B_OK;
1244
1245		case PERFORM_CODE_PREFERRED_SIZE:
1246			((perform_data_preferred_size*)_data)->return_value
1247				= BMenu::PreferredSize();
1248			return B_OK;
1249
1250		case PERFORM_CODE_LAYOUT_ALIGNMENT:
1251			((perform_data_layout_alignment*)_data)->return_value
1252				= BMenu::LayoutAlignment();
1253			return B_OK;
1254
1255		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
1256			((perform_data_has_height_for_width*)_data)->return_value
1257				= BMenu::HasHeightForWidth();
1258			return B_OK;
1259
1260		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
1261		{
1262			perform_data_get_height_for_width* data
1263				= (perform_data_get_height_for_width*)_data;
1264			BMenu::GetHeightForWidth(data->width, &data->min, &data->max,
1265				&data->preferred);
1266			return B_OK;
1267		}
1268
1269		case PERFORM_CODE_SET_LAYOUT:
1270		{
1271			perform_data_set_layout* data = (perform_data_set_layout*)_data;
1272			BMenu::SetLayout(data->layout);
1273			return B_OK;
1274		}
1275
1276		case PERFORM_CODE_LAYOUT_INVALIDATED:
1277		{
1278			perform_data_layout_invalidated* data
1279				= (perform_data_layout_invalidated*)_data;
1280			BMenu::LayoutInvalidated(data->descendants);
1281			return B_OK;
1282		}
1283
1284		case PERFORM_CODE_DO_LAYOUT:
1285		{
1286			BMenu::DoLayout();
1287			return B_OK;
1288		}
1289	}
1290
1291	return BView::Perform(code, _data);
1292}
1293
1294
1295// #pragma mark - BMenu protected methods
1296
1297
1298BMenu::BMenu(BRect frame, const char* name, uint32 resizingMode, uint32 flags,
1299	menu_layout layout, bool resizeToFit)
1300	:
1301	BView(frame, name, resizingMode, flags),
1302	fChosenItem(NULL),
1303	fSelected(NULL),
1304	fCachedMenuWindow(NULL),
1305	fSuper(NULL),
1306	fSuperitem(NULL),
1307	fAscent(-1.0f),
1308	fDescent(-1.0f),
1309	fFontHeight(-1.0f),
1310	fState(MENU_STATE_CLOSED),
1311	fLayout(layout),
1312	fExtraRect(NULL),
1313	fMaxContentWidth(0.0f),
1314	fInitMatrixSize(NULL),
1315	fExtraMenuData(NULL),
1316	fTrigger(0),
1317	fResizeToFit(resizeToFit),
1318	fUseCachedMenuLayout(false),
1319	fEnabled(true),
1320	fDynamicName(false),
1321	fRadioMode(false),
1322	fTrackNewBounds(false),
1323	fStickyMode(false),
1324	fIgnoreHidden(true),
1325	fTriggerEnabled(true),
1326	fRedrawAfterSticky(false),
1327	fAttachAborted(false)
1328{
1329	_InitData(NULL);
1330}
1331
1332
1333void
1334BMenu::SetItemMargins(float left, float top, float right, float bottom)
1335{
1336	fPad.Set(left, top, right, bottom);
1337}
1338
1339
1340void
1341BMenu::GetItemMargins(float* _left, float* _top, float* _right,
1342	float* _bottom) const
1343{
1344	if (_left != NULL)
1345		*_left = fPad.left;
1346
1347	if (_top != NULL)
1348		*_top = fPad.top;
1349
1350	if (_right != NULL)
1351		*_right = fPad.right;
1352
1353	if (_bottom != NULL)
1354		*_bottom = fPad.bottom;
1355}
1356
1357
1358menu_layout
1359BMenu::Layout() const
1360{
1361	return fLayout;
1362}
1363
1364
1365void
1366BMenu::Show()
1367{
1368	Show(false);
1369}
1370
1371
1372void
1373BMenu::Show(bool selectFirst)
1374{
1375	_Install(NULL);
1376	_Show(selectFirst);
1377}
1378
1379
1380void
1381BMenu::Hide()
1382{
1383	_Hide();
1384	_Uninstall();
1385}
1386
1387
1388BMenuItem*
1389BMenu::Track(bool sticky, BRect* clickToOpenRect)
1390{
1391	if (sticky && LockLooper()) {
1392		//RedrawAfterSticky(Bounds());
1393			// the call above didn't do anything, so I've removed it for now
1394		UnlockLooper();
1395	}
1396
1397	if (clickToOpenRect != NULL && LockLooper()) {
1398		fExtraRect = clickToOpenRect;
1399		ConvertFromScreen(fExtraRect);
1400		UnlockLooper();
1401	}
1402
1403	_SetStickyMode(sticky);
1404
1405	int action;
1406	BMenuItem* menuItem = _Track(&action);
1407
1408	fExtraRect = NULL;
1409
1410	return menuItem;
1411}
1412
1413
1414// #pragma mark - BMenu private methods
1415
1416
1417bool
1418BMenu::AddDynamicItem(add_state state)
1419{
1420	// Implemented in subclasses
1421	return false;
1422}
1423
1424
1425void
1426BMenu::DrawBackground(BRect updateRect)
1427{
1428	rgb_color base = ui_color(B_MENU_BACKGROUND_COLOR);
1429	uint32 flags = 0;
1430	if (!IsEnabled())
1431		flags |= BControlLook::B_DISABLED;
1432
1433	if (IsFocus())
1434		flags |= BControlLook::B_FOCUSED;
1435
1436	BRect rect = Bounds();
1437	uint32 borders = BControlLook::B_LEFT_BORDER
1438		| BControlLook::B_RIGHT_BORDER;
1439	if (Window() != NULL && Parent() != NULL) {
1440		if (Parent()->Frame().top == Window()->Bounds().top)
1441			borders |= BControlLook::B_TOP_BORDER;
1442
1443		if (Parent()->Frame().bottom == Window()->Bounds().bottom)
1444			borders |= BControlLook::B_BOTTOM_BORDER;
1445	} else {
1446		borders |= BControlLook::B_TOP_BORDER
1447			| BControlLook::B_BOTTOM_BORDER;
1448	}
1449	be_control_look->DrawMenuBackground(this, rect, updateRect, base, 0,
1450		borders);
1451}
1452
1453
1454void
1455BMenu::SetTrackingHook(menu_tracking_hook func, void* state)
1456{
1457	fExtraMenuData->trackingHook = func;
1458	fExtraMenuData->trackingState = state;
1459}
1460
1461
1462void BMenu::_ReservedMenu3() {}
1463void BMenu::_ReservedMenu4() {}
1464void BMenu::_ReservedMenu5() {}
1465void BMenu::_ReservedMenu6() {}
1466
1467
1468void
1469BMenu::_InitData(BMessage* archive)
1470{
1471	BPrivate::kEmptyMenuLabel = B_TRANSLATE("<empty>");
1472
1473	// TODO: Get _color, _fname, _fflt from the message, if present
1474	BFont font;
1475	font.SetFamilyAndStyle(sMenuInfo.f_family, sMenuInfo.f_style);
1476	font.SetSize(sMenuInfo.font_size);
1477	SetFont(&font, B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE);
1478
1479	fExtraMenuData = new (nothrow) BPrivate::ExtraMenuData();
1480
1481	fLayoutData = new LayoutData;
1482	fLayoutData->lastResizingMode = ResizingMode();
1483
1484	SetLowUIColor(B_MENU_BACKGROUND_COLOR);
1485	SetViewColor(B_TRANSPARENT_COLOR);
1486
1487	fTriggerEnabled = sMenuInfo.triggers_always_shown;
1488
1489	if (archive != NULL) {
1490		archive->FindInt32("_layout", (int32*)&fLayout);
1491		archive->FindBool("_rsize_to_fit", &fResizeToFit);
1492		bool disabled;
1493		if (archive->FindBool("_disable", &disabled) == B_OK)
1494			fEnabled = !disabled;
1495		archive->FindBool("_radio", &fRadioMode);
1496
1497		bool disableTrigger = false;
1498		archive->FindBool("_trig_disabled", &disableTrigger);
1499		fTriggerEnabled = !disableTrigger;
1500
1501		archive->FindBool("_dyn_label", &fDynamicName);
1502		archive->FindFloat("_maxwidth", &fMaxContentWidth);
1503
1504		BMessage msg;
1505		for (int32 i = 0; archive->FindMessage("_items", i, &msg) == B_OK; i++) {
1506			BArchivable* object = instantiate_object(&msg);
1507			if (BMenuItem* item = dynamic_cast<BMenuItem*>(object)) {
1508				BRect bounds;
1509				if (fLayout == B_ITEMS_IN_MATRIX
1510					&& archive->FindRect("_i_frames", i, &bounds) == B_OK)
1511					AddItem(item, bounds);
1512				else
1513					AddItem(item);
1514			}
1515		}
1516	}
1517}
1518
1519
1520bool
1521BMenu::_Show(bool selectFirstItem, bool keyDown)
1522{
1523	if (Window() != NULL)
1524		return false;
1525
1526	// See if the supermenu has a cached menuwindow,
1527	// and use that one if possible.
1528	BMenuWindow* window = NULL;
1529	bool ourWindow = false;
1530	if (fSuper != NULL) {
1531		fSuperbounds = fSuper->ConvertToScreen(fSuper->Bounds());
1532		window = fSuper->_MenuWindow();
1533	}
1534
1535	// Otherwise, create a new one
1536	// This happens for "stand alone" BPopUpMenus
1537	// (i.e. not within a BMenuField)
1538	if (window == NULL) {
1539		// Menu windows get the BMenu's handler name
1540		window = new (nothrow) BMenuWindow(Name());
1541		ourWindow = true;
1542	}
1543
1544	if (window == NULL)
1545		return false;
1546
1547	if (window->Lock()) {
1548		bool addAborted = false;
1549		if (keyDown)
1550			addAborted = _AddDynamicItems(keyDown);
1551
1552		if (addAborted) {
1553			if (ourWindow)
1554				window->Quit();
1555			else
1556				window->Unlock();
1557			return false;
1558		}
1559		fAttachAborted = false;
1560
1561		window->AttachMenu(this);
1562
1563		if (ItemAt(0) != NULL) {
1564			float width, height;
1565			ItemAt(0)->GetContentSize(&width, &height);
1566
1567			window->SetSmallStep(ceilf(height));
1568		}
1569
1570		// Menu didn't have the time to add its items: aborting...
1571		if (fAttachAborted) {
1572			window->DetachMenu();
1573			// TODO: Probably not needed, we can just let _hide() quit the
1574			// window.
1575			if (ourWindow)
1576				window->Quit();
1577			else
1578				window->Unlock();
1579			return false;
1580		}
1581
1582		_UpdateWindowViewSize(true);
1583		window->Show();
1584
1585		if (selectFirstItem)
1586			_SelectItem(ItemAt(0), false);
1587
1588		window->Unlock();
1589	}
1590
1591	return true;
1592}
1593
1594
1595void
1596BMenu::_Hide()
1597{
1598	BMenuWindow* window = dynamic_cast<BMenuWindow*>(Window());
1599	if (window == NULL || !window->Lock())
1600		return;
1601
1602	if (fSelected != NULL)
1603		_SelectItem(NULL);
1604
1605	window->Hide();
1606	window->DetachMenu();
1607		// we don't want to be deleted when the window is removed
1608
1609#if USE_CACHED_MENUWINDOW
1610	if (fSuper != NULL)
1611		window->Unlock();
1612	else
1613#endif
1614		window->Quit();
1615			// it's our window, quit it
1616
1617	_DeleteMenuWindow();
1618		// Delete the menu window used by our submenus
1619}
1620
1621
1622// #pragma mark - mouse tracking
1623
1624
1625const static bigtime_t kOpenSubmenuDelay = 225000;
1626const static bigtime_t kNavigationAreaTimeout = 1000000;
1627
1628
1629BMenuItem*
1630BMenu::_Track(int* action, long start)
1631{
1632	// TODO: cleanup
1633	BMenuItem* item = NULL;
1634	BRect navAreaRectAbove;
1635	BRect navAreaRectBelow;
1636	bigtime_t selectedTime = system_time();
1637	bigtime_t navigationAreaTime = 0;
1638
1639	fState = MENU_STATE_TRACKING;
1640	fChosenItem = NULL;
1641		// we will use this for keyboard selection
1642
1643	BPoint location;
1644	uint32 buttons = 0;
1645	if (LockLooper()) {
1646		GetMouse(&location, &buttons);
1647		UnlockLooper();
1648	}
1649
1650	bool releasedOnce = buttons == 0;
1651	while (fState != MENU_STATE_CLOSED) {
1652		if (_CustomTrackingWantsToQuit())
1653			break;
1654
1655		if (!LockLooper())
1656			break;
1657
1658		BMenuWindow* window = static_cast<BMenuWindow*>(Window());
1659		BPoint screenLocation = ConvertToScreen(location);
1660		if (window->CheckForScrolling(screenLocation)) {
1661			UnlockLooper();
1662			continue;
1663		}
1664
1665		// The order of the checks is important
1666		// to be able to handle overlapping menus:
1667		// first we check if mouse is inside a submenu,
1668		// then if the mouse is inside this menu,
1669		// then if it's over a super menu.
1670		if (_OverSubmenu(fSelected, screenLocation)
1671			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
1672			if (fState == MENU_STATE_TRACKING) {
1673				// not if from R.Arrow
1674				fState = MENU_STATE_TRACKING_SUBMENU;
1675			}
1676			navAreaRectAbove = BRect();
1677			navAreaRectBelow = BRect();
1678
1679			// Since the submenu has its own looper,
1680			// we can unlock ours. Doing so also make sure
1681			// that our window gets any update message to
1682			// redraw itself
1683			UnlockLooper();
1684
1685			// To prevent NULL access violation, ensure a menu has actually
1686			// been selected and that it has a submenu. Because keyboard and
1687			// mouse interactions set selected items differently, the menu
1688			// tracking thread needs to be careful in triggering the navigation
1689			// to the submenu.
1690			if (fSelected != NULL) {
1691				BMenu* submenu = fSelected->Submenu();
1692				int submenuAction = MENU_STATE_TRACKING;
1693				if (submenu != NULL) {
1694					submenu->_SetStickyMode(_IsStickyMode());
1695
1696					// The following call blocks until the submenu
1697					// gives control back to us, either because the mouse
1698					// pointer goes out of the submenu's bounds, or because
1699					// the user closes the menu
1700					BMenuItem* submenuItem = submenu->_Track(&submenuAction);
1701					if (submenuAction == MENU_STATE_CLOSED) {
1702						item = submenuItem;
1703						fState = MENU_STATE_CLOSED;
1704					} else if (submenuAction == MENU_STATE_KEY_LEAVE_SUBMENU) {
1705						if (LockLooper()) {
1706							BMenuItem* temp = fSelected;
1707							// close the submenu:
1708							_SelectItem(NULL);
1709							// but reselect the item itself for user:
1710							_SelectItem(temp, false);
1711							UnlockLooper();
1712						}
1713						// cancel  key-nav state
1714						fState = MENU_STATE_TRACKING;
1715					} else
1716						fState = MENU_STATE_TRACKING;
1717				}
1718			}
1719			if (!LockLooper())
1720				break;
1721		} else if ((item = _HitTestItems(location, B_ORIGIN)) != NULL) {
1722			_UpdateStateOpenSelect(item, location, navAreaRectAbove,
1723				navAreaRectBelow, selectedTime, navigationAreaTime);
1724			releasedOnce = true;
1725		} else if (_OverSuper(screenLocation)
1726			&& fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
1727			fState = MENU_STATE_TRACKING;
1728			UnlockLooper();
1729			break;
1730		} else if (fState == MENU_STATE_KEY_LEAVE_SUBMENU) {
1731			UnlockLooper();
1732			break;
1733		} else if (fSuper == NULL
1734			|| fSuper->fState != MENU_STATE_KEY_TO_SUBMENU) {
1735			// Mouse pointer outside menu:
1736			// If there's no other submenu opened,
1737			// deselect the current selected item
1738			if (fSelected != NULL
1739				&& (fSelected->Submenu() == NULL
1740					|| fSelected->Submenu()->Window() == NULL)) {
1741				_SelectItem(NULL);
1742				fState = MENU_STATE_TRACKING;
1743			}
1744
1745			if (fSuper != NULL) {
1746				// Give supermenu the chance to continue tracking
1747				*action = fState;
1748				UnlockLooper();
1749				return NULL;
1750			}
1751		}
1752
1753		UnlockLooper();
1754
1755		if (releasedOnce)
1756			_UpdateStateClose(item, location, buttons);
1757
1758		if (fState != MENU_STATE_CLOSED) {
1759			bigtime_t snoozeAmount = 50000;
1760
1761			BPoint newLocation = location;
1762			uint32 newButtons = buttons;
1763
1764			// If user doesn't move the mouse, loop here,
1765			// so we don't interfere with keyboard menu navigation
1766			do {
1767				snooze(snoozeAmount);
1768				if (!LockLooper())
1769					break;
1770				GetMouse(&newLocation, &newButtons, true);
1771				UnlockLooper();
1772			} while (newLocation == location && newButtons == buttons
1773				&& !(item != NULL && item->Submenu() != NULL
1774					&& item->Submenu()->Window() == NULL)
1775				&& fState == MENU_STATE_TRACKING);
1776
1777			if (newLocation != location || newButtons != buttons) {
1778				if (!releasedOnce && newButtons == 0 && buttons != 0)
1779					releasedOnce = true;
1780				location = newLocation;
1781				buttons = newButtons;
1782			}
1783
1784			if (releasedOnce)
1785				_UpdateStateClose(item, location, buttons);
1786		}
1787	}
1788
1789	if (action != NULL)
1790		*action = fState;
1791
1792	// keyboard Enter will set this
1793	if (fChosenItem != NULL)
1794		item = fChosenItem;
1795	else if (fSelected == NULL) {
1796		// needed to cover (rare) mouse/ESC combination
1797		item = NULL;
1798	}
1799
1800	if (fSelected != NULL && LockLooper()) {
1801		_SelectItem(NULL);
1802		UnlockLooper();
1803	}
1804
1805	// delete the menu window recycled for all the child menus
1806	_DeleteMenuWindow();
1807
1808	return item;
1809}
1810
1811
1812void
1813BMenu::_UpdateNavigationArea(BPoint position, BRect& navAreaRectAbove,
1814	BRect& navAreaRectBelow)
1815{
1816#define NAV_AREA_THRESHOLD    8
1817
1818	// The navigation area is a region in which mouse-overs won't select
1819	// the item under the cursor. This makes it easier to navigate to
1820	// submenus, as the cursor can be moved to submenu items directly instead
1821	// of having to move it horizontally into the submenu first. The concept
1822	// is illustrated below:
1823	//
1824	// +-------+----+---------+
1825	// |       |   /|         |
1826	// |       |  /*|         |
1827	// |[2]--> | /**|         |
1828	// |       |/[4]|         |
1829	// |------------|         |
1830	// |    [1]     |   [6]   |
1831	// |------------|         |
1832	// |       |\[5]|         |
1833	// |[3]--> | \**|         |
1834	// |       |  \*|         |
1835	// |       |   \|         |
1836	// |       +----|---------+
1837	// |            |
1838	// +------------+
1839	//
1840	// [1] Selected item, cursor position ('position')
1841	// [2] Upper navigation area rectangle ('navAreaRectAbove')
1842	// [3] Lower navigation area rectangle ('navAreaRectBelow')
1843	// [4] Upper navigation area
1844	// [5] Lower navigation area
1845	// [6] Submenu
1846	//
1847	// The rectangles are used to calculate if the cursor is in the actual
1848	// navigation area (see _UpdateStateOpenSelect()).
1849
1850	if (fSelected == NULL)
1851		return;
1852
1853	BMenu* submenu = fSelected->Submenu();
1854
1855	if (submenu != NULL) {
1856		BRect menuBounds = ConvertToScreen(Bounds());
1857
1858		BRect submenuBounds;
1859		if (fSelected->Submenu()->LockLooper()) {
1860			submenuBounds = fSelected->Submenu()->ConvertToScreen(
1861				fSelected->Submenu()->Bounds());
1862			fSelected->Submenu()->UnlockLooper();
1863		}
1864
1865		if (menuBounds.left < submenuBounds.left) {
1866			navAreaRectAbove.Set(position.x + NAV_AREA_THRESHOLD,
1867				submenuBounds.top, menuBounds.right,
1868				position.y);
1869			navAreaRectBelow.Set(position.x + NAV_AREA_THRESHOLD,
1870				position.y, menuBounds.right,
1871				submenuBounds.bottom);
1872		} else {
1873			navAreaRectAbove.Set(menuBounds.left,
1874				submenuBounds.top, position.x - NAV_AREA_THRESHOLD,
1875				position.y);
1876			navAreaRectBelow.Set(menuBounds.left,
1877				position.y, position.x - NAV_AREA_THRESHOLD,
1878				submenuBounds.bottom);
1879		}
1880	} else {
1881		navAreaRectAbove = BRect();
1882		navAreaRectBelow = BRect();
1883	}
1884}
1885
1886
1887void
1888BMenu::_UpdateStateOpenSelect(BMenuItem* item, BPoint position,
1889	BRect& navAreaRectAbove, BRect& navAreaRectBelow, bigtime_t& selectedTime,
1890	bigtime_t& navigationAreaTime)
1891{
1892	if (fState == MENU_STATE_CLOSED)
1893		return;
1894
1895	if (item != fSelected) {
1896		if (navigationAreaTime == 0)
1897			navigationAreaTime = system_time();
1898
1899		position = ConvertToScreen(position);
1900
1901		bool inNavAreaRectAbove = navAreaRectAbove.Contains(position);
1902		bool inNavAreaRectBelow = navAreaRectBelow.Contains(position);
1903
1904		if (fSelected == NULL
1905			|| (!inNavAreaRectAbove && !inNavAreaRectBelow)) {
1906			_SelectItem(item, false);
1907			navAreaRectAbove = BRect();
1908			navAreaRectBelow = BRect();
1909			selectedTime = system_time();
1910			navigationAreaTime = 0;
1911			return;
1912		}
1913
1914		BRect menuBounds = ConvertToScreen(Bounds());
1915
1916		BRect submenuBounds;
1917		if (fSelected->Submenu()->LockLooper()) {
1918			fSelected->Submenu()->ConvertToScreen(
1919				fSelected->Submenu()->Bounds());
1920			fSelected->Submenu()->UnlockLooper();
1921		}
1922
1923		float xOffset;
1924
1925		// navAreaRectAbove and navAreaRectBelow have the same X
1926		// position and width, so it doesn't matter which one we use to
1927		// calculate the X offset
1928		if (menuBounds.left < submenuBounds.left)
1929			xOffset = position.x - navAreaRectAbove.left;
1930		else
1931			xOffset = navAreaRectAbove.right - position.x;
1932
1933		bool inNavArea;
1934
1935		if (inNavAreaRectAbove) {
1936			float yOffset = navAreaRectAbove.bottom - position.y;
1937			float ratio = navAreaRectAbove.Width() / navAreaRectAbove.Height();
1938
1939			inNavArea = yOffset <= xOffset / ratio;
1940		} else {
1941			float yOffset = navAreaRectBelow.bottom - position.y;
1942			float ratio = navAreaRectBelow.Width() / navAreaRectBelow.Height();
1943
1944			inNavArea = yOffset >= (navAreaRectBelow.Height() - xOffset
1945				/ ratio);
1946		}
1947
1948		bigtime_t systime = system_time();
1949
1950		if (!inNavArea || (navigationAreaTime > 0 && systime -
1951			navigationAreaTime > kNavigationAreaTimeout)) {
1952			// Don't delay opening of submenu if the user had
1953			// to wait for the navigation area timeout anyway
1954			_SelectItem(item, inNavArea);
1955
1956			if (inNavArea) {
1957				_UpdateNavigationArea(position, navAreaRectAbove,
1958					navAreaRectBelow);
1959			} else {
1960				navAreaRectAbove = BRect();
1961				navAreaRectBelow = BRect();
1962			}
1963
1964			selectedTime = system_time();
1965			navigationAreaTime = 0;
1966		}
1967	} else if (fSelected->Submenu() != NULL &&
1968		system_time() - selectedTime > kOpenSubmenuDelay) {
1969		_SelectItem(fSelected, true);
1970
1971		if (!navAreaRectAbove.IsValid() && !navAreaRectBelow.IsValid()) {
1972			position = ConvertToScreen(position);
1973			_UpdateNavigationArea(position, navAreaRectAbove,
1974				navAreaRectBelow);
1975		}
1976	}
1977
1978	if (fState != MENU_STATE_TRACKING)
1979		fState = MENU_STATE_TRACKING;
1980}
1981
1982
1983void
1984BMenu::_UpdateStateClose(BMenuItem* item, const BPoint& where,
1985	const uint32& buttons)
1986{
1987	if (fState == MENU_STATE_CLOSED)
1988		return;
1989
1990	if (buttons != 0 && _IsStickyMode()) {
1991		if (item == NULL) {
1992			if (item != fSelected && LockLooper()) {
1993				_SelectItem(item, false);
1994				UnlockLooper();
1995			}
1996			fState = MENU_STATE_CLOSED;
1997		} else
1998			_SetStickyMode(false);
1999	} else if (buttons == 0 && !_IsStickyMode()) {
2000		if (fExtraRect != NULL && fExtraRect->Contains(where)) {
2001			_SetStickyMode(true);
2002			fExtraRect = NULL;
2003				// Setting this to NULL will prevent this code
2004				// to be executed next time
2005		} else {
2006			if (item != fSelected && LockLooper()) {
2007				_SelectItem(item, false);
2008				UnlockLooper();
2009			}
2010			fState = MENU_STATE_CLOSED;
2011		}
2012	}
2013}
2014
2015
2016bool
2017BMenu::_AddItem(BMenuItem* item, int32 index)
2018{
2019	ASSERT(item != NULL);
2020	if (index < 0 || index > fItems.CountItems())
2021		return false;
2022
2023	if (item->IsMarked())
2024		_ItemMarked(item);
2025
2026	if (!fItems.AddItem(item, index))
2027		return false;
2028
2029	// install the item on the supermenu's window
2030	// or onto our window, if we are a root menu
2031	BWindow* window = NULL;
2032	if (Superitem() != NULL)
2033		window = Superitem()->fWindow;
2034	else
2035		window = Window();
2036	if (window != NULL)
2037		item->Install(window);
2038
2039	item->SetSuper(this);
2040	return true;
2041}
2042
2043
2044bool
2045BMenu::_RemoveItems(int32 index, int32 count, BMenuItem* item,
2046	bool deleteItems)
2047{
2048	bool success = false;
2049	bool invalidateLayout = false;
2050
2051	bool locked = LockLooper();
2052	BWindow* window = Window();
2053
2054	// The plan is simple: If we're given a BMenuItem directly, we use it
2055	// and ignore index and count. Otherwise, we use them instead.
2056	if (item != NULL) {
2057		if (fItems.RemoveItem(item)) {
2058			if (item == fSelected && window != NULL)
2059				_SelectItem(NULL);
2060			item->Uninstall();
2061			item->SetSuper(NULL);
2062			if (deleteItems)
2063				delete item;
2064			success = invalidateLayout = true;
2065		}
2066	} else {
2067		// We iterate backwards because it's simpler
2068		int32 i = std::min(index + count - 1, fItems.CountItems() - 1);
2069		// NOTE: the range check for "index" is done after
2070		// calculating the last index to be removed, so
2071		// that the range is not "shifted" unintentionally
2072		index = std::max((int32)0, index);
2073		for (; i >= index; i--) {
2074			item = static_cast<BMenuItem*>(fItems.ItemAt(i));
2075			if (item != NULL) {
2076				if (fItems.RemoveItem(item)) {
2077					if (item == fSelected && window != NULL)
2078						_SelectItem(NULL);
2079					item->Uninstall();
2080					item->SetSuper(NULL);
2081					if (deleteItems)
2082						delete item;
2083					success = true;
2084					invalidateLayout = true;
2085				} else {
2086					// operation not entirely successful
2087					success = false;
2088					break;
2089				}
2090			}
2091		}
2092	}
2093
2094	if (invalidateLayout) {
2095		InvalidateLayout();
2096		if (locked && window != NULL) {
2097			_LayoutItems(0);
2098			_UpdateWindowViewSize(false);
2099			Invalidate();
2100		}
2101	}
2102
2103	if (locked)
2104		UnlockLooper();
2105
2106	return success;
2107}
2108
2109
2110bool
2111BMenu::_RelayoutIfNeeded()
2112{
2113	if (!fUseCachedMenuLayout) {
2114		fUseCachedMenuLayout = true;
2115		_CacheFontInfo();
2116		_LayoutItems(0);
2117		return true;
2118	}
2119	return false;
2120}
2121
2122
2123void
2124BMenu::_LayoutItems(int32 index)
2125{
2126	_CalcTriggers();
2127
2128	float width;
2129	float height;
2130	_ComputeLayout(index, fResizeToFit, true, &width, &height);
2131
2132	if (fResizeToFit)
2133		ResizeTo(width, height);
2134}
2135
2136
2137BSize
2138BMenu::_ValidatePreferredSize()
2139{
2140	if (!fLayoutData->preferred.IsWidthSet() || ResizingMode()
2141			!= fLayoutData->lastResizingMode) {
2142		_ComputeLayout(0, true, false, NULL, NULL);
2143		ResetLayoutInvalidation();
2144	}
2145
2146	return fLayoutData->preferred;
2147}
2148
2149
2150void
2151BMenu::_ComputeLayout(int32 index, bool bestFit, bool moveItems,
2152	float* _width, float* _height)
2153{
2154	// TODO: Take "bestFit", "moveItems", "index" into account,
2155	// Recalculate only the needed items,
2156	// not the whole layout every time
2157
2158	fLayoutData->lastResizingMode = ResizingMode();
2159
2160	BRect frame;
2161	switch (fLayout) {
2162		case B_ITEMS_IN_COLUMN:
2163		{
2164			BRect parentFrame;
2165			BRect* overrideFrame = NULL;
2166			if (dynamic_cast<_BMCMenuBar_*>(Supermenu()) != NULL) {
2167				// When the menu is modified while it's open, we get here in a
2168				// situation where trying to lock the looper would deadlock
2169				// (the window is locked waiting for the menu to terminate).
2170				// In that case, just give up on getting the supermenu bounds
2171				// and keep the menu at the current width and position.
2172				if (Supermenu()->LockLooperWithTimeout(0) == B_OK) {
2173					parentFrame = Supermenu()->Bounds();
2174					Supermenu()->UnlockLooper();
2175					overrideFrame = &parentFrame;
2176				}
2177			}
2178
2179			_ComputeColumnLayout(index, bestFit, moveItems, overrideFrame,
2180				frame);
2181			break;
2182		}
2183
2184		case B_ITEMS_IN_ROW:
2185			_ComputeRowLayout(index, bestFit, moveItems, frame);
2186			break;
2187
2188		case B_ITEMS_IN_MATRIX:
2189			_ComputeMatrixLayout(frame);
2190			break;
2191	}
2192
2193	// change width depending on resize mode
2194	BSize size;
2195	if ((ResizingMode() & B_FOLLOW_LEFT_RIGHT) == B_FOLLOW_LEFT_RIGHT) {
2196		if (dynamic_cast<_BMCMenuBar_*>(this) != NULL)
2197			size.width = Bounds().Width() - fPad.right;
2198		else if (Parent() != NULL)
2199			size.width = Parent()->Frame().Width();
2200		else if (Window() != NULL)
2201			size.width = Window()->Frame().Width();
2202		else
2203			size.width = Bounds().Width();
2204	} else
2205		size.width = frame.Width();
2206
2207	size.height = frame.Height();
2208
2209	if (_width)
2210		*_width = size.width;
2211
2212	if (_height)
2213		*_height = size.height;
2214
2215	if (bestFit)
2216		fLayoutData->preferred = size;
2217
2218	if (moveItems)
2219		fUseCachedMenuLayout = true;
2220}
2221
2222
2223void
2224BMenu::_ComputeColumnLayout(int32 index, bool bestFit, bool moveItems,
2225	BRect* overrideFrame, BRect& frame)
2226{
2227	bool command = false;
2228	bool control = false;
2229	bool shift = false;
2230	bool option = false;
2231
2232	if (index > 0)
2233		frame = ItemAt(index - 1)->Frame();
2234	else if (overrideFrame != NULL)
2235		frame.Set(0, 0, overrideFrame->right, -1);
2236	else
2237		frame.Set(0, 0, 0, -1);
2238
2239	BFont font;
2240	GetFont(&font);
2241
2242	for (; index < fItems.CountItems(); index++) {
2243		BMenuItem* item = ItemAt(index);
2244
2245		float width;
2246		float height;
2247		item->GetContentSize(&width, &height);
2248
2249		if (item->fModifiers && item->fShortcutChar) {
2250			width += font.Size();
2251			if ((item->fModifiers & B_COMMAND_KEY) != 0)
2252				command = true;
2253
2254			if ((item->fModifiers & B_CONTROL_KEY) != 0)
2255				control = true;
2256
2257			if ((item->fModifiers & B_SHIFT_KEY) != 0)
2258				shift = true;
2259
2260			if ((item->fModifiers & B_OPTION_KEY) != 0)
2261				option = true;
2262		}
2263
2264		item->fBounds.left = 0.0f;
2265		item->fBounds.top = frame.bottom + 1.0f;
2266		item->fBounds.bottom = item->fBounds.top + height + fPad.top
2267			+ fPad.bottom;
2268
2269		if (item->fSubmenu != NULL)
2270			width += item->Frame().Height() / 2;
2271
2272		frame.right = std::max(frame.right, width + fPad.left + fPad.right);
2273		frame.bottom = item->fBounds.bottom;
2274	}
2275
2276	if (command) {
2277		frame.right
2278			+= BPrivate::MenuPrivate::MenuItemCommand()->Bounds().Width() + 1;
2279	}
2280	if (control) {
2281		frame.right
2282			+= BPrivate::MenuPrivate::MenuItemControl()->Bounds().Width() + 1;
2283	}
2284	if (option) {
2285		frame.right
2286			+= BPrivate::MenuPrivate::MenuItemOption()->Bounds().Width() + 1;
2287	}
2288	if (shift) {
2289		frame.right
2290			+= BPrivate::MenuPrivate::MenuItemShift()->Bounds().Width() + 1;
2291	}
2292
2293	if (fMaxContentWidth > 0)
2294		frame.right = std::min(frame.right, fMaxContentWidth);
2295
2296	if (moveItems) {
2297		for (int32 i = 0; i < fItems.CountItems(); i++)
2298			ItemAt(i)->fBounds.right = frame.right;
2299	}
2300
2301	frame.top = 0;
2302	frame.right = ceilf(frame.right);
2303}
2304
2305
2306void
2307BMenu::_ComputeRowLayout(int32 index, bool bestFit, bool moveItems,
2308	BRect& frame)
2309{
2310	font_height fh;
2311	GetFontHeight(&fh);
2312	frame.Set(0.0f, 0.0f, 0.0f, ceilf(fh.ascent + fh.descent + fPad.top
2313		+ fPad.bottom));
2314
2315	for (int32 i = 0; i < fItems.CountItems(); i++) {
2316		BMenuItem* item = ItemAt(i);
2317
2318		float width, height;
2319		item->GetContentSize(&width, &height);
2320
2321		item->fBounds.left = frame.right;
2322		item->fBounds.top = 0.0f;
2323		item->fBounds.right = item->fBounds.left + width + fPad.left
2324			+ fPad.right;
2325
2326		frame.right = item->Frame().right + 1.0f;
2327		frame.bottom = std::max(frame.bottom, height + fPad.top + fPad.bottom);
2328	}
2329
2330	if (moveItems) {
2331		for (int32 i = 0; i < fItems.CountItems(); i++)
2332			ItemAt(i)->fBounds.bottom = frame.bottom;
2333	}
2334
2335	if (bestFit)
2336		frame.right = ceilf(frame.right);
2337	else
2338		frame.right = Bounds().right;
2339}
2340
2341
2342void
2343BMenu::_ComputeMatrixLayout(BRect &frame)
2344{
2345	frame.Set(0, 0, 0, 0);
2346	for (int32 i = 0; i < CountItems(); i++) {
2347		BMenuItem* item = ItemAt(i);
2348		if (item != NULL) {
2349			frame.left = std::min(frame.left, item->Frame().left);
2350			frame.right = std::max(frame.right, item->Frame().right);
2351			frame.top = std::min(frame.top, item->Frame().top);
2352			frame.bottom = std::max(frame.bottom, item->Frame().bottom);
2353		}
2354	}
2355}
2356
2357
2358void
2359BMenu::LayoutInvalidated(bool descendants)
2360{
2361	fUseCachedMenuLayout = false;
2362	fLayoutData->preferred.Set(B_SIZE_UNSET, B_SIZE_UNSET);
2363}
2364
2365
2366// Assumes the SuperMenu to be locked (due to calling ConvertToScreen())
2367BPoint
2368BMenu::ScreenLocation()
2369{
2370	BMenu* superMenu = Supermenu();
2371	BMenuItem* superItem = Superitem();
2372
2373	if (superMenu == NULL || superItem == NULL) {
2374		debugger("BMenu can't determine where to draw."
2375			"Override BMenu::ScreenLocation() to determine location.");
2376	}
2377
2378	BPoint point;
2379	if (superMenu->Layout() == B_ITEMS_IN_COLUMN)
2380		point = superItem->Frame().RightTop() + BPoint(1.0f, 1.0f);
2381	else
2382		point = superItem->Frame().LeftBottom() + BPoint(1.0f, 1.0f);
2383
2384	superMenu->ConvertToScreen(&point);
2385
2386	return point;
2387}
2388
2389
2390BRect
2391BMenu::_CalcFrame(BPoint where, bool* scrollOn)
2392{
2393	// TODO: Improve me
2394	BRect bounds = Bounds();
2395	BRect frame = bounds.OffsetToCopy(where);
2396
2397	BScreen screen(Window());
2398	BRect screenFrame = screen.Frame();
2399
2400	BMenu* superMenu = Supermenu();
2401	BMenuItem* superItem = Superitem();
2402
2403	// Reset frame shifted state since this menu is being redrawn
2404	fExtraMenuData->frameShiftedLeft = false;
2405
2406	// TODO: Horrible hack:
2407	// When added to a BMenuField, a BPopUpMenu is the child of
2408	// a _BMCMenuBar_ to "fake" the menu hierarchy
2409	bool inMenuField = dynamic_cast<_BMCMenuBar_*>(superMenu) != NULL;
2410
2411	// Offset the menu field menu window left by the width of the checkmark
2412	// so that the text when the menu is closed lines up with the text when
2413	// the menu is open.
2414	if (inMenuField)
2415		frame.OffsetBy(-8.0f, 0.0f);
2416
2417	bool scroll = false;
2418	if (superMenu == NULL || superItem == NULL || inMenuField) {
2419		// just move the window on screen
2420		if (frame.bottom > screenFrame.bottom)
2421			frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
2422		else if (frame.top < screenFrame.top)
2423			frame.OffsetBy(0, -frame.top);
2424
2425		if (frame.right > screenFrame.right) {
2426			frame.OffsetBy(screenFrame.right - frame.right, 0);
2427			fExtraMenuData->frameShiftedLeft = true;
2428		}
2429		else if (frame.left < screenFrame.left)
2430			frame.OffsetBy(-frame.left, 0);
2431	} else if (superMenu->Layout() == B_ITEMS_IN_COLUMN) {
2432		if (frame.right > screenFrame.right
2433				|| superMenu->fExtraMenuData->frameShiftedLeft) {
2434			frame.OffsetBy(-superItem->Frame().Width() - frame.Width() - 2, 0);
2435			fExtraMenuData->frameShiftedLeft = true;
2436		}
2437
2438		if (frame.left < 0)
2439			frame.OffsetBy(-frame.left + 6, 0);
2440
2441		if (frame.bottom > screenFrame.bottom)
2442			frame.OffsetBy(0, screenFrame.bottom - frame.bottom);
2443	} else {
2444		if (frame.bottom > screenFrame.bottom) {
2445			frame.OffsetBy(0, -superItem->Frame().Height()
2446				- frame.Height() - 3);
2447		}
2448
2449		if (frame.right > screenFrame.right)
2450			frame.OffsetBy(screenFrame.right - frame.right, 0);
2451	}
2452
2453	if (!scroll) {
2454		// basically, if this returns false, it means
2455		// that the menu frame won't fit completely inside the screen
2456		// TODO: Scrolling will currently only work up/down,
2457		// not left/right
2458		scroll = screenFrame.Height() < frame.Height();
2459	}
2460
2461	if (scrollOn != NULL)
2462		*scrollOn = scroll;
2463
2464	return frame;
2465}
2466
2467
2468void
2469BMenu::DrawItems(BRect updateRect)
2470{
2471	int32 itemCount = fItems.CountItems();
2472	for (int32 i = 0; i < itemCount; i++) {
2473		BMenuItem* item = ItemAt(i);
2474		if (item->Frame().Intersects(updateRect))
2475			item->Draw();
2476	}
2477}
2478
2479
2480int
2481BMenu::_State(BMenuItem** item) const
2482{
2483	if (fState == MENU_STATE_TRACKING || fState == MENU_STATE_CLOSED)
2484		return fState;
2485
2486	if (fSelected != NULL && fSelected->Submenu() != NULL)
2487		return fSelected->Submenu()->_State(item);
2488
2489	return fState;
2490}
2491
2492
2493void
2494BMenu::_InvokeItem(BMenuItem* item, bool now)
2495{
2496	if (!item->IsEnabled())
2497		return;
2498
2499	// Do the "selected" animation
2500	// TODO: Doesn't work. This is supposed to highlight
2501	// and dehighlight the item, works on beos but not on haiku.
2502	if (!item->Submenu() && LockLooper()) {
2503		snooze(50000);
2504		item->Select(true);
2505		Window()->UpdateIfNeeded();
2506		snooze(50000);
2507		item->Select(false);
2508		Window()->UpdateIfNeeded();
2509		snooze(50000);
2510		item->Select(true);
2511		Window()->UpdateIfNeeded();
2512		snooze(50000);
2513		item->Select(false);
2514		Window()->UpdateIfNeeded();
2515		UnlockLooper();
2516	}
2517
2518	// Lock the root menu window before calling BMenuItem::Invoke()
2519	BMenu* parent = this;
2520	BMenu* rootMenu = NULL;
2521	do {
2522		rootMenu = parent;
2523		parent = rootMenu->Supermenu();
2524	} while (parent != NULL);
2525
2526	if (rootMenu->LockLooper()) {
2527		item->Invoke();
2528		rootMenu->UnlockLooper();
2529	}
2530}
2531
2532
2533bool
2534BMenu::_OverSuper(BPoint location)
2535{
2536	if (!Supermenu())
2537		return false;
2538
2539	return fSuperbounds.Contains(location);
2540}
2541
2542
2543bool
2544BMenu::_OverSubmenu(BMenuItem* item, BPoint loc)
2545{
2546	if (item == NULL)
2547		return false;
2548
2549	BMenu* subMenu = item->Submenu();
2550	if (subMenu == NULL || subMenu->Window() == NULL)
2551		return false;
2552
2553	// assume that loc is in screen coordinates
2554	if (subMenu->Window()->Frame().Contains(loc))
2555		return true;
2556
2557	return subMenu->_OverSubmenu(subMenu->fSelected, loc);
2558}
2559
2560
2561BMenuWindow*
2562BMenu::_MenuWindow()
2563{
2564#if USE_CACHED_MENUWINDOW
2565	if (fCachedMenuWindow == NULL) {
2566		char windowName[64];
2567		snprintf(windowName, 64, "%s cached menu", Name());
2568		fCachedMenuWindow = new (nothrow) BMenuWindow(windowName);
2569	}
2570#endif
2571	return fCachedMenuWindow;
2572}
2573
2574
2575void
2576BMenu::_DeleteMenuWindow()
2577{
2578	if (fCachedMenuWindow != NULL) {
2579		fCachedMenuWindow->Lock();
2580		fCachedMenuWindow->Quit();
2581		fCachedMenuWindow = NULL;
2582	}
2583}
2584
2585
2586BMenuItem*
2587BMenu::_HitTestItems(BPoint where, BPoint slop) const
2588{
2589	// TODO: Take "slop" into account ?
2590
2591	// if the point doesn't lie within the menu's
2592	// bounds, bail out immediately
2593	if (!Bounds().Contains(where))
2594		return NULL;
2595
2596	int32 itemCount = CountItems();
2597	for (int32 i = 0; i < itemCount; i++) {
2598		BMenuItem* item = ItemAt(i);
2599		if (item->Frame().Contains(where)
2600			&& dynamic_cast<BSeparatorItem*>(item) == NULL) {
2601			return item;
2602		}
2603	}
2604
2605	return NULL;
2606}
2607
2608
2609BRect
2610BMenu::_Superbounds() const
2611{
2612	return fSuperbounds;
2613}
2614
2615
2616void
2617BMenu::_CacheFontInfo()
2618{
2619	font_height fh;
2620	GetFontHeight(&fh);
2621	fAscent = fh.ascent;
2622	fDescent = fh.descent;
2623	fFontHeight = ceilf(fh.ascent + fh.descent + fh.leading);
2624}
2625
2626
2627void
2628BMenu::_ItemMarked(BMenuItem* item)
2629{
2630	if (IsRadioMode()) {
2631		for (int32 i = 0; i < CountItems(); i++) {
2632			if (ItemAt(i) != item)
2633				ItemAt(i)->SetMarked(false);
2634		}
2635	}
2636
2637	if (IsLabelFromMarked() && Superitem() != NULL)
2638		Superitem()->SetLabel(item->Label());
2639}
2640
2641
2642void
2643BMenu::_Install(BWindow* target)
2644{
2645	for (int32 i = 0; i < CountItems(); i++)
2646		ItemAt(i)->Install(target);
2647}
2648
2649
2650void
2651BMenu::_Uninstall()
2652{
2653	for (int32 i = 0; i < CountItems(); i++)
2654		ItemAt(i)->Uninstall();
2655}
2656
2657
2658void
2659BMenu::_SelectItem(BMenuItem* item, bool showSubmenu, bool selectFirstItem,
2660	bool keyDown)
2661{
2662	// Avoid deselecting and then reselecting the same item
2663	// which would cause flickering
2664	if (item != fSelected) {
2665		if (fSelected != NULL) {
2666			fSelected->Select(false);
2667			BMenu* subMenu = fSelected->Submenu();
2668			if (subMenu != NULL && subMenu->Window() != NULL)
2669				subMenu->_Hide();
2670		}
2671
2672		fSelected = item;
2673		if (fSelected != NULL)
2674			fSelected->Select(true);
2675	}
2676
2677	if (fSelected != NULL && showSubmenu) {
2678		BMenu* subMenu = fSelected->Submenu();
2679		if (subMenu != NULL && subMenu->Window() == NULL) {
2680			if (!subMenu->_Show(selectFirstItem, keyDown)) {
2681				// something went wrong, deselect the item
2682				fSelected->Select(false);
2683				fSelected = NULL;
2684			}
2685		}
2686	}
2687}
2688
2689
2690bool
2691BMenu::_SelectNextItem(BMenuItem* item, bool forward)
2692{
2693	if (CountItems() == 0) // cannot select next item in an empty menu
2694		return false;
2695
2696	BMenuItem* nextItem = _NextItem(item, forward);
2697	if (nextItem == NULL)
2698		return false;
2699
2700	_SelectItem(nextItem, dynamic_cast<BMenuBar*>(this) != NULL);
2701
2702	if (LockLooper()) {
2703		be_app->ObscureCursor();
2704		UnlockLooper();
2705	}
2706
2707	return true;
2708}
2709
2710
2711BMenuItem*
2712BMenu::_NextItem(BMenuItem* item, bool forward) const
2713{
2714	const int32 numItems = fItems.CountItems();
2715	if (numItems == 0)
2716		return NULL;
2717
2718	int32 index = fItems.IndexOf(item);
2719	int32 loopCount = numItems;
2720	while (--loopCount) {
2721		// Cycle through menu items in the given direction...
2722		if (forward)
2723			index++;
2724		else
2725			index--;
2726
2727		// ... wrap around...
2728		if (index < 0)
2729			index = numItems - 1;
2730		else if (index >= numItems)
2731			index = 0;
2732
2733		// ... and return the first suitable item found.
2734		BMenuItem* nextItem = ItemAt(index);
2735		if (nextItem->IsEnabled())
2736			return nextItem;
2737	}
2738
2739	// If no other suitable item was found, return NULL.
2740	return NULL;
2741}
2742
2743
2744void
2745BMenu::_SetStickyMode(bool sticky)
2746{
2747	if (fStickyMode == sticky)
2748		return;
2749
2750	fStickyMode = sticky;
2751
2752	if (fSuper != NULL) {
2753		// propagate the status to the super menu
2754		fSuper->_SetStickyMode(sticky);
2755	} else {
2756		// TODO: Ugly hack, but it needs to be done in this method
2757		BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this);
2758		if (sticky && menuBar != NULL && menuBar->LockLooper()) {
2759			// If we are switching to sticky mode,
2760			// steal the focus from the current focus view
2761			// (needed to handle keyboard navigation)
2762			menuBar->_StealFocus();
2763			menuBar->UnlockLooper();
2764		}
2765	}
2766}
2767
2768
2769bool
2770BMenu::_IsStickyMode() const
2771{
2772	return fStickyMode;
2773}
2774
2775
2776void
2777BMenu::_GetShiftKey(uint32 &value) const
2778{
2779	// TODO: Move into init_interface_kit().
2780	// Currently we can't do that, as get_modifier_key() blocks forever
2781	// when called on input_server initialization, since it tries
2782	// to send a synchronous message to itself (input_server is
2783	// a BApplication)
2784
2785	if (get_modifier_key(B_LEFT_SHIFT_KEY, &value) != B_OK)
2786		value = 0x4b;
2787}
2788
2789
2790void
2791BMenu::_GetControlKey(uint32 &value) const
2792{
2793	// TODO: Move into init_interface_kit().
2794	// Currently we can't do that, as get_modifier_key() blocks forever
2795	// when called on input_server initialization, since it tries
2796	// to send a synchronous message to itself (input_server is
2797	// a BApplication)
2798
2799	if (get_modifier_key(B_LEFT_CONTROL_KEY, &value) != B_OK)
2800		value = 0x5c;
2801}
2802
2803
2804void
2805BMenu::_GetCommandKey(uint32 &value) const
2806{
2807	// TODO: Move into init_interface_kit().
2808	// Currently we can't do that, as get_modifier_key() blocks forever
2809	// when called on input_server initialization, since it tries
2810	// to send a synchronous message to itself (input_server is
2811	// a BApplication)
2812
2813	if (get_modifier_key(B_LEFT_COMMAND_KEY, &value) != B_OK)
2814		value = 0x66;
2815}
2816
2817
2818void
2819BMenu::_GetOptionKey(uint32 &value) const
2820{
2821	// TODO: Move into init_interface_kit().
2822	// Currently we can't do that, as get_modifier_key() blocks forever
2823	// when called on input_server initialization, since it tries
2824	// to send a synchronous message to itself (input_server is
2825	// a BApplication)
2826
2827	if (get_modifier_key(B_LEFT_OPTION_KEY, &value) != B_OK)
2828		value = 0x5d;
2829}
2830
2831
2832void
2833BMenu::_GetMenuKey(uint32 &value) const
2834{
2835	// TODO: Move into init_interface_kit().
2836	// Currently we can't do that, as get_modifier_key() blocks forever
2837	// when called on input_server initialization, since it tries
2838	// to send a synchronous message to itself (input_server is
2839	// a BApplication)
2840
2841	if (get_modifier_key(B_MENU_KEY, &value) != B_OK)
2842		value = 0x68;
2843}
2844
2845
2846void
2847BMenu::_CalcTriggers()
2848{
2849	BPrivate::TriggerList triggerList;
2850
2851	// Gathers the existing triggers set by the user
2852	for (int32 i = 0; i < CountItems(); i++) {
2853		char trigger = ItemAt(i)->Trigger();
2854		if (trigger != 0)
2855			triggerList.AddTrigger(trigger);
2856	}
2857
2858	// Set triggers for items which don't have one yet
2859	for (int32 i = 0; i < CountItems(); i++) {
2860		BMenuItem* item = ItemAt(i);
2861		if (item->Trigger() == 0) {
2862			uint32 trigger;
2863			int32 index;
2864			if (_ChooseTrigger(item->Label(), index, trigger, triggerList))
2865				item->SetAutomaticTrigger(index, trigger);
2866		}
2867	}
2868}
2869
2870
2871bool
2872BMenu::_ChooseTrigger(const char* title, int32& index, uint32& trigger,
2873	BPrivate::TriggerList& triggers)
2874{
2875	if (title == NULL)
2876		return false;
2877
2878	index = 0;
2879	uint32 c;
2880	const char* nextCharacter, *character;
2881
2882	// two runs: first we look out for alphanumeric ASCII characters
2883	nextCharacter = title;
2884	character = nextCharacter;
2885	while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
2886		if (!(c < 128 && BUnicodeChar::IsAlNum(c)) || triggers.HasTrigger(c)) {
2887			character = nextCharacter;
2888			continue;
2889		}
2890		trigger = BUnicodeChar::ToLower(c);
2891		index = (int32)(character - title);
2892		return triggers.AddTrigger(c);
2893	}
2894
2895	// then, if we still haven't found something, we accept anything
2896	nextCharacter = title;
2897	character = nextCharacter;
2898	while ((c = BUnicodeChar::FromUTF8(&nextCharacter)) != 0) {
2899		if (BUnicodeChar::IsSpace(c) || triggers.HasTrigger(c)) {
2900			character = nextCharacter;
2901			continue;
2902		}
2903		trigger = BUnicodeChar::ToLower(c);
2904		index = (int32)(character - title);
2905		return triggers.AddTrigger(c);
2906	}
2907
2908	return false;
2909}
2910
2911
2912void
2913BMenu::_UpdateWindowViewSize(const bool &move)
2914{
2915	BMenuWindow* window = static_cast<BMenuWindow*>(Window());
2916	if (window == NULL)
2917		return;
2918
2919	if (dynamic_cast<BMenuBar*>(this) != NULL)
2920		return;
2921
2922	if (!fResizeToFit)
2923		return;
2924
2925	bool scroll = false;
2926	const BPoint screenLocation = move ? ScreenLocation()
2927		: window->Frame().LeftTop();
2928	BRect frame = _CalcFrame(screenLocation, &scroll);
2929	ResizeTo(frame.Width(), frame.Height());
2930
2931	if (fItems.CountItems() > 0) {
2932		if (!scroll) {
2933			window->ResizeTo(Bounds().Width(), Bounds().Height());
2934		} else {
2935			BScreen screen(window);
2936
2937			// Only scroll on menus not attached to a menubar, or when the
2938			// menu frame is above the visible screen
2939			if (dynamic_cast<BMenuBar*>(Supermenu()) == NULL || frame.top < 0) {
2940
2941				// If we need scrolling, resize the window to fit the screen and
2942				// attach scrollers to our cached BMenuWindow.
2943				window->ResizeTo(Bounds().Width(), screen.Frame().Height());
2944				frame.top = 0;
2945
2946				// we currently only support scrolling for B_ITEMS_IN_COLUMN
2947				if (fLayout == B_ITEMS_IN_COLUMN) {
2948					window->AttachScrollers();
2949
2950					BMenuItem* selectedItem = FindMarked();
2951					if (selectedItem != NULL) {
2952						// scroll to the selected item
2953						if (Supermenu() == NULL) {
2954							window->TryScrollTo(selectedItem->Frame().top);
2955						} else {
2956							BPoint point = selectedItem->Frame().LeftTop();
2957							BPoint superPoint = Superitem()->Frame().LeftTop();
2958							Supermenu()->ConvertToScreen(&superPoint);
2959							ConvertToScreen(&point);
2960							window->TryScrollTo(point.y - superPoint.y);
2961						}
2962					}
2963				}
2964			}
2965		}
2966	} else {
2967		_CacheFontInfo();
2968		window->ResizeTo(StringWidth(BPrivate::kEmptyMenuLabel)
2969				+ fPad.left + fPad.right,
2970			fFontHeight + fPad.top + fPad.bottom);
2971	}
2972
2973	if (move)
2974		window->MoveTo(frame.LeftTop());
2975}
2976
2977
2978bool
2979BMenu::_AddDynamicItems(bool keyDown)
2980{
2981	bool addAborted = false;
2982	if (AddDynamicItem(B_INITIAL_ADD)) {
2983		BMenuItem* superItem = Superitem();
2984		BMenu* superMenu = Supermenu();
2985		do {
2986			if (superMenu != NULL
2987				&& !superMenu->_OkToProceed(superItem, keyDown)) {
2988				AddDynamicItem(B_ABORT);
2989				addAborted = true;
2990				break;
2991			}
2992		} while (AddDynamicItem(B_PROCESSING));
2993	}
2994
2995	return addAborted;
2996}
2997
2998
2999bool
3000BMenu::_OkToProceed(BMenuItem* item, bool keyDown)
3001{
3002	BPoint where;
3003	uint32 buttons;
3004	GetMouse(&where, &buttons, false);
3005	bool stickyMode = _IsStickyMode();
3006	// Quit if user clicks the mouse button in sticky mode
3007	// or releases the mouse button in nonsticky mode
3008	// or moves the pointer over another item
3009	// TODO: I added the check for BMenuBar to solve a problem with Deskbar.
3010	// BeOS seems to do something similar. This could also be a bug in
3011	// Deskbar, though.
3012	if ((buttons != 0 && stickyMode)
3013		|| ((dynamic_cast<BMenuBar*>(this) == NULL
3014			&& (buttons == 0 && !stickyMode))
3015		|| ((_HitTestItems(where) != item) && !keyDown))) {
3016		return false;
3017	}
3018
3019	return true;
3020}
3021
3022
3023bool
3024BMenu::_CustomTrackingWantsToQuit()
3025{
3026	if (fExtraMenuData != NULL && fExtraMenuData->trackingHook != NULL
3027		&& fExtraMenuData->trackingState != NULL) {
3028		return fExtraMenuData->trackingHook(this,
3029			fExtraMenuData->trackingState);
3030	}
3031
3032	return false;
3033}
3034
3035
3036void
3037BMenu::_QuitTracking(bool onlyThis)
3038{
3039	_SelectItem(NULL);
3040	if (BMenuBar* menuBar = dynamic_cast<BMenuBar*>(this))
3041		menuBar->_RestoreFocus();
3042
3043	fState = MENU_STATE_CLOSED;
3044
3045	if (!onlyThis) {
3046		// Close the whole menu hierarchy
3047		if (Supermenu() != NULL)
3048			Supermenu()->fState = MENU_STATE_CLOSED;
3049
3050		if (_IsStickyMode())
3051			_SetStickyMode(false);
3052
3053		if (LockLooper()) {
3054			be_app->ShowCursor();
3055			UnlockLooper();
3056		}
3057	}
3058
3059	_Hide();
3060}
3061
3062
3063//	#pragma mark - menu_info functions
3064
3065
3066// TODO: Maybe the following two methods would fit better into
3067// InterfaceDefs.cpp
3068// In R5, they do all the work client side, we let the app_server handle the
3069// details.
3070status_t
3071set_menu_info(menu_info* info)
3072{
3073	if (!info)
3074		return B_BAD_VALUE;
3075
3076	BPrivate::AppServerLink link;
3077	link.StartMessage(AS_SET_MENU_INFO);
3078	link.Attach<menu_info>(*info);
3079
3080	status_t status = B_ERROR;
3081	if (link.FlushWithReply(status) == B_OK && status == B_OK)
3082		BMenu::sMenuInfo = *info;
3083		// Update also the local copy, in case anyone relies on it
3084
3085	return status;
3086}
3087
3088
3089status_t
3090get_menu_info(menu_info* info)
3091{
3092	if (!info)
3093		return B_BAD_VALUE;
3094
3095	BPrivate::AppServerLink link;
3096	link.StartMessage(AS_GET_MENU_INFO);
3097
3098	status_t status = B_ERROR;
3099	if (link.FlushWithReply(status) == B_OK && status == B_OK)
3100		link.Read<menu_info>(info);
3101
3102	return status;
3103}
3104
3105
3106extern "C" void
3107B_IF_GCC_2(InvalidateLayout__5BMenub,_ZN5BMenu16InvalidateLayoutEb)(
3108	BMenu* menu, bool descendants)
3109{
3110	menu->InvalidateLayout();
3111}
3112