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