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 (burton666@libero.it)
8 *		Stephan Aßmus <superstippi@gmx.de>
9 */
10
11
12#include <MenuBar.h>
13
14#include <math.h>
15
16#include <Application.h>
17#include <Autolock.h>
18#include <ControlLook.h>
19#include <LayoutUtils.h>
20#include <MenuItem.h>
21#include <Window.h>
22
23#include <AppMisc.h>
24#include <binary_compatibility/Interface.h>
25#include <MenuPrivate.h>
26#include <TokenSpace.h>
27#include <InterfaceDefs.h>
28
29#include "BMCPrivate.h"
30
31
32using BPrivate::gDefaultTokens;
33
34
35struct menubar_data {
36	BMenuBar*	menuBar;
37	int32		menuIndex;
38
39	bool		sticky;
40	bool		showMenu;
41
42	bool		useRect;
43	BRect		rect;
44};
45
46
47BMenuBar::BMenuBar(BRect frame, const char* title, uint32 resizeMask,
48		menu_layout layout, bool resizeToFit)
49	:
50	BMenu(frame, title, resizeMask, B_WILL_DRAW | B_FRAME_EVENTS
51		| B_FULL_UPDATE_ON_RESIZE, layout, resizeToFit),
52	fBorder(B_BORDER_FRAME),
53	fTrackingPID(-1),
54	fPrevFocusToken(-1),
55	fMenuSem(-1),
56	fLastBounds(NULL),
57	fTracking(false)
58{
59	_InitData(layout);
60}
61
62
63BMenuBar::BMenuBar(const char* title, menu_layout layout, uint32 flags)
64	:
65	BMenu(BRect(), title, B_FOLLOW_NONE,
66		flags | B_WILL_DRAW | B_FRAME_EVENTS | B_SUPPORTS_LAYOUT,
67		layout, false),
68	fBorder(B_BORDER_FRAME),
69	fTrackingPID(-1),
70	fPrevFocusToken(-1),
71	fMenuSem(-1),
72	fLastBounds(NULL),
73	fTracking(false)
74{
75	_InitData(layout);
76}
77
78
79BMenuBar::BMenuBar(BMessage* data)
80	:
81	BMenu(data),
82	fBorder(B_BORDER_FRAME),
83	fTrackingPID(-1),
84	fPrevFocusToken(-1),
85	fMenuSem(-1),
86	fLastBounds(NULL),
87	fTracking(false)
88{
89	int32 border;
90
91	if (data->FindInt32("_border", &border) == B_OK)
92		SetBorder((menu_bar_border)border);
93
94	menu_layout layout = B_ITEMS_IN_COLUMN;
95	data->FindInt32("_layout", (int32*)&layout);
96
97	_InitData(layout);
98}
99
100
101BMenuBar::~BMenuBar()
102{
103	if (fTracking) {
104		status_t dummy;
105		wait_for_thread(fTrackingPID, &dummy);
106	}
107
108	delete fLastBounds;
109}
110
111
112BArchivable*
113BMenuBar::Instantiate(BMessage* data)
114{
115	if (validate_instantiation(data, "BMenuBar"))
116		return new BMenuBar(data);
117
118	return NULL;
119}
120
121
122status_t
123BMenuBar::Archive(BMessage* data, bool deep) const
124{
125	status_t err = BMenu::Archive(data, deep);
126
127	if (err < B_OK)
128		return err;
129
130	if (Border() != B_BORDER_FRAME)
131		err = data->AddInt32("_border", Border());
132
133	return err;
134}
135
136
137// #pragma mark -
138
139
140void
141BMenuBar::AttachedToWindow()
142{
143	_Install(Window());
144	Window()->SetKeyMenuBar(this);
145
146	BMenu::AttachedToWindow();
147
148	*fLastBounds = Bounds();
149}
150
151
152void
153BMenuBar::DetachedFromWindow()
154{
155	BMenu::DetachedFromWindow();
156}
157
158
159void
160BMenuBar::AllAttached()
161{
162	BMenu::AllAttached();
163}
164
165
166void
167BMenuBar::AllDetached()
168{
169	BMenu::AllDetached();
170}
171
172
173void
174BMenuBar::WindowActivated(bool state)
175{
176	BView::WindowActivated(state);
177}
178
179
180void
181BMenuBar::MakeFocus(bool state)
182{
183	BMenu::MakeFocus(state);
184}
185
186
187// #pragma mark -
188
189
190void
191BMenuBar::ResizeToPreferred()
192{
193	BMenu::ResizeToPreferred();
194}
195
196
197void
198BMenuBar::GetPreferredSize(float* width, float* height)
199{
200	BMenu::GetPreferredSize(width, height);
201}
202
203
204BSize
205BMenuBar::MinSize()
206{
207	return BMenu::MinSize();
208}
209
210
211BSize
212BMenuBar::MaxSize()
213{
214	BSize size = BMenu::MaxSize();
215	return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
216		BSize(B_SIZE_UNLIMITED, size.height));
217}
218
219
220BSize
221BMenuBar::PreferredSize()
222{
223	return BMenu::PreferredSize();
224}
225
226
227void
228BMenuBar::FrameMoved(BPoint newPosition)
229{
230	BMenu::FrameMoved(newPosition);
231}
232
233
234void
235BMenuBar::FrameResized(float newWidth, float newHeight)
236{
237	// invalidate right border
238	if (newWidth != fLastBounds->Width()) {
239		BRect rect(min_c(fLastBounds->right, newWidth), 0,
240			max_c(fLastBounds->right, newWidth), newHeight);
241		Invalidate(rect);
242	}
243
244	// invalidate bottom border
245	if (newHeight != fLastBounds->Height()) {
246		BRect rect(0, min_c(fLastBounds->bottom, newHeight) - 1,
247			newWidth, max_c(fLastBounds->bottom, newHeight));
248		Invalidate(rect);
249	}
250
251	fLastBounds->Set(0, 0, newWidth, newHeight);
252
253	BMenu::FrameResized(newWidth, newHeight);
254}
255
256
257// #pragma mark -
258
259
260void
261BMenuBar::Show()
262{
263	BView::Show();
264}
265
266
267void
268BMenuBar::Hide()
269{
270	BView::Hide();
271}
272
273
274void
275BMenuBar::Draw(BRect updateRect)
276{
277	if (_RelayoutIfNeeded()) {
278		Invalidate();
279		return;
280	}
281
282	if (be_control_look != NULL) {
283		BRect rect(Bounds());
284		rgb_color base = LowColor();
285		uint32 flags = 0;
286
287		be_control_look->DrawBorder(this, rect, updateRect, base,
288			B_PLAIN_BORDER, flags, BControlLook::B_BOTTOM_BORDER);
289
290		be_control_look->DrawMenuBarBackground(this, rect, updateRect, base);
291
292		_DrawItems(updateRect);
293		return;
294	}
295
296	// TODO: implement additional border styles
297	rgb_color color = HighColor();
298
299	BRect bounds(Bounds());
300	// Restore the background of the previously selected menuitem
301	DrawBackground(bounds & updateRect);
302
303	rgb_color noTint = LowColor();
304
305	SetHighColor(tint_color(noTint, B_LIGHTEN_2_TINT));
306	StrokeLine(BPoint(0.0f, bounds.bottom - 2.0f), BPoint(0.0f, 0.0f));
307	StrokeLine(BPoint(bounds.right, 0.0f));
308
309	SetHighColor(tint_color(noTint, B_DARKEN_1_TINT));
310	StrokeLine(BPoint(1.0f, bounds.bottom - 1.0f),
311		BPoint(bounds.right, bounds.bottom - 1.0f));
312
313	SetHighColor(tint_color(noTint, B_DARKEN_2_TINT));
314	StrokeLine(BPoint(0.0f, bounds.bottom),
315		BPoint(bounds.right, bounds.bottom));
316	StrokeLine(BPoint(bounds.right, 0.0f), BPoint(bounds.right, bounds.bottom));
317
318	SetHighColor(color);
319		// revert to previous used color (cheap PushState()/PopState())
320
321	_DrawItems(updateRect);
322}
323
324
325// #pragma mark -
326
327
328void
329BMenuBar::MessageReceived(BMessage* msg)
330{
331	BMenu::MessageReceived(msg);
332}
333
334
335void
336BMenuBar::MouseDown(BPoint where)
337{
338	if (fTracking)
339		return;
340
341	uint32 buttons;
342	GetMouse(&where, &buttons);
343
344  	BWindow* window = Window();
345  	if (!window->IsActive() || !window->IsFront()) {
346		if ((mouse_mode() == B_FOCUS_FOLLOWS_MOUSE)
347			|| ((mouse_mode() == B_CLICK_TO_FOCUS_MOUSE)
348				&& ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0))) {
349			window->Activate();
350			window->UpdateIfNeeded();
351		}
352	}
353
354	StartMenuBar(-1, false, false);
355}
356
357
358void
359BMenuBar::MouseUp(BPoint where)
360{
361	BView::MouseUp(where);
362}
363
364
365// #pragma mark -
366
367
368BHandler*
369BMenuBar::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
370	int32 form, const char* property)
371{
372	return BMenu::ResolveSpecifier(msg, index, specifier, form, property);
373}
374
375
376status_t
377BMenuBar::GetSupportedSuites(BMessage* data)
378{
379	return BMenu::GetSupportedSuites(data);
380}
381
382
383// #pragma mark -
384
385
386void
387BMenuBar::SetBorder(menu_bar_border border)
388{
389	fBorder = border;
390}
391
392
393menu_bar_border
394BMenuBar::Border() const
395{
396	return fBorder;
397}
398
399
400// #pragma mark -
401
402
403status_t
404BMenuBar::Perform(perform_code code, void* _data)
405{
406	switch (code) {
407		case PERFORM_CODE_MIN_SIZE:
408			((perform_data_min_size*)_data)->return_value
409				= BMenuBar::MinSize();
410			return B_OK;
411		case PERFORM_CODE_MAX_SIZE:
412			((perform_data_max_size*)_data)->return_value
413				= BMenuBar::MaxSize();
414			return B_OK;
415		case PERFORM_CODE_PREFERRED_SIZE:
416			((perform_data_preferred_size*)_data)->return_value
417				= BMenuBar::PreferredSize();
418			return B_OK;
419		case PERFORM_CODE_LAYOUT_ALIGNMENT:
420			((perform_data_layout_alignment*)_data)->return_value
421				= BMenuBar::LayoutAlignment();
422			return B_OK;
423		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
424			((perform_data_has_height_for_width*)_data)->return_value
425				= BMenuBar::HasHeightForWidth();
426			return B_OK;
427		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
428		{
429			perform_data_get_height_for_width* data
430				= (perform_data_get_height_for_width*)_data;
431			BMenuBar::GetHeightForWidth(data->width, &data->min, &data->max,
432				&data->preferred);
433			return B_OK;
434		}
435		case PERFORM_CODE_SET_LAYOUT:
436		{
437			perform_data_set_layout* data = (perform_data_set_layout*)_data;
438			BMenuBar::SetLayout(data->layout);
439			return B_OK;
440		}
441		case PERFORM_CODE_LAYOUT_INVALIDATED:
442		{
443			perform_data_layout_invalidated* data
444				= (perform_data_layout_invalidated*)_data;
445			BMenuBar::LayoutInvalidated(data->descendants);
446			return B_OK;
447		}
448		case PERFORM_CODE_DO_LAYOUT:
449		{
450			BMenuBar::DoLayout();
451			return B_OK;
452		}
453	}
454
455	return BMenu::Perform(code, _data);
456}
457
458
459// #pragma mark -
460
461
462void BMenuBar::_ReservedMenuBar1() {}
463void BMenuBar::_ReservedMenuBar2() {}
464void BMenuBar::_ReservedMenuBar3() {}
465void BMenuBar::_ReservedMenuBar4() {}
466
467
468BMenuBar &
469BMenuBar::operator=(const BMenuBar &)
470{
471	return *this;
472}
473
474
475// #pragma mark -
476
477
478void
479BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu,
480	BRect* specialRect)
481{
482	if (fTracking)
483		return;
484
485	BWindow* window = Window();
486	if (window == NULL)
487		debugger("MenuBar must be added to a window before it can be used.");
488
489	BAutolock lock(window);
490	if (!lock.IsLocked())
491		return;
492
493	fPrevFocusToken = -1;
494	fTracking = true;
495
496	// We are called from the window's thread,
497	// so let's call MenusBeginning() directly
498	window->MenusBeginning();
499
500	fMenuSem = create_sem(0, "window close sem");
501	_set_menu_sem_(window, fMenuSem);
502
503	fTrackingPID = spawn_thread(_TrackTask, "menu_tracking", B_DISPLAY_PRIORITY,
504		NULL);
505	if (fTrackingPID >= 0) {
506		menubar_data data;
507		data.menuBar = this;
508		data.menuIndex = menuIndex;
509		data.sticky = sticky;
510		data.showMenu = showMenu;
511		data.useRect = specialRect != NULL;
512		if (data.useRect)
513			data.rect = *specialRect;
514
515		resume_thread(fTrackingPID);
516		send_data(fTrackingPID, 0, &data, sizeof(data));
517	} else {
518		fTracking = false;
519		_set_menu_sem_(window, B_NO_MORE_SEMS);
520		delete_sem(fMenuSem);
521	}
522}
523
524
525/*static*/ int32
526BMenuBar::_TrackTask(void* arg)
527{
528	menubar_data data;
529	thread_id id;
530	receive_data(&id, &data, sizeof(data));
531
532	BMenuBar* menuBar = data.menuBar;
533	if (data.useRect)
534		menuBar->fExtraRect = &data.rect;
535	menuBar->_SetStickyMode(data.sticky);
536
537	int32 action;
538	menuBar->_Track(&action, data.menuIndex, data.showMenu);
539
540	menuBar->fTracking = false;
541	menuBar->fExtraRect = NULL;
542
543	// We aren't the BWindow thread, so don't call MenusEnded() directly
544	BWindow* window = menuBar->Window();
545	window->PostMessage(_MENUS_DONE_);
546
547	_set_menu_sem_(window, B_BAD_SEM_ID);
548	delete_sem(menuBar->fMenuSem);
549	menuBar->fMenuSem = B_BAD_SEM_ID;
550
551	return 0;
552}
553
554
555BMenuItem*
556BMenuBar::_Track(int32* action, int32 startIndex, bool showMenu)
557{
558	// TODO: Cleanup, merge some "if" blocks if possible
559	fChosenItem = NULL;
560
561	BWindow* window = Window();
562	fState = MENU_STATE_TRACKING;
563
564	BPoint where;
565	uint32 buttons;
566	if (window->Lock()) {
567		if (startIndex != -1) {
568			be_app->ObscureCursor();
569			_SelectItem(ItemAt(startIndex), true, false);
570		}
571		GetMouse(&where, &buttons);
572		window->Unlock();
573	}
574
575	while (fState != MENU_STATE_CLOSED) {
576		bigtime_t snoozeAmount = 40000;
577		if (Window() == NULL || !window->Lock())
578			break;
579
580		BMenuItem* menuItem = NULL;
581		if (dynamic_cast<_BMCMenuBar_*>(this))
582			menuItem = ItemAt(0);
583		else
584			menuItem = _HitTestItems(where, B_ORIGIN);
585		if (_OverSubmenu(fSelected, ConvertToScreen(where))
586			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
587			// call _Track() from the selected sub-menu when the mouse cursor
588			// is over its window
589			BMenu* menu = fSelected->Submenu();
590			window->Unlock();
591			snoozeAmount = 30000;
592			bool wasSticky = _IsStickyMode();
593			menu->_SetStickyMode(wasSticky);
594			int localAction;
595			fChosenItem = menu->_Track(&localAction);
596
597			// The mouse could have meen moved since the last time we
598			// checked its position, or buttons might have been pressed.
599			// Unfortunately our child menus don't tell
600			// us the new position.
601			// TODO: Maybe have a shared struct between all menus
602			// where to store the current mouse position ?
603			// (Or just use the BView mouse hooks)
604			BPoint newWhere;
605			if (window->Lock()) {
606				GetMouse(&newWhere, &buttons);
607				window->Unlock();
608			}
609
610			// This code is needed to make menus
611			// that are children of BMenuFields "sticky" (see ticket #953)
612			if (localAction == MENU_STATE_CLOSED) {
613				if (fExtraRect != NULL && fExtraRect->Contains(where)
614					// 9 = 3 pixels ^ 2 (since point_distance() returns the
615					// square of the distance)
616					&& point_distance(newWhere, where) < 9) {
617					_SetStickyMode(true);
618					fExtraRect = NULL;
619				} else
620					fState = MENU_STATE_CLOSED;
621			}
622			if (!window->Lock())
623				break;
624		} else if (menuItem != NULL) {
625			if (menuItem->Submenu() != NULL && menuItem != fSelected) {
626				if (menuItem->Submenu()->Window() == NULL) {
627					// open the menu if it's not opened yet
628					_SelectItem(menuItem);
629				} else {
630					// Menu was already opened, close it and bail
631					_SelectItem(NULL);
632					fState = MENU_STATE_CLOSED;
633					fChosenItem = NULL;
634				}
635			} else {
636				// No submenu, just select the item
637				_SelectItem(menuItem);
638			}
639		} else if (menuItem == NULL && fSelected != NULL
640			&& !_IsStickyMode() && Bounds().Contains(where)) {
641			_SelectItem(NULL);
642			fState = MENU_STATE_TRACKING;
643		}
644
645		window->Unlock();
646
647		if (fState != MENU_STATE_CLOSED) {
648			// If user doesn't move the mouse, loop here,
649			// so we don't interfere with keyboard menu navigation
650			BPoint newLocation = where;
651			uint32 newButtons = buttons;
652			do {
653				snooze(snoozeAmount);
654				if (!LockLooper())
655					break;
656				GetMouse(&newLocation, &newButtons, true);
657				UnlockLooper();
658			} while (newLocation == where && newButtons == buttons
659				&& fState == MENU_STATE_TRACKING);
660
661			where = newLocation;
662			buttons = newButtons;
663
664			if (buttons != 0 && _IsStickyMode()) {
665				if (menuItem == NULL
666					|| (menuItem->Submenu() && menuItem->Submenu()->Window())) {
667					// clicked outside menu bar or on item with already
668					// open sub menu
669					fState = MENU_STATE_CLOSED;
670				} else
671					_SetStickyMode(false);
672			} else if (buttons == 0 && !_IsStickyMode()) {
673				if ((fSelected != NULL && fSelected->Submenu() == NULL)
674					|| menuItem == NULL) {
675					fChosenItem = fSelected;
676					fState = MENU_STATE_CLOSED;
677				} else
678					_SetStickyMode(true);
679			}
680		}
681	}
682
683	if (window->Lock()) {
684		if (fSelected != NULL)
685			_SelectItem(NULL);
686
687		if (fChosenItem != NULL)
688			fChosenItem->Invoke();
689		_RestoreFocus();
690		window->Unlock();
691	}
692
693	if (_IsStickyMode())
694		_SetStickyMode(false);
695
696	_DeleteMenuWindow();
697
698	if (action != NULL)
699		*action = fState;
700
701	return fChosenItem;
702
703}
704
705
706void
707BMenuBar::_StealFocus()
708{
709	// We already stole the focus, don't do anything
710	if (fPrevFocusToken != -1)
711		return;
712
713	BWindow* window = Window();
714	if (window != NULL && window->Lock()) {
715		BView* focus = window->CurrentFocus();
716		if (focus != NULL && focus != this)
717			fPrevFocusToken = _get_object_token_(focus);
718		MakeFocus();
719		window->Unlock();
720	}
721}
722
723
724void
725BMenuBar::_RestoreFocus()
726{
727	BWindow* window = Window();
728	if (window != NULL && window->Lock()) {
729		BHandler* handler = NULL;
730		if (fPrevFocusToken != -1
731			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN,
732				(void**)&handler) == B_OK) {
733			BView* view = dynamic_cast<BView*>(handler);
734			if (view != NULL && view->Window() == window)
735				view->MakeFocus();
736
737		} else if (IsFocus())
738			MakeFocus(false);
739
740		fPrevFocusToken = -1;
741		window->Unlock();
742	}
743}
744
745
746void
747BMenuBar::_InitData(menu_layout layout)
748{
749	fLastBounds = new BRect(Bounds());
750	SetItemMargins(8, 2, 8, 2);
751	_SetIgnoreHidden(true);
752}
753