xref: /haiku/src/kits/interface/MenuBar.cpp (revision 93a78ecaa45114d68952d08c4778f073515102f2)
1 /*
2  * Copyright 2001-2006, 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 	SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR), B_LIGHTEN_2_TINT));
156 	StrokeLine(BPoint(0.0f, bounds.bottom - 2.0f), BPoint(0.0f, 0.0f));
157 	StrokeLine(BPoint(bounds.right, 0.0f));
158 
159 	SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR), B_DARKEN_1_TINT));
160 	StrokeLine(BPoint(1.0f, bounds.bottom - 1.0f),
161 		BPoint(bounds.right, bounds.bottom - 1.0f));
162 
163 	SetHighColor(tint_color(ui_color(B_MENU_BACKGROUND_COLOR), B_DARKEN_2_TINT));
164 	StrokeLine(BPoint(0.0f, bounds.bottom), BPoint(bounds.right, bounds.bottom));
165 	StrokeLine(BPoint(bounds.right, 0.0f), BPoint(bounds.right, bounds.bottom));
166 
167 	SetHighColor(color);
168 		// revert to previous used color (cheap PushState()/PopState())
169 
170 	DrawItems(updateRect);
171 }
172 
173 
174 void
175 BMenuBar::AttachedToWindow()
176 {
177 	Install(Window());
178 	Window()->SetKeyMenuBar(this);
179 
180 	BMenu::AttachedToWindow();
181 
182 	*fLastBounds = Bounds();
183 }
184 
185 
186 void
187 BMenuBar::DetachedFromWindow()
188 {
189 	BMenu::DetachedFromWindow();
190 }
191 
192 
193 void
194 BMenuBar::MessageReceived(BMessage *msg)
195 {
196 	BMenu::MessageReceived(msg);
197 }
198 
199 
200 void
201 BMenuBar::MouseDown(BPoint where)
202 {
203 	if (fTracking)
204 		return;
205 
206 	BWindow *window = Window();
207 	if (!window->IsActive() || !window->IsFront()) {
208 		window->Activate();
209 		window->UpdateIfNeeded();
210 	}
211 
212 	StartMenuBar(-1, false, false);
213 }
214 
215 
216 void
217 BMenuBar::WindowActivated(bool state)
218 {
219 	BView::WindowActivated(state);
220 }
221 
222 
223 void
224 BMenuBar::MouseUp(BPoint where)
225 {
226 	BView::MouseUp(where);
227 }
228 
229 
230 void
231 BMenuBar::FrameMoved(BPoint newPosition)
232 {
233 	BMenu::FrameMoved(newPosition);
234 }
235 
236 
237 void
238 BMenuBar::FrameResized(float newWidth, float newHeight)
239 {
240 	// invalidate right border
241 	if (newWidth != fLastBounds->Width()) {
242 		BRect rect(min_c(fLastBounds->right, newWidth), 0,
243 			max_c(fLastBounds->right, newWidth), newHeight);
244 		Invalidate(rect);
245 	}
246 
247 	// invalidate bottom border
248 	if (newHeight != fLastBounds->Height()) {
249 		BRect rect(0, min_c(fLastBounds->bottom, newHeight) - 1,
250 			newWidth, max_c(fLastBounds->bottom, newHeight));
251 		Invalidate(rect);
252 	}
253 
254 	fLastBounds->Set(0, 0, newWidth, newHeight);
255 
256 	BMenu::FrameResized(newWidth, newHeight);
257 }
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 BHandler *
275 BMenuBar::ResolveSpecifier(BMessage *msg, int32 index, BMessage *specifier, int32 form, const char *property)
276 {
277 	return BMenu::ResolveSpecifier(msg, index, specifier, form, property);
278 }
279 
280 
281 status_t
282 BMenuBar::GetSupportedSuites(BMessage *data)
283 {
284 	return BMenu::GetSupportedSuites(data);
285 }
286 
287 
288 void
289 BMenuBar::ResizeToPreferred()
290 {
291 	BMenu::ResizeToPreferred();
292 }
293 
294 
295 void
296 BMenuBar::GetPreferredSize(float *width, float *height)
297 {
298 	BMenu::GetPreferredSize(width, height);
299 }
300 
301 
302 void
303 BMenuBar::MakeFocus(bool state)
304 {
305 	BMenu::MakeFocus(state);
306 }
307 
308 
309 void
310 BMenuBar::AllAttached()
311 {
312 	BMenu::AllAttached();
313 }
314 
315 
316 void
317 BMenuBar::AllDetached()
318 {
319 	BMenu::AllDetached();
320 }
321 
322 
323 status_t
324 BMenuBar::Perform(perform_code d, void *arg)
325 {
326 	return BMenu::Perform(d, arg);
327 }
328 
329 
330 BSize
331 BMenuBar::MinSize()
332 {
333 	return BMenu::MinSize();
334 }
335 
336 
337 BSize
338 BMenuBar::MaxSize()
339 {
340 	BSize size = BMenu::MaxSize();
341 	return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
342 		BSize(B_SIZE_UNLIMITED, size.height));
343 }
344 
345 
346 BSize
347 BMenuBar::PreferredSize()
348 {
349 	return BMenu::PreferredSize();
350 }
351 
352 
353 // #pragma mark -
354 
355 
356 void BMenuBar::_ReservedMenuBar1() {}
357 void BMenuBar::_ReservedMenuBar2() {}
358 void BMenuBar::_ReservedMenuBar3() {}
359 void BMenuBar::_ReservedMenuBar4() {}
360 
361 
362 BMenuBar &
363 BMenuBar::operator=(const BMenuBar &)
364 {
365 	return *this;
366 }
367 
368 
369 void
370 BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu, BRect *specialRect)
371 {
372 	if (fTracking)
373 		return;
374 
375 	BWindow *window = Window();
376 	if (window == NULL)
377 		debugger("MenuBar must be added to a window before it can be used.");
378 
379 	BAutolock lock(window);
380 	if (!lock.IsLocked())
381 		return;
382 
383 	fPrevFocusToken = -1;
384 	fTracking = true;
385 
386 	window->MenusBeginning();
387 
388 	fMenuSem = create_sem(0, "window close sem");
389 	_set_menu_sem_(window, fMenuSem);
390 
391 	fTrackingPID = spawn_thread(TrackTask, "menu_tracking", B_DISPLAY_PRIORITY, NULL);
392 	if (fTrackingPID >= 0) {
393 		menubar_data data;
394 		data.menuBar = this;
395 		data.menuIndex = menuIndex;
396 		data.sticky = sticky;
397 		data.showMenu = showMenu;
398 		data.useRect = specialRect != NULL;
399 		if (data.useRect)
400 			data.rect = *specialRect;
401 
402 		resume_thread(fTrackingPID);
403 		send_data(fTrackingPID, 0, &data, sizeof(data));
404 
405 	} else {
406 		fTracking = false;
407 		_set_menu_sem_(window, B_NO_MORE_SEMS);
408 		delete_sem(fMenuSem);
409 	}
410 }
411 
412 
413 long
414 BMenuBar::TrackTask(void *arg)
415 {
416 	menubar_data data;
417 	thread_id id;
418 
419 	receive_data(&id, &data, sizeof(data));
420 
421 	BMenuBar *menuBar = data.menuBar;
422 	if (data.useRect)
423 		menuBar->fExtraRect = &data.rect;
424 	menuBar->SetStickyMode(data.sticky);
425 
426 	int32 action;
427 	menuBar->Track(&action, data.menuIndex, data.showMenu);
428 
429 	menuBar->fTracking = false;
430 	menuBar->fExtraRect = NULL;
431 
432 	// Sends a _MENUS_DONE_ message to the BWindow.
433 	// Weird: There is a _MENUS_DONE_ message but not a
434 	// _MENUS_BEGINNING_ message, in fact the MenusBeginning()
435 	// hook function is called directly.
436 	BWindow *window = menuBar->Window();
437 	window->PostMessage(_MENUS_DONE_);
438 
439 	_set_menu_sem_(window, B_BAD_SEM_ID);
440 	delete_sem(menuBar->fMenuSem);
441 	menuBar->fMenuSem = B_BAD_SEM_ID;
442 
443 	return 0;
444 }
445 
446 
447 // Note: since sqrt is slow, we don't use it and return the square of the distance
448 // TODO: Move this to some common place, could be used in BMenu too.
449 #define square(x) ((x) * (x))
450 static float
451 point_distance(const BPoint &pointA, const BPoint &pointB)
452 {
453 	return square(pointA.x - pointB.x) + square(pointA.y - pointB.y);
454 }
455 #undef square
456 
457 
458 BMenuItem *
459 BMenuBar::Track(int32 *action, int32 startIndex, bool showMenu)
460 {
461 	// TODO: Cleanup, merge some "if" blocks if possible
462 	fChosenItem = NULL;
463 
464 	BWindow *window = Window();
465 	fState = MENU_STATE_TRACKING;
466 
467 	if (startIndex != -1) {
468 		be_app->ObscureCursor();
469 		if (window->Lock()) {
470 			_SelectItem(ItemAt(startIndex), true, true);
471 			window->Unlock();
472 		}
473 	}
474 
475 	while (true) {
476 		bigtime_t snoozeAmount = 40000;
477 		bool locked = window->Lock();//WithTimeout(200000)
478 		if (!locked)
479 			break;
480 
481 		BPoint where;
482 		ulong buttons;
483 		GetMouse(&where, &buttons, true);
484 
485 		BMenuItem *menuItem = HitTestItems(where, B_ORIGIN);
486 		if (menuItem != NULL) {
487 			// Select item if:
488 			// - no previous selection
489 			// - nonsticky mode and different selection,
490 			// - clicked in sticky mode
491 			if (fSelected == NULL
492 				|| (!IsStickyMode() && menuItem != fSelected)
493 				|| (buttons != 0 && IsStickyMode())) {
494 				if (menuItem->Submenu() != NULL) {
495 					if (menuItem->Submenu()->Window() == NULL) {
496 						// open the menu if it's not opened yet
497 						_SelectItem(menuItem);
498 						if (IsStickyMode())
499 							SetStickyMode(false);
500 					} else {
501 						// Menu was already opened, close it and bail
502 						_SelectItem(NULL);
503 						fState = MENU_STATE_CLOSED;
504 						fChosenItem = NULL;
505 					}
506 				} else {
507 					// No submenu, just select the item
508 					_SelectItem(menuItem);
509 				}
510 			}
511 		}
512 
513 		if (OverSubmenu(fSelected, ConvertToScreen(where))) {
514 			// call _track() from the selected sub-menu when the mouse cursor
515 			// is over its window
516 			BMenu *menu = fSelected->Submenu();
517 			window->Unlock();
518 			locked = false;
519 			snoozeAmount = 30000;
520 			bool wasSticky = IsStickyMode();
521 			if (wasSticky)
522 				menu->SetStickyMode(true);
523 			int localAction;
524 			fChosenItem = menu->_track(&localAction);
525 			if (menu->State(NULL) == MENU_STATE_TRACKING && menu->IsStickyMode())
526 				menu->SetStickyMode(false);
527 
528 			// check if the user started holding down a mouse button in a submenu
529 			if (wasSticky && !IsStickyMode())
530 				buttons = 1;
531 					// buttons must have been pressed in the meantime
532 
533 			// This code is needed to make menus
534 			// that are children of BMenuFields "sticky" (see ticket #953)
535 			if (localAction == MENU_STATE_CLOSED) {
536 				// The mouse could have meen moved since the last time we
537 				// checked its position. Unfortunately our child menus don't tell
538 				// us the new position.
539 				// TODO: Maybe have a shared struct between all menus
540 				// where to store the current mouse position ?
541 				BPoint newWhere;
542 				ulong newButtons;
543 				if (window->Lock()) {
544 					GetMouse(&newWhere, &newButtons);
545 					window->Unlock();
546 				}
547 
548 				if (fExtraRect != NULL && fExtraRect->Contains(where)
549 					// 9 = 3 pixels ^ 2 (since point_distance() returns the square of the distance)
550 					&& point_distance(newWhere, where) < 9) {
551 					SetStickyMode(true);
552 					fExtraRect = NULL;
553 				} else
554 					fState = MENU_STATE_CLOSED;
555 			}
556 
557 		} else if (menuItem == NULL && fSelected != NULL
558 			&& !IsStickyMode() && fState != MENU_STATE_TRACKING_SUBMENU) {
559 			_SelectItem(NULL);
560 			fState = MENU_STATE_TRACKING;
561 		}
562 
563 		if (locked)
564 			window->Unlock();
565 
566 		if (fState == MENU_STATE_CLOSED
567 			|| (buttons != 0 && IsStickyMode() && menuItem == NULL))
568 			break;
569 		else if (buttons == 0 && !IsStickyMode()) {
570 			if ((fSelected != NULL && fSelected->Submenu() == NULL) || menuItem == NULL) {
571 				fChosenItem = fSelected;
572 				break;
573 			} else
574 				SetStickyMode(true);
575 		}
576 
577 		if (snoozeAmount > 0)
578 			snooze(snoozeAmount);
579 	}
580 
581 	if (window->Lock()) {
582 		if (fSelected != NULL)
583 			_SelectItem(NULL);
584 
585 		if (fChosenItem != NULL)
586 			fChosenItem->Invoke();
587 		RestoreFocus();
588 		window->Unlock();
589 	}
590 
591 	if (IsStickyMode())
592 		SetStickyMode(false);
593 
594 	DeleteMenuWindow();
595 
596 	if (action != NULL)
597 		*action = fState;
598 
599 	return fChosenItem;
600 }
601 
602 
603 void
604 BMenuBar::StealFocus()
605 {
606 	// We already stole the focus, don't do anything
607 	if (fPrevFocusToken != -1)
608 		return;
609 
610 	BWindow *window = Window();
611 	if (window != NULL && window->Lock()) {
612 		BView *focus = window->CurrentFocus();
613 		if (focus != NULL && focus != this)
614 			fPrevFocusToken = _get_object_token_(focus);
615 		MakeFocus();
616 		window->Unlock();
617 	}
618 }
619 
620 
621 void
622 BMenuBar::RestoreFocus()
623 {
624 	BWindow *window = Window();
625 	if (window != NULL && window->Lock()) {
626 		BHandler *handler = NULL;
627 		if (fPrevFocusToken != -1
628 			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN, (void **)&handler) == B_OK) {
629 			BView *view = dynamic_cast<BView *>(handler);
630 			if (view != NULL && view->Window() == window)
631 				view->MakeFocus();
632 
633 		} else if (IsFocus())
634 			MakeFocus(false);
635 
636 		fPrevFocusToken = -1;
637 		window->Unlock();
638 	}
639 }
640 
641 
642 void
643 BMenuBar::InitData(menu_layout layout)
644 {
645 	fLastBounds = new BRect(Bounds());
646 	SetItemMargins(8, 2, 8, 2);
647 	SetIgnoreHidden(true);
648 }
649