xref: /haiku/src/kits/interface/MenuBar.cpp (revision 1214ef1b2100f2b3299fc9d8d6142e46f70a4c3f)
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 	// We are called from the window's thread,
389 	// so let's call MenusBeginning() directly
390 	window->MenusBeginning();
391 
392 	fMenuSem = create_sem(0, "window close sem");
393 	_set_menu_sem_(window, fMenuSem);
394 
395 	fTrackingPID = spawn_thread(_TrackTask, "menu_tracking", B_DISPLAY_PRIORITY, NULL);
396 	if (fTrackingPID >= 0) {
397 		menubar_data data;
398 		data.menuBar = this;
399 		data.menuIndex = menuIndex;
400 		data.sticky = sticky;
401 		data.showMenu = showMenu;
402 		data.useRect = specialRect != NULL;
403 		if (data.useRect)
404 			data.rect = *specialRect;
405 
406 		resume_thread(fTrackingPID);
407 		send_data(fTrackingPID, 0, &data, sizeof(data));
408 
409 	} else {
410 		fTracking = false;
411 		_set_menu_sem_(window, B_NO_MORE_SEMS);
412 		delete_sem(fMenuSem);
413 	}
414 }
415 
416 
417 /* static */
418 int32
419 BMenuBar::_TrackTask(void *arg)
420 {
421 	menubar_data data;
422 	thread_id id;
423 	receive_data(&id, &data, sizeof(data));
424 
425 	BMenuBar *menuBar = data.menuBar;
426 	if (data.useRect)
427 		menuBar->fExtraRect = &data.rect;
428 	menuBar->_SetStickyMode(data.sticky);
429 
430 	int32 action;
431 	menuBar->_Track(&action, data.menuIndex, data.showMenu);
432 
433 	menuBar->fTracking = false;
434 	menuBar->fExtraRect = NULL;
435 
436 	// We aren't the BWindow thread, so don't call MenusEnded() directly
437 	BWindow *window = menuBar->Window();
438 	window->PostMessage(_MENUS_DONE_);
439 
440 	_set_menu_sem_(window, B_BAD_SEM_ID);
441 	delete_sem(menuBar->fMenuSem);
442 	menuBar->fMenuSem = B_BAD_SEM_ID;
443 
444 	return 0;
445 }
446 
447 
448 BMenuItem *
449 BMenuBar::_Track(int32 *action, int32 startIndex, bool showMenu)
450 {
451 	// TODO: Cleanup, merge some "if" blocks if possible
452 	fChosenItem = NULL;
453 
454 	BWindow *window = Window();
455 	fState = MENU_STATE_TRACKING;
456 
457 	BPoint where;
458 	uint32 buttons;
459 	if (window->Lock()) {
460 		if (startIndex != -1) {
461 			be_app->ObscureCursor();
462 			_SelectItem(ItemAt(startIndex), true, true);
463 		}
464 		GetMouse(&where, &buttons);
465 		window->Unlock();
466 	}
467 
468 	while (fState != MENU_STATE_CLOSED) {
469 		bigtime_t snoozeAmount = 40000;
470 		if (Window() == NULL || !window->Lock())
471 			break;
472 
473 		BMenuItem *menuItem = _HitTestItems(where, B_ORIGIN);
474 		if (menuItem != NULL) {
475 			// Select item if:
476 			// - no previous selection
477 			// - nonsticky mode and different selection,
478 			// - clicked in sticky mode
479 			if (fSelected == NULL
480 				|| (!_IsStickyMode() && menuItem != fSelected)
481 				|| (buttons != 0 && _IsStickyMode())) {
482 				if (menuItem->Submenu() != NULL) {
483 					if (menuItem->Submenu()->Window() == NULL) {
484 						// open the menu if it's not opened yet
485 						_SelectItem(menuItem);
486 						if (_IsStickyMode())
487 							_SetStickyMode(false);
488 					} else {
489 						// Menu was already opened, close it and bail
490 						_SelectItem(NULL);
491 						fState = MENU_STATE_CLOSED;
492 						fChosenItem = NULL;
493 					}
494 				} else {
495 					// No submenu, just select the item
496 					_SelectItem(menuItem);
497 				}
498 			}
499 		} else if (_OverSubmenu(fSelected, ConvertToScreen(where))) {
500 			// call _Track() from the selected sub-menu when the mouse cursor
501 			// is over its window
502 			BMenu *menu = fSelected->Submenu();
503 			window->Unlock();
504 			snoozeAmount = 30000;
505 			bool wasSticky = _IsStickyMode();
506 			menu->_SetStickyMode(wasSticky);
507 			int localAction;
508 			fChosenItem = menu->_Track(&localAction);
509 
510 			// The mouse could have meen moved since the last time we
511 			// checked its position, or buttons might have been pressed.
512 			// Unfortunately our child menus don't tell
513 			// us the new position.
514 			// TODO: Maybe have a shared struct between all menus
515 			// where to store the current mouse position ?
516 			// (Or just use the BView mouse hooks)
517 			BPoint newWhere;
518 			if (window->Lock()) {
519 				GetMouse(&newWhere, &buttons);
520 				window->Unlock();
521 			}
522 
523 			// This code is needed to make menus
524 			// that are children of BMenuFields "sticky" (see ticket #953)
525 			if (localAction == MENU_STATE_CLOSED) {
526 				if (fExtraRect != NULL && fExtraRect->Contains(where)
527 					// 9 = 3 pixels ^ 2 (since point_distance() returns the square of the distance)
528 					&& point_distance(newWhere, where) < 9) {
529 					_SetStickyMode(true);
530 					fExtraRect = NULL;
531 				} else
532 					fState = MENU_STATE_CLOSED;
533 			}
534 			if (!window->Lock())
535 				break;
536 		} else if (menuItem == NULL && fSelected != NULL
537 			&& !_IsStickyMode() && Bounds().Contains(where)) {
538 			_SelectItem(NULL);
539 			fState = MENU_STATE_TRACKING;
540 		}
541 
542 		window->Unlock();
543 
544 		if (fState != MENU_STATE_CLOSED) {
545 			if (snoozeAmount > 0)
546 				snooze(snoozeAmount);
547 
548 			if (window->Lock()) {
549 				GetMouse(&where, &buttons, true);
550 				window->Unlock();
551 			}
552 
553 			if ((buttons != 0 && _IsStickyMode() && menuItem == NULL))
554 				fState = MENU_STATE_CLOSED;
555 			else if (buttons == 0 && !_IsStickyMode()) {
556 				if ((fSelected != NULL && fSelected->Submenu() == NULL)
557 					|| menuItem == NULL) {
558 					fChosenItem = fSelected;
559 					fState = MENU_STATE_CLOSED;
560 				} else
561 					_SetStickyMode(true);
562 			}
563 		}
564 	}
565 
566 	if (window->Lock()) {
567 		if (fSelected != NULL)
568 			_SelectItem(NULL);
569 
570 		if (fChosenItem != NULL)
571 			fChosenItem->Invoke();
572 		_RestoreFocus();
573 		window->Unlock();
574 	}
575 
576 	if (_IsStickyMode())
577 		_SetStickyMode(false);
578 
579 	_DeleteMenuWindow();
580 
581 	if (action != NULL)
582 		*action = fState;
583 
584 	return fChosenItem;
585 }
586 
587 
588 void
589 BMenuBar::_StealFocus()
590 {
591 	// We already stole the focus, don't do anything
592 	if (fPrevFocusToken != -1)
593 		return;
594 
595 	BWindow *window = Window();
596 	if (window != NULL && window->Lock()) {
597 		BView *focus = window->CurrentFocus();
598 		if (focus != NULL && focus != this)
599 			fPrevFocusToken = _get_object_token_(focus);
600 		MakeFocus();
601 		window->Unlock();
602 	}
603 }
604 
605 
606 void
607 BMenuBar::_RestoreFocus()
608 {
609 	BWindow *window = Window();
610 	if (window != NULL && window->Lock()) {
611 		BHandler *handler = NULL;
612 		if (fPrevFocusToken != -1
613 			&& gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN, (void **)&handler) == B_OK) {
614 			BView *view = dynamic_cast<BView *>(handler);
615 			if (view != NULL && view->Window() == window)
616 				view->MakeFocus();
617 
618 		} else if (IsFocus())
619 			MakeFocus(false);
620 
621 		fPrevFocusToken = -1;
622 		window->Unlock();
623 	}
624 }
625 
626 
627 void
628 BMenuBar::_InitData(menu_layout layout)
629 {
630 	fLastBounds = new BRect(Bounds());
631 	SetItemMargins(8, 2, 8, 2);
632 	_SetIgnoreHidden(true);
633 }
634