1/*
2 * Copyright 2001-2015, 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* name, uint32 resizingMode,
48		menu_layout layout, bool resizeToFit)
49	:
50	BMenu(frame, name, resizingMode, 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* name, menu_layout layout, uint32 flags)
64	:
65	BMenu(BRect(), name, 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* archive)
80	:
81	BMenu(archive),
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 (archive->FindInt32("_border", &border) == B_OK)
92		SetBorder((menu_bar_border)border);
93
94	menu_layout layout = B_ITEMS_IN_COLUMN;
95	archive->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	BRect rect(Bounds());
283	rgb_color base = LowColor();
284	uint32 flags = 0;
285
286	be_control_look->DrawBorder(this, rect, updateRect, base,
287		B_PLAIN_BORDER, flags, BControlLook::B_BOTTOM_BORDER);
288
289	be_control_look->DrawMenuBarBackground(this, rect, updateRect, base,
290		0, fBorders);
291
292	DrawItems(updateRect);
293}
294
295
296// #pragma mark -
297
298
299void
300BMenuBar::MessageReceived(BMessage* message)
301{
302	BMenu::MessageReceived(message);
303}
304
305
306void
307BMenuBar::MouseDown(BPoint where)
308{
309	if (fTracking)
310		return;
311
312	uint32 buttons;
313	GetMouse(&where, &buttons);
314
315	BWindow* window = Window();
316	if (!window->IsActive() || !window->IsFront()) {
317		if ((mouse_mode() == B_FOCUS_FOLLOWS_MOUSE)
318			|| ((mouse_mode() == B_CLICK_TO_FOCUS_MOUSE)
319				&& ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0))) {
320			// right-click to bring-to-front and send-to-back
321			// (might cause some regressions in FFM)
322			window->Activate();
323			window->UpdateIfNeeded();
324		}
325	}
326
327	StartMenuBar(-1, false, false);
328}
329
330
331void
332BMenuBar::MouseUp(BPoint where)
333{
334	BView::MouseUp(where);
335}
336
337
338// #pragma mark -
339
340
341BHandler*
342BMenuBar::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
343	int32 form, const char* property)
344{
345	return BMenu::ResolveSpecifier(msg, index, specifier, form, property);
346}
347
348
349status_t
350BMenuBar::GetSupportedSuites(BMessage* data)
351{
352	return BMenu::GetSupportedSuites(data);
353}
354
355
356// #pragma mark -
357
358
359void
360BMenuBar::SetBorder(menu_bar_border border)
361{
362	fBorder = border;
363}
364
365
366menu_bar_border
367BMenuBar::Border() const
368{
369	return fBorder;
370}
371
372
373void
374BMenuBar::SetBorders(uint32 borders)
375{
376	fBorders = borders;
377}
378
379
380uint32
381BMenuBar::Borders() const
382{
383	return fBorders;
384}
385
386
387// #pragma mark -
388
389
390status_t
391BMenuBar::Perform(perform_code code, void* _data)
392{
393	switch (code) {
394		case PERFORM_CODE_MIN_SIZE:
395			((perform_data_min_size*)_data)->return_value
396				= BMenuBar::MinSize();
397			return B_OK;
398
399		case PERFORM_CODE_MAX_SIZE:
400			((perform_data_max_size*)_data)->return_value
401				= BMenuBar::MaxSize();
402			return B_OK;
403
404		case PERFORM_CODE_PREFERRED_SIZE:
405			((perform_data_preferred_size*)_data)->return_value
406				= BMenuBar::PreferredSize();
407			return B_OK;
408
409		case PERFORM_CODE_LAYOUT_ALIGNMENT:
410			((perform_data_layout_alignment*)_data)->return_value
411				= BMenuBar::LayoutAlignment();
412			return B_OK;
413
414		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
415			((perform_data_has_height_for_width*)_data)->return_value
416				= BMenuBar::HasHeightForWidth();
417			return B_OK;
418
419		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
420		{
421			perform_data_get_height_for_width* data
422				= (perform_data_get_height_for_width*)_data;
423			BMenuBar::GetHeightForWidth(data->width, &data->min, &data->max,
424				&data->preferred);
425			return B_OK;
426		}
427
428		case PERFORM_CODE_SET_LAYOUT:
429		{
430			perform_data_set_layout* data = (perform_data_set_layout*)_data;
431			BMenuBar::SetLayout(data->layout);
432			return B_OK;
433		}
434
435		case PERFORM_CODE_LAYOUT_INVALIDATED:
436		{
437			perform_data_layout_invalidated* data
438				= (perform_data_layout_invalidated*)_data;
439			BMenuBar::LayoutInvalidated(data->descendants);
440			return B_OK;
441		}
442
443		case PERFORM_CODE_DO_LAYOUT:
444		{
445			BMenuBar::DoLayout();
446			return B_OK;
447		}
448	}
449
450	return BMenu::Perform(code, _data);
451}
452
453
454// #pragma mark -
455
456
457void BMenuBar::_ReservedMenuBar1() {}
458void BMenuBar::_ReservedMenuBar2() {}
459void BMenuBar::_ReservedMenuBar3() {}
460void BMenuBar::_ReservedMenuBar4() {}
461
462
463BMenuBar&
464BMenuBar::operator=(const BMenuBar &)
465{
466	return *this;
467}
468
469
470// #pragma mark -
471
472
473void
474BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu,
475	BRect* specialRect)
476{
477	if (fTracking)
478		return;
479
480	BWindow* window = Window();
481	if (window == NULL)
482		debugger("MenuBar must be added to a window before it can be used.");
483
484	BAutolock lock(window);
485	if (!lock.IsLocked())
486		return;
487
488	fPrevFocusToken = -1;
489	fTracking = true;
490
491	// We are called from the window's thread,
492	// so let's call MenusBeginning() directly
493	window->MenusBeginning();
494
495	fMenuSem = create_sem(0, "window close sem");
496	_set_menu_sem_(window, fMenuSem);
497
498	fTrackingPID = spawn_thread(_TrackTask, "menu_tracking",
499		B_DISPLAY_PRIORITY, NULL);
500	if (fTrackingPID >= 0) {
501		menubar_data data;
502		data.menuBar = this;
503		data.menuIndex = menuIndex;
504		data.sticky = sticky;
505		data.showMenu = showMenu;
506		data.useRect = specialRect != NULL;
507		if (data.useRect)
508			data.rect = *specialRect;
509
510		resume_thread(fTrackingPID);
511		send_data(fTrackingPID, 0, &data, sizeof(data));
512	} else {
513		fTracking = false;
514		_set_menu_sem_(window, B_NO_MORE_SEMS);
515		delete_sem(fMenuSem);
516	}
517}
518
519
520/*static*/ int32
521BMenuBar::_TrackTask(void* arg)
522{
523	menubar_data data;
524	thread_id id;
525	receive_data(&id, &data, sizeof(data));
526
527	BMenuBar* menuBar = data.menuBar;
528	if (data.useRect)
529		menuBar->fExtraRect = &data.rect;
530	menuBar->_SetStickyMode(data.sticky);
531
532	int32 action;
533	menuBar->_Track(&action, data.menuIndex, data.showMenu);
534
535	menuBar->fTracking = false;
536	menuBar->fExtraRect = NULL;
537
538	// We aren't the BWindow thread, so don't call MenusEnded() directly
539	BWindow* window = menuBar->Window();
540	window->PostMessage(_MENUS_DONE_);
541
542	_set_menu_sem_(window, B_BAD_SEM_ID);
543	delete_sem(menuBar->fMenuSem);
544	menuBar->fMenuSem = B_BAD_SEM_ID;
545
546	return 0;
547}
548
549
550BMenuItem*
551BMenuBar::_Track(int32* action, int32 startIndex, bool showMenu)
552{
553	// TODO: Cleanup, merge some "if" blocks if possible
554	BMenuItem* item = NULL;
555	fState = MENU_STATE_TRACKING;
556	fChosenItem = NULL;
557		// we will use this for keyboard selection
558
559	BPoint where;
560	uint32 buttons;
561	if (LockLooper()) {
562		if (startIndex != -1) {
563			be_app->ObscureCursor();
564			_SelectItem(ItemAt(startIndex), true, false);
565		}
566		GetMouse(&where, &buttons);
567		UnlockLooper();
568	}
569
570	while (fState != MENU_STATE_CLOSED) {
571		bigtime_t snoozeAmount = 40000;
572		if (!LockLooper())
573			break;
574
575		item = dynamic_cast<_BMCMenuBar_*>(this) != NULL ? ItemAt(0)
576			: _HitTestItems(where, B_ORIGIN);
577
578		if (_OverSubmenu(fSelected, ConvertToScreen(where))
579			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
580			// call _Track() from the selected sub-menu when the mouse cursor
581			// is over its window
582			BMenu* submenu = fSelected->Submenu();
583			UnlockLooper();
584			snoozeAmount = 30000;
585			submenu->_SetStickyMode(_IsStickyMode());
586			int localAction;
587			fChosenItem = submenu->_Track(&localAction);
588
589			// The mouse could have meen moved since the last time we
590			// checked its position, or buttons might have been pressed.
591			// Unfortunately our child menus don't tell
592			// us the new position.
593			// TODO: Maybe have a shared struct between all menus
594			// where to store the current mouse position ?
595			// (Or just use the BView mouse hooks)
596			BPoint newWhere;
597			if (LockLooper()) {
598				GetMouse(&newWhere, &buttons);
599				UnlockLooper();
600			}
601
602			// Needed to make BMenuField child menus "sticky"
603			// (see ticket #953)
604			if (localAction == MENU_STATE_CLOSED) {
605				if (fExtraRect != NULL && fExtraRect->Contains(where)
606					&& point_distance(newWhere, where) < 9) {
607					// 9 = 3 pixels ^ 2 (since point_distance() returns the
608					// square of the distance)
609					_SetStickyMode(true);
610					fExtraRect = NULL;
611				} else
612					fState = MENU_STATE_CLOSED;
613			}
614			if (!LockLooper())
615				break;
616		} else if (item != NULL) {
617			if (item->Submenu() != NULL && item != fSelected) {
618				if (item->Submenu()->Window() == NULL) {
619					// open the menu if it's not opened yet
620					_SelectItem(item);
621				} else {
622					// Menu was already opened, close it and bail
623					_SelectItem(NULL);
624					fState = MENU_STATE_CLOSED;
625					fChosenItem = NULL;
626				}
627			} else {
628				// No submenu, just select the item
629				_SelectItem(item);
630			}
631		} else if (item == NULL && fSelected != NULL
632			&& !_IsStickyMode() && Bounds().Contains(where)) {
633			_SelectItem(NULL);
634			fState = MENU_STATE_TRACKING;
635		}
636
637		UnlockLooper();
638
639		if (fState != MENU_STATE_CLOSED) {
640			BPoint newWhere = where;
641			uint32 newButtons = buttons;
642
643			do {
644				// If user doesn't move the mouse or change buttons loop
645				// here so that we don't interfere with keyboard menu
646				// navigation
647				snooze(snoozeAmount);
648				if (!LockLooper())
649					break;
650
651				GetMouse(&newWhere, &newButtons);
652				UnlockLooper();
653			} while (newWhere == where && newButtons == buttons
654				&& fState == MENU_STATE_TRACKING);
655
656			if (newButtons != 0 && _IsStickyMode()) {
657				if (item == NULL || (item->Submenu() != NULL
658						&& item->Submenu()->Window() != NULL)) {
659					// clicked outside the menu bar or on item with already
660					// open sub menu
661					fState = MENU_STATE_CLOSED;
662				} else
663					_SetStickyMode(false);
664			} else if (newButtons == 0 && !_IsStickyMode()) {
665				if ((fSelected != NULL && fSelected->Submenu() == NULL)
666					|| item == NULL) {
667					// clicked on an item without a submenu or clicked and
668					// released the mouse button outside the menu bar
669					fChosenItem = fSelected;
670					fState = MENU_STATE_CLOSED;
671				} else
672					_SetStickyMode(true);
673			}
674			where = newWhere;
675			buttons = newButtons;
676		}
677	}
678
679	if (LockLooper()) {
680		if (fSelected != NULL)
681			_SelectItem(NULL);
682
683		if (fChosenItem != NULL)
684			fChosenItem->Invoke();
685
686		_RestoreFocus();
687		UnlockLooper();
688	}
689
690	if (_IsStickyMode())
691		_SetStickyMode(false);
692
693	_DeleteMenuWindow();
694
695	if (action != NULL)
696		*action = fState;
697
698	return fChosenItem;
699}
700
701
702void
703BMenuBar::_StealFocus()
704{
705	// We already stole the focus, don't do anything
706	if (fPrevFocusToken != -1)
707		return;
708
709	BWindow* window = Window();
710	if (window != NULL && window->Lock()) {
711		BView* focusView = window->CurrentFocus();
712		if (focusView != NULL && focusView != this)
713			fPrevFocusToken = _get_object_token_(focusView);
714		MakeFocus();
715		window->Unlock();
716	}
717}
718
719
720void
721BMenuBar::_RestoreFocus()
722{
723	BWindow* window = Window();
724	if (window != NULL && window->Lock()) {
725		BHandler* handler = NULL;
726		if (fPrevFocusToken != -1
727			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN,
728				(void**)&handler) == B_OK) {
729			BView* view = dynamic_cast<BView*>(handler);
730			if (view != NULL && view->Window() == window)
731				view->MakeFocus();
732		} else if (IsFocus())
733			MakeFocus(false);
734
735		fPrevFocusToken = -1;
736		window->Unlock();
737	}
738}
739
740
741void
742BMenuBar::_InitData(menu_layout layout)
743{
744	fBorders = BControlLook::B_ALL_BORDERS;
745	fLastBounds = new BRect(Bounds());
746	SetItemMargins(8.0f, 2.0f, 8.0f, 2.0f);
747	_SetIgnoreHidden(true);
748	SetLowUIColor(B_MENU_BACKGROUND_COLOR);
749	SetViewColor(B_TRANSPARENT_COLOR);
750}
751