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