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