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