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