xref: /haiku/src/kits/interface/MenuBar.cpp (revision 37fedaf8494b34aad811abcc49e79aa32943f880)
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 
32 using BPrivate::gDefaultTokens;
33 
34 
35 struct 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 
47 BMenuBar::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 
63 BMenuBar::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 
79 BMenuBar::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 
101 BMenuBar::~BMenuBar()
102 {
103 	if (fTracking) {
104 		status_t dummy;
105 		wait_for_thread(fTrackingPID, &dummy);
106 	}
107 
108 	delete fLastBounds;
109 }
110 
111 
112 BArchivable*
113 BMenuBar::Instantiate(BMessage* data)
114 {
115 	if (validate_instantiation(data, "BMenuBar"))
116 		return new BMenuBar(data);
117 
118 	return NULL;
119 }
120 
121 
122 status_t
123 BMenuBar::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 
140 void
141 BMenuBar::AttachedToWindow()
142 {
143 	_Install(Window());
144 	Window()->SetKeyMenuBar(this);
145 
146 	BMenu::AttachedToWindow();
147 
148 	*fLastBounds = Bounds();
149 }
150 
151 
152 void
153 BMenuBar::DetachedFromWindow()
154 {
155 	BMenu::DetachedFromWindow();
156 }
157 
158 
159 void
160 BMenuBar::AllAttached()
161 {
162 	BMenu::AllAttached();
163 }
164 
165 
166 void
167 BMenuBar::AllDetached()
168 {
169 	BMenu::AllDetached();
170 }
171 
172 
173 void
174 BMenuBar::WindowActivated(bool state)
175 {
176 	BView::WindowActivated(state);
177 }
178 
179 
180 void
181 BMenuBar::MakeFocus(bool state)
182 {
183 	BMenu::MakeFocus(state);
184 }
185 
186 
187 // #pragma mark -
188 
189 
190 void
191 BMenuBar::ResizeToPreferred()
192 {
193 	BMenu::ResizeToPreferred();
194 }
195 
196 
197 void
198 BMenuBar::GetPreferredSize(float* width, float* height)
199 {
200 	BMenu::GetPreferredSize(width, height);
201 }
202 
203 
204 BSize
205 BMenuBar::MinSize()
206 {
207 	return BMenu::MinSize();
208 }
209 
210 
211 BSize
212 BMenuBar::MaxSize()
213 {
214 	BSize size = BMenu::MaxSize();
215 	return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
216 		BSize(B_SIZE_UNLIMITED, size.height));
217 }
218 
219 
220 BSize
221 BMenuBar::PreferredSize()
222 {
223 	return BMenu::PreferredSize();
224 }
225 
226 
227 void
228 BMenuBar::FrameMoved(BPoint newPosition)
229 {
230 	BMenu::FrameMoved(newPosition);
231 }
232 
233 
234 void
235 BMenuBar::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 
260 void
261 BMenuBar::Show()
262 {
263 	BView::Show();
264 }
265 
266 
267 void
268 BMenuBar::Hide()
269 {
270 	BView::Hide();
271 }
272 
273 
274 void
275 BMenuBar::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 
291 	_DrawItems(updateRect);
292 }
293 
294 
295 // #pragma mark -
296 
297 
298 void
299 BMenuBar::MessageReceived(BMessage* msg)
300 {
301 	BMenu::MessageReceived(msg);
302 }
303 
304 
305 void
306 BMenuBar::MouseDown(BPoint where)
307 {
308 	if (fTracking)
309 		return;
310 
311 	uint32 buttons;
312 	GetMouse(&where, &buttons);
313 
314 	BWindow* window = Window();
315 	if (!window->IsActive() || !window->IsFront()) {
316 		if ((mouse_mode() == B_FOCUS_FOLLOWS_MOUSE)
317 			|| ((mouse_mode() == B_CLICK_TO_FOCUS_MOUSE)
318 				&& ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0))) {
319 			// right-click to bring-to-front and send-to-back
320 			// (might cause some regressions in FFM)
321 			window->Activate();
322 			window->UpdateIfNeeded();
323 		}
324 	}
325 
326 	StartMenuBar(-1, false, false);
327 }
328 
329 
330 void
331 BMenuBar::MouseUp(BPoint where)
332 {
333 	BView::MouseUp(where);
334 }
335 
336 
337 // #pragma mark -
338 
339 
340 BHandler*
341 BMenuBar::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
342 	int32 form, const char* property)
343 {
344 	return BMenu::ResolveSpecifier(msg, index, specifier, form, property);
345 }
346 
347 
348 status_t
349 BMenuBar::GetSupportedSuites(BMessage* data)
350 {
351 	return BMenu::GetSupportedSuites(data);
352 }
353 
354 
355 // #pragma mark -
356 
357 
358 void
359 BMenuBar::SetBorder(menu_bar_border border)
360 {
361 	fBorder = border;
362 }
363 
364 
365 menu_bar_border
366 BMenuBar::Border() const
367 {
368 	return fBorder;
369 }
370 
371 
372 // #pragma mark -
373 
374 
375 status_t
376 BMenuBar::Perform(perform_code code, void* _data)
377 {
378 	switch (code) {
379 		case PERFORM_CODE_MIN_SIZE:
380 			((perform_data_min_size*)_data)->return_value
381 				= BMenuBar::MinSize();
382 			return B_OK;
383 
384 		case PERFORM_CODE_MAX_SIZE:
385 			((perform_data_max_size*)_data)->return_value
386 				= BMenuBar::MaxSize();
387 			return B_OK;
388 
389 		case PERFORM_CODE_PREFERRED_SIZE:
390 			((perform_data_preferred_size*)_data)->return_value
391 				= BMenuBar::PreferredSize();
392 			return B_OK;
393 
394 		case PERFORM_CODE_LAYOUT_ALIGNMENT:
395 			((perform_data_layout_alignment*)_data)->return_value
396 				= BMenuBar::LayoutAlignment();
397 			return B_OK;
398 
399 		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
400 			((perform_data_has_height_for_width*)_data)->return_value
401 				= BMenuBar::HasHeightForWidth();
402 			return B_OK;
403 
404 		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
405 		{
406 			perform_data_get_height_for_width* data
407 				= (perform_data_get_height_for_width*)_data;
408 			BMenuBar::GetHeightForWidth(data->width, &data->min, &data->max,
409 				&data->preferred);
410 			return B_OK;
411 		}
412 
413 		case PERFORM_CODE_SET_LAYOUT:
414 		{
415 			perform_data_set_layout* data = (perform_data_set_layout*)_data;
416 			BMenuBar::SetLayout(data->layout);
417 			return B_OK;
418 		}
419 
420 		case PERFORM_CODE_LAYOUT_INVALIDATED:
421 		{
422 			perform_data_layout_invalidated* data
423 				= (perform_data_layout_invalidated*)_data;
424 			BMenuBar::LayoutInvalidated(data->descendants);
425 			return B_OK;
426 		}
427 
428 		case PERFORM_CODE_DO_LAYOUT:
429 		{
430 			BMenuBar::DoLayout();
431 			return B_OK;
432 		}
433 	}
434 
435 	return BMenu::Perform(code, _data);
436 }
437 
438 
439 // #pragma mark -
440 
441 
442 void BMenuBar::_ReservedMenuBar1() {}
443 void BMenuBar::_ReservedMenuBar2() {}
444 void BMenuBar::_ReservedMenuBar3() {}
445 void BMenuBar::_ReservedMenuBar4() {}
446 
447 
448 BMenuBar &
449 BMenuBar::operator=(const BMenuBar &)
450 {
451 	return *this;
452 }
453 
454 
455 // #pragma mark -
456 
457 
458 void
459 BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu,
460 	BRect* specialRect)
461 {
462 	if (fTracking)
463 		return;
464 
465 	BWindow* window = Window();
466 	if (window == NULL)
467 		debugger("MenuBar must be added to a window before it can be used.");
468 
469 	BAutolock lock(window);
470 	if (!lock.IsLocked())
471 		return;
472 
473 	fPrevFocusToken = -1;
474 	fTracking = true;
475 
476 	// We are called from the window's thread,
477 	// so let's call MenusBeginning() directly
478 	window->MenusBeginning();
479 
480 	fMenuSem = create_sem(0, "window close sem");
481 	_set_menu_sem_(window, fMenuSem);
482 
483 	fTrackingPID = spawn_thread(_TrackTask, "menu_tracking",
484 		B_DISPLAY_PRIORITY, NULL);
485 	if (fTrackingPID >= 0) {
486 		menubar_data data;
487 		data.menuBar = this;
488 		data.menuIndex = menuIndex;
489 		data.sticky = sticky;
490 		data.showMenu = showMenu;
491 		data.useRect = specialRect != NULL;
492 		if (data.useRect)
493 			data.rect = *specialRect;
494 
495 		resume_thread(fTrackingPID);
496 		send_data(fTrackingPID, 0, &data, sizeof(data));
497 	} else {
498 		fTracking = false;
499 		_set_menu_sem_(window, B_NO_MORE_SEMS);
500 		delete_sem(fMenuSem);
501 	}
502 }
503 
504 
505 /*static*/ int32
506 BMenuBar::_TrackTask(void* arg)
507 {
508 	menubar_data data;
509 	thread_id id;
510 	receive_data(&id, &data, sizeof(data));
511 
512 	BMenuBar* menuBar = data.menuBar;
513 	if (data.useRect)
514 		menuBar->fExtraRect = &data.rect;
515 	menuBar->_SetStickyMode(data.sticky);
516 
517 	int32 action;
518 	menuBar->_Track(&action, data.menuIndex, data.showMenu);
519 
520 	menuBar->fTracking = false;
521 	menuBar->fExtraRect = NULL;
522 
523 	// We aren't the BWindow thread, so don't call MenusEnded() directly
524 	BWindow* window = menuBar->Window();
525 	window->PostMessage(_MENUS_DONE_);
526 
527 	_set_menu_sem_(window, B_BAD_SEM_ID);
528 	delete_sem(menuBar->fMenuSem);
529 	menuBar->fMenuSem = B_BAD_SEM_ID;
530 
531 	return 0;
532 }
533 
534 
535 BMenuItem*
536 BMenuBar::_Track(int32* action, int32 startIndex, bool showMenu)
537 {
538 	// TODO: Cleanup, merge some "if" blocks if possible
539 	BMenuItem* item = NULL;
540 	fState = MENU_STATE_TRACKING;
541 	fChosenItem = NULL;
542 		// we will use this for keyboard selection
543 
544 	BPoint where;
545 	uint32 buttons;
546 	if (LockLooper()) {
547 		if (startIndex != -1) {
548 			be_app->ObscureCursor();
549 			_SelectItem(ItemAt(startIndex), true, false);
550 		}
551 		GetMouse(&where, &buttons);
552 		UnlockLooper();
553 	}
554 
555 	while (fState != MENU_STATE_CLOSED) {
556 		bigtime_t snoozeAmount = 40000;
557 		if (!LockLooper())
558 			break;
559 
560 		item = dynamic_cast<_BMCMenuBar_*>(this) != NULL ? ItemAt(0)
561 			: _HitTestItems(where, B_ORIGIN);
562 
563 		if (_OverSubmenu(fSelected, ConvertToScreen(where))
564 			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
565 			// call _Track() from the selected sub-menu when the mouse cursor
566 			// is over its window
567 			BMenu* submenu = fSelected->Submenu();
568 			UnlockLooper();
569 			snoozeAmount = 30000;
570 			submenu->_SetStickyMode(_IsStickyMode());
571 			int localAction;
572 			fChosenItem = submenu->_Track(&localAction);
573 
574 			// The mouse could have meen moved since the last time we
575 			// checked its position, or buttons might have been pressed.
576 			// Unfortunately our child menus don't tell
577 			// us the new position.
578 			// TODO: Maybe have a shared struct between all menus
579 			// where to store the current mouse position ?
580 			// (Or just use the BView mouse hooks)
581 			BPoint newWhere;
582 			if (LockLooper()) {
583 				GetMouse(&newWhere, &buttons);
584 				UnlockLooper();
585 			}
586 
587 			// Needed to make BMenuField child menus "sticky"
588 			// (see ticket #953)
589 			if (localAction == MENU_STATE_CLOSED) {
590 				if (fExtraRect != NULL && fExtraRect->Contains(where)
591 					&& point_distance(newWhere, where) < 9) {
592 					// 9 = 3 pixels ^ 2 (since point_distance() returns the
593 					// square of the distance)
594 					_SetStickyMode(true);
595 					fExtraRect = NULL;
596 				} else
597 					fState = MENU_STATE_CLOSED;
598 			}
599 			if (!LockLooper())
600 				break;
601 		} else if (item != NULL) {
602 			if (item->Submenu() != NULL && item != fSelected) {
603 				if (item->Submenu()->Window() == NULL) {
604 					// open the menu if it's not opened yet
605 					_SelectItem(item);
606 				} else {
607 					// Menu was already opened, close it and bail
608 					_SelectItem(NULL);
609 					fState = MENU_STATE_CLOSED;
610 					fChosenItem = NULL;
611 				}
612 			} else {
613 				// No submenu, just select the item
614 				_SelectItem(item);
615 			}
616 		} else if (item == NULL && fSelected != NULL
617 			&& !_IsStickyMode() && Bounds().Contains(where)) {
618 			_SelectItem(NULL);
619 			fState = MENU_STATE_TRACKING;
620 		}
621 
622 		UnlockLooper();
623 
624 		if (fState != MENU_STATE_CLOSED) {
625 			BPoint newWhere = where;
626 			uint32 newButtons = buttons;
627 
628 			do {
629 				// If user doesn't move the mouse or change buttons loop
630 				// here so that we don't interfere with keyboard menu
631 				// navigation
632 				snooze(snoozeAmount);
633 				if (!LockLooper())
634 					break;
635 
636 				GetMouse(&newWhere, &newButtons);
637 				UnlockLooper();
638 			} while (newWhere == where && newButtons == buttons
639 				&& fState == MENU_STATE_TRACKING);
640 
641 			if (newButtons != 0 && _IsStickyMode()) {
642 				if (item == NULL || (item->Submenu() != NULL
643 						&& item->Submenu()->Window() != NULL)) {
644 					// clicked outside the menu bar or on item with already
645 					// open sub menu
646 					fState = MENU_STATE_CLOSED;
647 				} else
648 					_SetStickyMode(false);
649 			} else if (newButtons == 0 && !_IsStickyMode()) {
650 				if ((fSelected != NULL && fSelected->Submenu() == NULL)
651 					|| item == NULL) {
652 					// clicked on an item without a submenu or clicked and
653 					// released the mouse button outside the menu bar
654 					fChosenItem = fSelected;
655 					fState = MENU_STATE_CLOSED;
656 				} else
657 					_SetStickyMode(true);
658 			}
659 			where = newWhere;
660 			buttons = newButtons;
661 		}
662 	}
663 
664 	if (LockLooper()) {
665 		if (fSelected != NULL)
666 			_SelectItem(NULL);
667 
668 		if (fChosenItem != NULL)
669 			fChosenItem->Invoke();
670 
671 		_RestoreFocus();
672 		UnlockLooper();
673 	}
674 
675 	if (_IsStickyMode())
676 		_SetStickyMode(false);
677 
678 	_DeleteMenuWindow();
679 
680 	if (action != NULL)
681 		*action = fState;
682 
683 	return fChosenItem;
684 }
685 
686 
687 void
688 BMenuBar::_StealFocus()
689 {
690 	// We already stole the focus, don't do anything
691 	if (fPrevFocusToken != -1)
692 		return;
693 
694 	BWindow* window = Window();
695 	if (window != NULL && window->Lock()) {
696 		BView* focusView = window->CurrentFocus();
697 		if (focusView != NULL && focusView != this)
698 			fPrevFocusToken = _get_object_token_(focusView);
699 		MakeFocus();
700 		window->Unlock();
701 	}
702 }
703 
704 
705 void
706 BMenuBar::_RestoreFocus()
707 {
708 	BWindow* window = Window();
709 	if (window != NULL && window->Lock()) {
710 		BHandler* handler = NULL;
711 		if (fPrevFocusToken != -1
712 			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN,
713 				(void**)&handler) == B_OK) {
714 			BView* view = dynamic_cast<BView*>(handler);
715 			if (view != NULL && view->Window() == window)
716 				view->MakeFocus();
717 		} else if (IsFocus())
718 			MakeFocus(false);
719 
720 		fPrevFocusToken = -1;
721 		window->Unlock();
722 	}
723 }
724 
725 
726 void
727 BMenuBar::_InitData(menu_layout layout)
728 {
729 	fLastBounds = new BRect(Bounds());
730 	SetItemMargins(8, 2, 8, 2);
731 	_SetIgnoreHidden(true);
732 }
733