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