xref: /haiku/src/kits/interface/Alert.cpp (revision 9ecf9d1c1d4888d341a6eac72112c72d1ae3a4cb)
1 /*
2  * Copyright 2001-2006, Haiku.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		Erik Jaesler (erik@cgsoftware.com)
7  *		Axel Dörfler, axeld@pinc-software.de
8  */
9 
10 //!	BAlert displays a modal alert window.
11 
12 #include <Alert.h>
13 #include <Autolock.h>
14 #include <Beep.h>
15 #include <Bitmap.h>
16 #include <Button.h>
17 #include <File.h>
18 #include <FindDirectory.h>
19 #include <Invoker.h>
20 #include <Looper.h>
21 #include <Message.h>
22 #include <MessageFilter.h>
23 #include <Path.h>
24 #include <Resources.h>
25 #include <Screen.h>
26 #include <TextView.h>
27 #include <View.h>
28 
29 #include <new>
30 #include <stdio.h>
31 #include <string.h>
32 
33 //#define DEBUG_ALERT
34 #ifdef DEBUG_ALERT
35 #	define FTRACE(x) fprintf(x)
36 #else
37 #	define FTRACE(x) /* nothing */
38 #endif
39 
40 // Default size of the Alert window.
41 #define DEFAULT_RECT	BRect(0, 0, 310, 75)
42 #define max(LHS, RHS)	((LHS) > (RHS) ? (LHS) : (RHS))
43 
44 static const unsigned int kAlertButtonMsg = 'ALTB';
45 static const int kSemTimeOut = 50000;
46 
47 static const int kLeftOffset = 10;
48 static const int kTopOffset = 6;
49 static const int kBottomOffset = 8;
50 static const int kRightOffset = 8;
51 
52 static const int kButtonSpacing = 6;
53 static const int kButtonOffsetSpacing = 62;
54 static const int kButtonUsualWidth = 75;
55 
56 static const int kWindowIconOffset = 27;
57 static const int kWindowMinOffset = 12;
58 static const int kWindowMinWidth = 310;
59 static const int kWindowOffsetMinWidth = 335;
60 
61 static const int kIconStripeWidth = 30;
62 
63 static const int kTextIconOffset = kWindowIconOffset + kIconStripeWidth - 2;
64 static const int kTextButtonOffset = 10;
65 
66 
67 class TAlertView : public BView {
68 	public:
69 		TAlertView(BRect frame);
70 		TAlertView(BMessage* archive);
71 		~TAlertView();
72 
73 		static TAlertView*	Instantiate(BMessage* archive);
74 		status_t			Archive(BMessage* archive, bool deep = true);
75 
76 		virtual void	Draw(BRect updateRect);
77 
78 		// These functions (or something analogous) are missing from libbe.so's
79 		// dump.  I can only assume that the bitmap is a public var in the
80 		// original implementation -- or BAlert is a friend of TAlertView.
81 		// Neither one is necessary, since I can just add these.
82 		void			SetBitmap(BBitmap* Icon)	{ fIconBitmap = Icon; }
83 		BBitmap*		Bitmap()					{ return fIconBitmap; }
84 
85 	private:
86 		BBitmap*	fIconBitmap;
87 };
88 
89 class _BAlertFilter_ : public BMessageFilter {
90 	public:
91 		_BAlertFilter_(BAlert* Alert);
92 		~_BAlertFilter_();
93 
94 		virtual filter_result Filter(BMessage* msg, BHandler** target);
95 
96 	private:
97 		BAlert*	fAlert;
98 };
99 
100 
101 //	#pragma mark - BAlert
102 
103 
104 BAlert::BAlert(const char *title, const char *text, const char *button1,
105 		const char *button2, const char *button3, button_width width,
106 		alert_type type)
107 	: BWindow(DEFAULT_RECT, title, B_MODAL_WINDOW,
108 		B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
109 {
110 	_InitObject(text, button1, button2, button3, width, B_EVEN_SPACING, type);
111 }
112 
113 
114 BAlert::BAlert(const char *title, const char *text, const char *button1,
115 		const char *button2, const char *button3, button_width width,
116 		button_spacing spacing, alert_type type)
117 	: BWindow(DEFAULT_RECT, title, B_MODAL_WINDOW,
118 		B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
119 {
120 	_InitObject(text, button1, button2, button3, width, spacing, type);
121 }
122 
123 
124 BAlert::~BAlert()
125 {
126 	// Probably not necessary, but it makes me feel better.
127 	if (fAlertSem >= B_OK)
128 		delete_sem(fAlertSem);
129 }
130 
131 
132 BAlert::BAlert(BMessage* data)
133 	: BWindow(data)
134 {
135 	fInvoker = NULL;
136 	fAlertSem = -1;
137 	fAlertValue = -1;
138 
139 	fTextView = (BTextView*)FindView("_tv_");
140 
141 	fButtons[0] = (BButton*)FindView("_b0_");
142 	fButtons[1] = (BButton*)FindView("_b1_");
143 	fButtons[2] = (BButton*)FindView("_b2_");
144 
145 	if (fButtons[2])
146 		SetDefaultButton(fButtons[2]);
147 	else if (fButtons[1])
148 		SetDefaultButton(fButtons[1]);
149 	else if (fButtons[0])
150 		SetDefaultButton(fButtons[0]);
151 
152 	TAlertView* view = (TAlertView*)FindView("_master_");
153 	if (view)
154 		view->SetBitmap(_InitIcon());
155 
156 	// Get keys
157 	char key;
158 	for (int32 i = 0; i < 3; ++i) {
159 		if (data->FindInt8("_but_key", i, (int8*)&key) == B_OK)
160 			fKeys[i] = key;
161 	}
162 
163 	int32 temp;
164 	// Get alert type
165 	if (data->FindInt32("_atype", &temp) == B_OK)
166 		fMsgType = (alert_type)temp;
167 
168 	// Get button width
169 	if (data->FindInt32("_but_width", &temp) == B_OK)
170 		fButtonWidth = (button_width)temp;
171 
172 	AddCommonFilter(new _BAlertFilter_(this));
173 }
174 
175 
176 BArchivable*
177 BAlert::Instantiate(BMessage* data)
178 {
179 	if (!validate_instantiation(data, "BAlert"))
180 		return NULL;
181 
182 	return new BAlert(data);
183 }
184 
185 
186 status_t
187 BAlert::Archive(BMessage* data, bool deep) const
188 {
189 	status_t ret = BWindow::Archive(data, deep);
190 
191 	// Stow the text
192 	if (ret == B_OK)
193 		ret = data->AddString("_text", fTextView->Text());
194 
195 	// Stow the alert type
196 	if (ret == B_OK)
197 		ret = data->AddInt32("_atype", fMsgType);
198 
199 	// Stow the button width
200 	if (ret == B_OK)
201 		ret = data->AddInt32("_but_width", fButtonWidth);
202 
203 	// Stow the shortcut keys
204 	if (fKeys[0] || fKeys[1] || fKeys[2]) {
205 		// If we have any to save, we must save something for everyone so it
206 		// doesn't get confusing on the unarchive.
207 		if (ret == B_OK)
208 			ret = data->AddInt8("_but_key", fKeys[0]);
209 		if (ret == B_OK)
210 			ret = data->AddInt8("_but_key", fKeys[1]);
211 		if (ret == B_OK)
212 			ret = data->AddInt8("_but_key", fKeys[2]);
213 	}
214 
215 	return ret;
216 }
217 
218 
219 void
220 BAlert::SetShortcut(int32 index, char key)
221 {
222 	if (index >= 0 && index < 3)
223 		fKeys[index] = key;
224 }
225 
226 
227 char
228 BAlert::Shortcut(int32 index) const
229 {
230 	if (index >= 0 && index < 3)
231 		return fKeys[index];
232 
233 	return 0;
234 }
235 
236 
237 int32
238 BAlert::Go()
239 {
240 	fAlertSem = create_sem(0, "AlertSem");
241 	if (fAlertSem < B_OK) {
242 		Quit();
243 		return -1;
244 	}
245 
246 	// Get the originating window, if it exists
247 	BWindow* window =
248 		dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
249 
250 	Show();
251 
252 	// Heavily modified from TextEntryAlert code; the original didn't let the
253 	// blocked window ever draw.
254 	if (window) {
255 		status_t err;
256 		for (;;) {
257 			do {
258 				err = acquire_sem_etc(fAlertSem, 1, B_RELATIVE_TIMEOUT,
259 									  kSemTimeOut);
260 				// We've (probably) had our time slice taken away from us
261 			} while (err == B_INTERRUPTED);
262 
263 			if (err == B_BAD_SEM_ID) {
264 				// Semaphore was finally nuked in MessageReceived
265 				break;
266 			}
267 			window->UpdateIfNeeded();
268 		}
269 	} else {
270 		// No window to update, so just hang out until we're done.
271 		while (acquire_sem(fAlertSem) == B_INTERRUPTED) {
272 		}
273 	}
274 
275 	// Have to cache the value since we delete on Quit()
276 	int32 value = fAlertValue;
277 	if (Lock())
278 		Quit();
279 
280 	return value;
281 }
282 
283 
284 status_t
285 BAlert::Go(BInvoker* invoker)
286 {
287 	fInvoker = invoker;
288 	Show();
289 	return B_OK;
290 }
291 
292 
293 void
294 BAlert::MessageReceived(BMessage* msg)
295 {
296 	if (msg->what != kAlertButtonMsg)
297 		return BWindow::MessageReceived(msg);
298 
299 	int32 which;
300 	if (msg->FindInt32("which", &which) == B_OK) {
301 		if (fAlertSem < B_OK) {
302 			// Semaphore hasn't been created; we're running asynchronous
303 			if (fInvoker) {
304 				BMessage* out = fInvoker->Message();
305 				if (out && (out->ReplaceInt32("which", which) == B_OK
306 							|| out->AddInt32("which", which) == B_OK))
307 					fInvoker->Invoke();
308 			}
309 			PostMessage(B_QUIT_REQUESTED);
310 		} else {
311 			// Created semaphore means were running synchronously
312 			fAlertValue = which;
313 
314 			// TextAlertVar does release_sem() below, and then sets the
315 			// member var.  That doesn't make much sense to me, since we
316 			// want to be able to clean up at some point.  Better to just
317 			// nuke the semaphore now; we don't need it any more and this
318 			// lets synchronous Go() continue just as well.
319 			delete_sem(fAlertSem);
320 			fAlertSem = -1;
321 		}
322 	}
323 }
324 
325 
326 void
327 BAlert::FrameResized(float newWidth, float newHeight)
328 {
329 	BWindow::FrameResized(newWidth, newHeight);
330 }
331 
332 
333 BButton*
334 BAlert::ButtonAt(int32 index) const
335 {
336 	if (index >= 0 && index < 3)
337 		return fButtons[index];
338 
339 	return NULL;
340 }
341 
342 
343 BTextView*
344 BAlert::TextView() const
345 {
346 	return fTextView;
347 }
348 
349 
350 BHandler*
351 BAlert::ResolveSpecifier(BMessage* msg, int32 index,
352 	BMessage* specifier, int32 form, const char* property)
353 {
354 	return BWindow::ResolveSpecifier(msg, index, specifier, form, property);
355 }
356 
357 
358 status_t
359 BAlert::GetSupportedSuites(BMessage* data)
360 {
361 	return BWindow::GetSupportedSuites(data);
362 }
363 
364 
365 void
366 BAlert::DispatchMessage(BMessage* msg, BHandler* handler)
367 {
368 	BWindow::DispatchMessage(msg, handler);
369 }
370 
371 
372 void
373 BAlert::Quit()
374 {
375 	BWindow::Quit();
376 }
377 
378 
379 bool
380 BAlert::QuitRequested()
381 {
382 	return BWindow::QuitRequested();
383 }
384 
385 
386 BPoint
387 BAlert::AlertPosition(float width, float height)
388 {
389 	BPoint result(100, 100);
390 
391 	BWindow* window =
392 		dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
393 
394 	BScreen screen(window);
395  	BRect screenFrame(0, 0, 640, 480);
396  	if (screen.IsValid())
397  		screenFrame = screen.Frame();
398 
399 	// Horizontally, we're smack in the middle
400 	result.x = screenFrame.left + (screenFrame.Width() / 2.0) - (width / 2.0);
401 
402 	// This is probably sooo wrong, but it looks right on 1024 x 768
403 	result.y = screenFrame.top + (screenFrame.Height() / 4.0) - ceil(height / 3.0);
404 
405 	return result;
406 }
407 
408 
409 status_t
410 BAlert::Perform(perform_code d, void* arg)
411 {
412 	return BWindow::Perform(d, arg);
413 }
414 
415 
416 void BAlert::_ReservedAlert1() {}
417 void BAlert::_ReservedAlert2() {}
418 void BAlert::_ReservedAlert3() {}
419 
420 
421 void
422 BAlert::_InitObject(const char* text, const char* button0, const char* button1,
423 	const char* button2, button_width buttonWidth, button_spacing spacing,
424 	alert_type type)
425 {
426 	fInvoker = NULL;
427 	fAlertSem = -1;
428 	fAlertValue = -1;
429 	fButtons[0] = fButtons[1] = fButtons[2] = NULL;
430 	fTextView = NULL;
431 	fKeys[0] = fKeys[1] = fKeys[2] = 0;
432 	fMsgType = type;
433 	fButtonWidth = buttonWidth;
434 
435 	// Set up the "_master_" view
436 	TAlertView* view = new TAlertView(Bounds());
437 	AddChild(view);
438 	view->SetBitmap(_InitIcon());
439 
440 	// Must have at least one button
441 	if (button0 == NULL) {
442 		debugger("BAlert's must have at least one button.");
443 		button0 = "";
444 	}
445 
446 	// Set up the buttons
447 
448 	int32 buttonCount = 1;
449 	view->AddChild(fButtons[0] = _CreateButton(0, button0));
450 
451 	if (button1 != NULL) {
452 		view->AddChild(fButtons[1] = _CreateButton(1, button1));
453 		buttonCount++;
454 	}
455 
456 	if (button2 != NULL) {
457 		view->AddChild(fButtons[2] = _CreateButton(2, button2));
458 		buttonCount++;
459 	}
460 
461 	// Find the widest button only if the widest value needs to be known.
462 
463 	if (fButtonWidth == B_WIDTH_FROM_WIDEST) {
464 		float maxWidth = 0;
465 		for (int i = 0; i < buttonCount; i++) {
466 			float width = fButtons[i]->Bounds().Width();
467 			if (width > maxWidth)
468 				maxWidth = width;
469 		}
470 
471 		// resize buttons
472 		for (int i = 0; i < buttonCount; i++) {
473 			fButtons[i]->ResizeTo(maxWidth, fButtons[i]->Bounds().Height());
474 		}
475 	}
476 
477 	float defaultButtonFrameWidth = -fButtons[buttonCount - 1]->Bounds().Width() / 2.0f;
478 	SetDefaultButton(fButtons[buttonCount - 1]);
479 	defaultButtonFrameWidth += fButtons[buttonCount - 1]->Bounds().Width() / 2.0f;
480 
481 	// Layout buttons
482 
483 	float fontFactor = be_plain_font->Size() / 11.0f;
484 
485 	for (int i = buttonCount - 1; i >= 0; --i) {
486 		float x = -fButtons[i]->Bounds().Width();
487 		if (i + 1 == buttonCount)
488 			x += Bounds().right - kRightOffset + defaultButtonFrameWidth;
489 		else
490 			x += fButtons[i + 1]->Frame().left - kButtonSpacing;
491 
492 		if (buttonCount > 1 && i == 0 && spacing == B_OFFSET_SPACING)
493 			x -= kButtonOffsetSpacing * fontFactor;
494 
495 		fButtons[i]->MoveTo(x, fButtons[i]->Frame().top);
496 	}
497 
498 	// Adjust the window's width, if necessary
499 
500 	float totalWidth = kRightOffset + fButtons[buttonCount - 1]->Frame().right
501 		- defaultButtonFrameWidth - fButtons[0]->Frame().left;
502 	if (view->Bitmap())
503 		totalWidth += kIconStripeWidth + kWindowIconOffset;
504 	else
505 		totalWidth += kWindowMinOffset;
506 
507 	float width = (spacing == B_OFFSET_SPACING
508 		? kWindowOffsetMinWidth : kWindowMinWidth) * fontFactor;
509 
510 	ResizeTo(max_c(totalWidth, width), Bounds().Height());
511 
512 	// Set up the text view
513 
514 	BRect textViewRect(kLeftOffset, kTopOffset,
515 		Bounds().right - kRightOffset,
516 		fButtons[0]->Frame().top - kTextButtonOffset);
517 	if (view->Bitmap())
518 		textViewRect.left = kTextIconOffset;
519 
520 	fTextView = new BTextView(textViewRect, "_tv_",
521 		textViewRect.OffsetByCopy(B_ORIGIN),
522 		B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW);
523 	fTextView->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
524 	fTextView->SetText(text, strlen(text));
525 	fTextView->MakeEditable(false);
526 	fTextView->MakeSelectable(false);
527 	fTextView->SetWordWrap(true);
528 	view->AddChild(fTextView);
529 
530 	// Now resize the TextView vertically so that all the text is visible
531 	float textHeight = fTextView->TextHeight(0, fTextView->CountLines());
532 	textViewRect.OffsetTo(0, 0);
533 	textHeight -= textViewRect.Height();
534 	ResizeBy(0, textHeight);
535 	fTextView->ResizeBy(0, textHeight);
536 	textViewRect.bottom += textHeight;
537 	fTextView->SetTextRect(textViewRect);
538 
539 	AddCommonFilter(new _BAlertFilter_(this));
540 
541 	MoveTo(AlertPosition(Frame().Width(), Frame().Height()));
542 }
543 
544 
545 BBitmap*
546 BAlert::_InitIcon()
547 {
548 	// After a bit of a search, I found the icons in app_server. =P
549 	BBitmap* icon = NULL;
550 	BPath path;
551 	status_t status = find_directory(B_BEOS_SERVERS_DIRECTORY, &path);
552 	if (status >= B_OK) {
553 		path.Append("app_server");
554 		BFile file;
555 		status = file.SetTo(path.Path(), B_READ_ONLY);
556 		if (status >= B_OK) {
557 			BResources resources;
558 			status = resources.SetTo(&file);
559 			if (status >= B_OK) {
560 				// Which icon are we trying to load?
561 				const char* iconName = "";	// Don't want any seg faults
562 				switch (fMsgType) {
563 					case B_INFO_ALERT:
564 						iconName = "info";
565 						break;
566 					case B_IDEA_ALERT:
567 						iconName = "idea";
568 						break;
569 					case B_WARNING_ALERT:
570 						iconName = "warn";
571 						break;
572 					case B_STOP_ALERT:
573 						iconName = "stop";
574 						break;
575 
576 					default:
577 						// Alert type is either invalid or B_EMPTY_ALERT;
578 						// either way, we're not going to load an icon
579 						return NULL;
580 				}
581 
582 				// Load the raw icon data
583 				size_t size;
584 				const void* rawIcon =
585 					resources.LoadResource('ICON', iconName, &size);
586 
587 				if (rawIcon) {
588 					// Now build the bitmap
589 					icon = new (std::nothrow) BBitmap(BRect(0, 0, 31, 31), 0, B_CMAP8);
590 					if (icon != NULL)
591 						icon->SetBits(rawIcon, size, 0, B_CMAP8);
592 				} else {
593 					FTRACE((stderr, "BAlert::InitIcon() - Icon resource not found\n"));
594 				}
595 			} else {
596 				FTRACE((stderr, "BAlert::InitIcon() - BResources init failed: %s\n", strerror(status)));
597 			}
598 		} else {
599 			FTRACE((stderr, "BAlert::InitIcon() - BFile init failed: %s\n", strerror(status)));
600 		}
601 	} else {
602 		FTRACE((stderr, "BAlert::InitIcon() - find_directory failed: %s\n", strerror(status)));
603 	}
604 
605 	if (icon == NULL) {
606 		// If there's no icon, it's an empty alert indeed.
607 		fMsgType = B_EMPTY_ALERT;
608 	}
609 
610 	return icon;
611 }
612 
613 
614 BButton*
615 BAlert::_CreateButton(int32 which, const char* label)
616 {
617 	BMessage* message = new BMessage(kAlertButtonMsg);
618 	if (message == NULL)
619 		return NULL;
620 
621 	message->AddInt32("which", which);
622 
623 	BRect rect;
624 	rect.top = Bounds().bottom - kBottomOffset;
625 	rect.bottom = rect.top;
626 
627 	char name[32];
628 	snprintf(name, sizeof(name), "_b%ld_", which);
629 
630 	BButton* button = new (std::nothrow) BButton(rect, name, label, message,
631 		B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM);
632 	if (button == NULL)
633 		return NULL;
634 
635 	float width, height;
636 	button->GetPreferredSize(&width, &height);
637 
638 	if (fButtonWidth == B_WIDTH_AS_USUAL) {
639 		float fontFactor = be_plain_font->Size() / 11.0f;
640 		width = max_c(width, kButtonUsualWidth * fontFactor);
641 	}
642 
643 	button->ResizeTo(width, height);
644 	button->MoveBy(0.0f, -height);
645 	return button;
646 }
647 
648 
649 //	#pragma mark - TAlertView
650 
651 
652 TAlertView::TAlertView(BRect frame)
653 	:	BView(frame, "TAlertView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW),
654 		fIconBitmap(NULL)
655 {
656 	SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR));
657 }
658 
659 
660 TAlertView::TAlertView(BMessage* archive)
661 	:	BView(archive),
662 		fIconBitmap(NULL)
663 {
664 }
665 
666 
667 TAlertView::~TAlertView()
668 {
669 	delete fIconBitmap;
670 }
671 
672 
673 TAlertView*
674 TAlertView::Instantiate(BMessage* archive)
675 {
676 	if (!validate_instantiation(archive, "TAlertView"))
677 		return NULL;
678 
679 	return new TAlertView(archive);
680 }
681 
682 
683 status_t
684 TAlertView::Archive(BMessage* archive, bool deep)
685 {
686 	return BView::Archive(archive, deep);
687 }
688 
689 
690 void
691 TAlertView::Draw(BRect updateRect)
692 {
693 	// Here's the fun stuff
694 	if (fIconBitmap) {
695 		BRect stripeRect = Bounds();
696 		stripeRect.right = kIconStripeWidth;
697 		SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
698 		FillRect(stripeRect);
699 
700 		SetDrawingMode(B_OP_OVER);
701 		DrawBitmapAsync(fIconBitmap, BPoint(18, 6));
702 		SetDrawingMode(B_OP_COPY);
703 	}
704 }
705 
706 
707 //------------------------------------------------------------------------------
708 //	#pragma mark - _BAlertFilter_
709 
710 
711 _BAlertFilter_::_BAlertFilter_(BAlert* alert)
712 	: BMessageFilter(B_KEY_DOWN),
713 	fAlert(alert)
714 {
715 }
716 
717 
718 _BAlertFilter_::~_BAlertFilter_()
719 {
720 }
721 
722 
723 filter_result
724 _BAlertFilter_::Filter(BMessage* msg, BHandler** target)
725 {
726 	if (msg->what == B_KEY_DOWN) {
727 		char byte;
728 		if (msg->FindInt8("byte", (int8*)&byte) == B_OK) {
729 			for (int i = 0; i < 3; ++i) {
730 				if (byte == fAlert->Shortcut(i) && fAlert->ButtonAt(i)) {
731 					char space = ' ';
732 					fAlert->ButtonAt(i)->KeyDown(&space, 1);
733 
734 					return B_SKIP_MESSAGE;
735 				}
736 			}
737 		}
738 	}
739 
740 	return B_DISPATCH_MESSAGE;
741 }
742 
743