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