xref: /haiku/src/kits/interface/MenuBar.cpp (revision be012e21222c4d8d70082d12353acb163dc60ba8)
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 		case PERFORM_CODE_MAX_SIZE:
384 			((perform_data_max_size*)_data)->return_value
385 				= BMenuBar::MaxSize();
386 			return B_OK;
387 		case PERFORM_CODE_PREFERRED_SIZE:
388 			((perform_data_preferred_size*)_data)->return_value
389 				= BMenuBar::PreferredSize();
390 			return B_OK;
391 		case PERFORM_CODE_LAYOUT_ALIGNMENT:
392 			((perform_data_layout_alignment*)_data)->return_value
393 				= BMenuBar::LayoutAlignment();
394 			return B_OK;
395 		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
396 			((perform_data_has_height_for_width*)_data)->return_value
397 				= BMenuBar::HasHeightForWidth();
398 			return B_OK;
399 		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
400 		{
401 			perform_data_get_height_for_width* data
402 				= (perform_data_get_height_for_width*)_data;
403 			BMenuBar::GetHeightForWidth(data->width, &data->min, &data->max,
404 				&data->preferred);
405 			return B_OK;
406 		}
407 		case PERFORM_CODE_SET_LAYOUT:
408 		{
409 			perform_data_set_layout* data = (perform_data_set_layout*)_data;
410 			BMenuBar::SetLayout(data->layout);
411 			return B_OK;
412 		}
413 		case PERFORM_CODE_LAYOUT_INVALIDATED:
414 		{
415 			perform_data_layout_invalidated* data
416 				= (perform_data_layout_invalidated*)_data;
417 			BMenuBar::LayoutInvalidated(data->descendants);
418 			return B_OK;
419 		}
420 		case PERFORM_CODE_DO_LAYOUT:
421 		{
422 			BMenuBar::DoLayout();
423 			return B_OK;
424 		}
425 	}
426 
427 	return BMenu::Perform(code, _data);
428 }
429 
430 
431 // #pragma mark -
432 
433 
434 void BMenuBar::_ReservedMenuBar1() {}
435 void BMenuBar::_ReservedMenuBar2() {}
436 void BMenuBar::_ReservedMenuBar3() {}
437 void BMenuBar::_ReservedMenuBar4() {}
438 
439 
440 BMenuBar &
441 BMenuBar::operator=(const BMenuBar &)
442 {
443 	return *this;
444 }
445 
446 
447 // #pragma mark -
448 
449 
450 void
451 BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu,
452 	BRect* specialRect)
453 {
454 	if (fTracking)
455 		return;
456 
457 	BWindow* window = Window();
458 	if (window == NULL)
459 		debugger("MenuBar must be added to a window before it can be used.");
460 
461 	BAutolock lock(window);
462 	if (!lock.IsLocked())
463 		return;
464 
465 	fPrevFocusToken = -1;
466 	fTracking = true;
467 
468 	// We are called from the window's thread,
469 	// so let's call MenusBeginning() directly
470 	window->MenusBeginning();
471 
472 	fMenuSem = create_sem(0, "window close sem");
473 	_set_menu_sem_(window, fMenuSem);
474 
475 	fTrackingPID = spawn_thread(_TrackTask, "menu_tracking",
476 		B_DISPLAY_PRIORITY, NULL);
477 	if (fTrackingPID >= 0) {
478 		menubar_data data;
479 		data.menuBar = this;
480 		data.menuIndex = menuIndex;
481 		data.sticky = sticky;
482 		data.showMenu = showMenu;
483 		data.useRect = specialRect != NULL;
484 		if (data.useRect)
485 			data.rect = *specialRect;
486 
487 		resume_thread(fTrackingPID);
488 		send_data(fTrackingPID, 0, &data, sizeof(data));
489 	} else {
490 		fTracking = false;
491 		_set_menu_sem_(window, B_NO_MORE_SEMS);
492 		delete_sem(fMenuSem);
493 	}
494 }
495 
496 
497 /*static*/ int32
498 BMenuBar::_TrackTask(void* arg)
499 {
500 	menubar_data data;
501 	thread_id id;
502 	receive_data(&id, &data, sizeof(data));
503 
504 	BMenuBar* menuBar = data.menuBar;
505 	if (data.useRect)
506 		menuBar->fExtraRect = &data.rect;
507 	menuBar->_SetStickyMode(data.sticky);
508 
509 	int32 action;
510 	menuBar->_Track(&action, data.menuIndex, data.showMenu);
511 
512 	menuBar->fTracking = false;
513 	menuBar->fExtraRect = NULL;
514 
515 	// We aren't the BWindow thread, so don't call MenusEnded() directly
516 	BWindow* window = menuBar->Window();
517 	window->PostMessage(_MENUS_DONE_);
518 
519 	_set_menu_sem_(window, B_BAD_SEM_ID);
520 	delete_sem(menuBar->fMenuSem);
521 	menuBar->fMenuSem = B_BAD_SEM_ID;
522 
523 	return 0;
524 }
525 
526 
527 BMenuItem*
528 BMenuBar::_Track(int32* action, int32 startIndex, bool showMenu)
529 {
530 	// TODO: Cleanup, merge some "if" blocks if possible
531 	BMenuItem* item = NULL;
532 	fState = MENU_STATE_TRACKING;
533 	fChosenItem = NULL;
534 		// we will use this for keyboard selection
535 
536 	BPoint where;
537 	uint32 buttons;
538 	if (LockLooper()) {
539 		if (startIndex != -1) {
540 			be_app->ObscureCursor();
541 			_SelectItem(ItemAt(startIndex), true, false);
542 		}
543 		GetMouse(&where, &buttons);
544 		UnlockLooper();
545 	}
546 
547 	while (fState != MENU_STATE_CLOSED) {
548 		bigtime_t snoozeAmount = 40000;
549 		if (!LockLooper())
550 			break;
551 
552 		item = dynamic_cast<_BMCMenuBar_*>(this) != NULL ? ItemAt(0)
553 			: _HitTestItems(where, B_ORIGIN);
554 
555 		if (_OverSubmenu(fSelected, ConvertToScreen(where))
556 			|| fState == MENU_STATE_KEY_TO_SUBMENU) {
557 			// call _Track() from the selected sub-menu when the mouse cursor
558 			// is over its window
559 			BMenu* submenu = fSelected->Submenu();
560 			UnlockLooper();
561 			snoozeAmount = 30000;
562 			submenu->_SetStickyMode(_IsStickyMode());
563 			int localAction;
564 			fChosenItem = submenu->_Track(&localAction);
565 
566 			// The mouse could have meen moved since the last time we
567 			// checked its position, or buttons might have been pressed.
568 			// Unfortunately our child menus don't tell
569 			// us the new position.
570 			// TODO: Maybe have a shared struct between all menus
571 			// where to store the current mouse position ?
572 			// (Or just use the BView mouse hooks)
573 			BPoint newWhere;
574 			if (LockLooper()) {
575 				GetMouse(&newWhere, &buttons);
576 				UnlockLooper();
577 			}
578 
579 			// Needed to make BMenuField child menus "sticky"
580 			// (see ticket #953)
581 			if (localAction == MENU_STATE_CLOSED) {
582 				if (fExtraRect != NULL && fExtraRect->Contains(where)
583 					&& point_distance(newWhere, where) < 9) {
584 					// 9 = 3 pixels ^ 2 (since point_distance() returns the
585 					// square of the distance)
586 					_SetStickyMode(true);
587 					fExtraRect = NULL;
588 				} else
589 					fState = MENU_STATE_CLOSED;
590 			}
591 			if (!LockLooper())
592 				break;
593 		} else if (item != NULL) {
594 			if (item->Submenu() != NULL && item != fSelected) {
595 				if (item->Submenu()->Window() == NULL) {
596 					// open the menu if it's not opened yet
597 					_SelectItem(item);
598 				} else {
599 					// Menu was already opened, close it and bail
600 					_SelectItem(NULL);
601 					fState = MENU_STATE_CLOSED;
602 					fChosenItem = NULL;
603 				}
604 			} else {
605 				// No submenu, just select the item
606 				_SelectItem(item);
607 			}
608 		} else if (item == NULL && fSelected != NULL
609 			&& !_IsStickyMode() && Bounds().Contains(where)) {
610 			_SelectItem(NULL);
611 			fState = MENU_STATE_TRACKING;
612 		}
613 
614 		UnlockLooper();
615 
616 		if (fState != MENU_STATE_CLOSED) {
617 			BPoint newWhere = where;
618 			uint32 newButtons = buttons;
619 
620 			do {
621 				// If user doesn't move the mouse or change buttons loop
622 				// here so that we don't interfere with keyboard menu
623 				// navigation
624 				snooze(snoozeAmount);
625 				if (!LockLooper())
626 					break;
627 
628 				GetMouse(&newWhere, &newButtons);
629 				UnlockLooper();
630 			} while (newWhere == where && newButtons == buttons
631 				&& fState == MENU_STATE_TRACKING);
632 
633 			if (newButtons != 0 && _IsStickyMode()) {
634 				if (item == NULL || (item->Submenu() != NULL
635 						&& item->Submenu()->Window() != NULL)) {
636 					// clicked outside the menu bar or on item with already
637 					// open sub menu
638 					fState = MENU_STATE_CLOSED;
639 				} else
640 					_SetStickyMode(false);
641 			} else if (newButtons == 0 && !_IsStickyMode()) {
642 				if ((fSelected != NULL && fSelected->Submenu() == NULL)
643 					|| item == NULL) {
644 					// clicked on an item without a submenu or clicked and
645 					// released the mouse button outside the menu bar
646 					fChosenItem = fSelected;
647 					fState = MENU_STATE_CLOSED;
648 				} else
649 					_SetStickyMode(true);
650 			}
651 			where = newWhere;
652 			buttons = newButtons;
653 		}
654 	}
655 
656 	if (LockLooper()) {
657 		if (fSelected != NULL)
658 			_SelectItem(NULL);
659 
660 		if (fChosenItem != NULL)
661 			fChosenItem->Invoke();
662 
663 		_RestoreFocus();
664 		UnlockLooper();
665 	}
666 
667 	if (_IsStickyMode())
668 		_SetStickyMode(false);
669 
670 	_DeleteMenuWindow();
671 
672 	if (action != NULL)
673 		*action = fState;
674 
675 	return fChosenItem;
676 }
677 
678 
679 void
680 BMenuBar::_StealFocus()
681 {
682 	// We already stole the focus, don't do anything
683 	if (fPrevFocusToken != -1)
684 		return;
685 
686 	BWindow* window = Window();
687 	if (window != NULL && window->Lock()) {
688 		BView* focusView = window->CurrentFocus();
689 		if (focusView != NULL && focusView != this)
690 			fPrevFocusToken = _get_object_token_(focusView);
691 		MakeFocus();
692 		window->Unlock();
693 	}
694 }
695 
696 
697 void
698 BMenuBar::_RestoreFocus()
699 {
700 	BWindow* window = Window();
701 	if (window != NULL && window->Lock()) {
702 		BHandler* handler = NULL;
703 		if (fPrevFocusToken != -1
704 			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN,
705 				(void**)&handler) == B_OK) {
706 			BView* view = dynamic_cast<BView*>(handler);
707 			if (view != NULL && view->Window() == window)
708 				view->MakeFocus();
709 		} else if (IsFocus())
710 			MakeFocus(false);
711 
712 		fPrevFocusToken = -1;
713 		window->Unlock();
714 	}
715 }
716 
717 
718 void
719 BMenuBar::_InitData(menu_layout layout)
720 {
721 	fLastBounds = new BRect(Bounds());
722 	SetItemMargins(8, 2, 8, 2);
723 	_SetIgnoreHidden(true);
724 }
725