1/*
2 * Copyright 2001-2015 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Axel D��rfler, axeld@pinc-software.de
7 *		Erik Jaesler, erik@cgsoftware.com
8 *		John Scipione, jscipione@gmail.com
9 */
10
11
12//!	BAlert displays a modal alert window.
13
14
15#include <Alert.h>
16
17#include <new>
18
19#include <stdio.h>
20
21#include <Bitmap.h>
22#include <Button.h>
23#include <ControlLook.h>
24#include <Debug.h>
25#include <FindDirectory.h>
26#include <IconUtils.h>
27#include <LayoutBuilder.h>
28#include <MenuField.h>
29#include <MessageFilter.h>
30#include <Path.h>
31#include <Resources.h>
32#include <Screen.h>
33#include <String.h>
34#include <Window.h>
35
36#include <binary_compatibility/Interface.h>
37
38
39//#define DEBUG_ALERT
40#ifdef DEBUG_ALERT
41#	define FTRACE(x) fprintf(x)
42#else
43#	define FTRACE(x) ;
44#endif
45
46
47class TAlertView : public BView {
48public:
49								TAlertView();
50								TAlertView(BMessage* archive);
51								~TAlertView();
52
53	static	TAlertView*			Instantiate(BMessage* archive);
54	virtual	status_t			Archive(BMessage* archive,
55									bool deep = true) const;
56
57	virtual	void				GetPreferredSize(float* _width, float* _height);
58	virtual	BSize				MaxSize();
59	virtual	void				Draw(BRect updateRect);
60
61			void				SetBitmap(BBitmap* icon);
62			BBitmap*			Bitmap()
63									{ return fIconBitmap; }
64
65private:
66			BBitmap*			fIconBitmap;
67};
68
69
70class _BAlertFilter_ : public BMessageFilter {
71public:
72								_BAlertFilter_(BAlert* Alert);
73								~_BAlertFilter_();
74
75	virtual	filter_result		Filter(BMessage* msg, BHandler** target);
76
77private:
78			BAlert*				fAlert;
79};
80
81
82static const unsigned int kAlertButtonMsg = 'ALTB';
83static const int kSemTimeOut = 50000;
84
85static const int kButtonOffsetSpacing = 62;
86static const int kButtonUsualWidth = 55;
87static const int kIconStripeWidthFactor = 5;
88
89static const int kWindowMinWidth = 310;
90static const int kWindowOffsetMinWidth = 335;
91
92
93// #pragma mark -
94
95
96BAlert::BAlert()
97	:
98	BWindow(BRect(0, 0, 100, 100), "", B_MODAL_WINDOW,
99		B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
100{
101	_Init(NULL, NULL, NULL, NULL, B_WIDTH_FROM_WIDEST, B_EVEN_SPACING,
102		B_INFO_ALERT);
103}
104
105
106BAlert::BAlert(const char *title, const char *text, const char *button1,
107		const char *button2, const char *button3, button_width width,
108		alert_type type)
109	:
110	BWindow(BRect(0, 0, 100, 100), title, B_MODAL_WINDOW,
111		B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
112{
113	_Init(text, button1, button2, button3, width, B_EVEN_SPACING, type);
114}
115
116
117BAlert::BAlert(const char *title, const char *text, const char *button1,
118		const char *button2, const char *button3, button_width width,
119		button_spacing spacing, alert_type type)
120	:
121	BWindow(BRect(0, 0, 100, 100), title, B_MODAL_WINDOW,
122		B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
123{
124	_Init(text, button1, button2, button3, width, spacing, type);
125}
126
127
128BAlert::BAlert(BMessage* data)
129	:
130	BWindow(data)
131{
132	fInvoker = NULL;
133	fAlertSem = -1;
134	fAlertValue = -1;
135
136	fTextView = (BTextView*)FindView("_tv_");
137
138	// TODO: window loses default button on dearchive!
139	// TODO: ButtonAt() doesn't work afterwards (also affects shortcuts)
140
141	TAlertView* view = (TAlertView*)FindView("_master_");
142	if (view)
143		view->SetBitmap(_CreateTypeIcon());
144
145	// Get keys
146	char key;
147	for (int32 i = 0; i < 3; ++i) {
148		if (data->FindInt8("_but_key", i, (int8*)&key) == B_OK)
149			fKeys[i] = key;
150	}
151
152	AddCommonFilter(new(std::nothrow) _BAlertFilter_(this));
153}
154
155
156BAlert::~BAlert()
157{
158	// Probably not necessary, but it makes me feel better.
159	if (fAlertSem >= B_OK)
160		delete_sem(fAlertSem);
161}
162
163
164BArchivable*
165BAlert::Instantiate(BMessage* data)
166{
167	if (!validate_instantiation(data, "BAlert"))
168		return NULL;
169
170	return new(std::nothrow) BAlert(data);
171}
172
173
174status_t
175BAlert::Archive(BMessage* data, bool deep) const
176{
177	status_t ret = BWindow::Archive(data, deep);
178
179	// Stow the text
180	if (ret == B_OK)
181		ret = data->AddString("_text", fTextView->Text());
182
183	// Stow the alert type
184	if (ret == B_OK)
185		ret = data->AddInt32("_atype", fType);
186
187	// Stow the button width
188	if (ret == B_OK)
189		ret = data->AddInt32("_but_width", fButtonWidth);
190
191	// Stow the shortcut keys
192	if (fKeys[0] || fKeys[1] || fKeys[2]) {
193		// If we have any to save, we must save something for everyone so it
194		// doesn't get confusing on the unarchive.
195		if (ret == B_OK)
196			ret = data->AddInt8("_but_key", fKeys[0]);
197		if (ret == B_OK)
198			ret = data->AddInt8("_but_key", fKeys[1]);
199		if (ret == B_OK)
200			ret = data->AddInt8("_but_key", fKeys[2]);
201	}
202
203	return ret;
204}
205
206
207alert_type
208BAlert::Type() const
209{
210	return (alert_type)fType;
211}
212
213
214void
215BAlert::SetType(alert_type type)
216{
217	fType = type;
218}
219
220
221void
222BAlert::SetText(const char* text)
223{
224	TextView()->SetText(text);
225}
226
227
228void
229BAlert::SetIcon(BBitmap* bitmap)
230{
231	fIconView->SetBitmap(bitmap);
232}
233
234
235void
236BAlert::SetButtonSpacing(button_spacing spacing)
237{
238	fButtonSpacing = spacing;
239}
240
241
242void
243BAlert::SetButtonWidth(button_width width)
244{
245	fButtonWidth = width;
246}
247
248
249void
250BAlert::SetShortcut(int32 index, char key)
251{
252	if (index >= 0 && (size_t)index < fKeys.size())
253		fKeys[index] = key;
254}
255
256
257char
258BAlert::Shortcut(int32 index) const
259{
260	if (index >= 0 && (size_t)index < fKeys.size())
261		return fKeys[index];
262
263	return 0;
264}
265
266
267int32
268BAlert::Go()
269{
270	fAlertSem = create_sem(0, "AlertSem");
271	if (fAlertSem < 0) {
272		Quit();
273		return -1;
274	}
275
276	// Get the originating window, if it exists
277	BWindow* window = dynamic_cast<BWindow*>(
278		BLooper::LooperForThread(find_thread(NULL)));
279
280	_Prepare();
281	Show();
282
283	if (window != NULL) {
284		status_t status;
285		for (;;) {
286			do {
287				status = acquire_sem_etc(fAlertSem, 1, B_RELATIVE_TIMEOUT,
288					kSemTimeOut);
289				// We've (probably) had our time slice taken away from us
290			} while (status == B_INTERRUPTED);
291
292			if (status == B_BAD_SEM_ID) {
293				// Semaphore was finally nuked in MessageReceived
294				break;
295			}
296			window->UpdateIfNeeded();
297		}
298	} else {
299		// No window to update, so just hang out until we're done.
300		while (acquire_sem(fAlertSem) == B_INTERRUPTED) {
301		}
302	}
303
304	// Have to cache the value since we delete on Quit()
305	int32 value = fAlertValue;
306	if (Lock())
307		Quit();
308
309	return value;
310}
311
312
313status_t
314BAlert::Go(BInvoker* invoker)
315{
316	fInvoker = invoker;
317	_Prepare();
318	Show();
319	return B_OK;
320}
321
322
323void
324BAlert::MessageReceived(BMessage* msg)
325{
326	if (msg->what != kAlertButtonMsg)
327		return BWindow::MessageReceived(msg);
328
329	int32 which;
330	if (msg->FindInt32("which", &which) == B_OK) {
331		if (fAlertSem < 0) {
332			// Semaphore hasn't been created; we're running asynchronous
333			if (fInvoker != NULL) {
334				BMessage* out = fInvoker->Message();
335				if (out && (out->ReplaceInt32("which", which) == B_OK
336							|| out->AddInt32("which", which) == B_OK))
337					fInvoker->Invoke();
338			}
339			PostMessage(B_QUIT_REQUESTED);
340		} else {
341			// Created semaphore means were running synchronously
342			fAlertValue = which;
343
344			// TextAlertVar does release_sem() below, and then sets the
345			// member var.  That doesn't make much sense to me, since we
346			// want to be able to clean up at some point.  Better to just
347			// nuke the semaphore now; we don't need it any more and this
348			// lets synchronous Go() continue just as well.
349			delete_sem(fAlertSem);
350			fAlertSem = -1;
351		}
352	}
353}
354
355
356void
357BAlert::FrameResized(float newWidth, float newHeight)
358{
359	BWindow::FrameResized(newWidth, newHeight);
360}
361
362
363void
364BAlert::AddButton(const char* label, char key)
365{
366	if (label == NULL || label[0] == '\0')
367		return;
368
369	BButton* button = _CreateButton(fButtons.size(), label);
370	fButtons.push_back(button);
371	fKeys.push_back(key);
372
373	SetDefaultButton(button);
374	fButtonLayout->AddView(button);
375}
376
377
378int32
379BAlert::CountButtons() const
380{
381	return (int32)fButtons.size();
382}
383
384
385BButton*
386BAlert::ButtonAt(int32 index) const
387{
388	if (index >= 0 && (size_t)index < fButtons.size())
389		return fButtons[index];
390
391	return NULL;
392}
393
394
395BTextView*
396BAlert::TextView() const
397{
398	return fTextView;
399}
400
401
402BHandler*
403BAlert::ResolveSpecifier(BMessage* msg, int32 index,
404	BMessage* specifier, int32 form, const char* property)
405{
406	return BWindow::ResolveSpecifier(msg, index, specifier, form, property);
407}
408
409
410status_t
411BAlert::GetSupportedSuites(BMessage* data)
412{
413	return BWindow::GetSupportedSuites(data);
414}
415
416
417void
418BAlert::DispatchMessage(BMessage* msg, BHandler* handler)
419{
420	BWindow::DispatchMessage(msg, handler);
421}
422
423
424void
425BAlert::Quit()
426{
427	BWindow::Quit();
428}
429
430
431bool
432BAlert::QuitRequested()
433{
434	return BWindow::QuitRequested();
435}
436
437
438//! This method is deprecated, do not use - use BWindow::CenterIn() instead.
439BPoint
440BAlert::AlertPosition(float width, float height)
441{
442	BPoint result(100, 100);
443
444	BWindow* window =
445		dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
446
447	BScreen screen(window);
448	BRect screenFrame(0, 0, 640, 480);
449	if (screen.IsValid())
450		screenFrame = screen.Frame();
451
452	// Horizontally, we're smack in the middle
453	result.x = screenFrame.left + (screenFrame.Width() / 2.0) - (width / 2.0);
454
455	// This is probably sooo wrong, but it looks right on 1024 x 768
456	result.y = screenFrame.top + (screenFrame.Height() / 4.0) - ceil(height / 3.0);
457
458	return result;
459}
460
461
462status_t
463BAlert::Perform(perform_code code, void* _data)
464{
465	switch (code) {
466		case PERFORM_CODE_SET_LAYOUT:
467			perform_data_set_layout* data = (perform_data_set_layout*)_data;
468			BAlert::SetLayout(data->layout);
469			return B_OK;
470	}
471
472	return BWindow::Perform(code, _data);
473}
474
475
476void BAlert::_ReservedAlert1() {}
477void BAlert::_ReservedAlert2() {}
478void BAlert::_ReservedAlert3() {}
479
480
481void
482BAlert::_Init(const char* text, const char* button0, const char* button1,
483	const char* button2, button_width buttonWidth, button_spacing spacing,
484	alert_type type)
485{
486	fInvoker = NULL;
487	fAlertSem = -1;
488	fAlertValue = -1;
489
490	fIconView = new TAlertView();
491
492	fTextView = new BTextView("_tv_");
493	fTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
494	rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
495	fTextView->SetFontAndColor(be_plain_font, B_FONT_ALL, &textColor);
496	fTextView->MakeEditable(false);
497	fTextView->MakeSelectable(false);
498	fTextView->SetWordWrap(true);
499	fTextView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
500
501	fButtonLayout = new BGroupLayout(B_HORIZONTAL, B_USE_HALF_ITEM_SPACING);
502
503	SetType(type);
504	SetButtonWidth(buttonWidth);
505	SetButtonSpacing(spacing);
506	SetText(text);
507
508	BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0)
509		.Add(fIconView)
510		.AddGroup(B_VERTICAL, B_USE_HALF_ITEM_SPACING)
511			.SetInsets(B_USE_HALF_ITEM_INSETS)
512			.Add(fTextView)
513			.AddGroup(B_HORIZONTAL, 0)
514				.AddGlue()
515				.Add(fButtonLayout);
516
517	AddButton(button0);
518	AddButton(button1);
519	AddButton(button2);
520
521	AddCommonFilter(new(std::nothrow) _BAlertFilter_(this));
522}
523
524
525BBitmap*
526BAlert::_CreateTypeIcon()
527{
528	if (Type() == B_EMPTY_ALERT)
529		return NULL;
530
531	// The icons are in the app_server resources
532	BBitmap* icon = NULL;
533
534	// Which icon are we trying to load?
535	const char* iconName;
536	switch (fType) {
537		case B_INFO_ALERT:
538			iconName = "dialog-information";
539			break;
540		case B_IDEA_ALERT:
541			iconName = "dialog-idea";
542			break;
543		case B_WARNING_ALERT:
544			iconName = "dialog-warning";
545			break;
546		case B_STOP_ALERT:
547			iconName = "dialog-error";
548			break;
549
550		default:
551			// Alert type is either invalid or B_EMPTY_ALERT;
552			// either way, we're not going to load an icon
553			return NULL;
554	}
555
556	// Allocate the icon bitmap
557	icon = new(std::nothrow) BBitmap(BRect(BPoint(0, 0), be_control_look->ComposeIconSize(32)),
558		0, B_RGBA32);
559	if (icon == NULL || icon->InitCheck() < B_OK) {
560		FTRACE((stderr, "BAlert::_CreateTypeIcon() - No memory for bitmap\n"));
561		delete icon;
562		return NULL;
563	}
564
565	// Load the raw icon data
566	BIconUtils::GetSystemIcon(iconName, icon);
567
568	return icon;
569}
570
571
572BButton*
573BAlert::_CreateButton(int32 which, const char* label)
574{
575	BMessage* message = new BMessage(kAlertButtonMsg);
576	if (message == NULL)
577		return NULL;
578
579	message->AddInt32("which", which);
580
581	char name[32];
582	snprintf(name, sizeof(name), "_b%" B_PRId32 "_", which);
583
584	return new(std::nothrow) BButton(name, label, message);
585}
586
587
588/*!	Tweaks the layout according to the configuration.
589*/
590void
591BAlert::_Prepare()
592{
593	// Must have at least one button
594	if (CountButtons() == 0)
595		debugger("BAlerts must have at least one button.");
596
597	float fontFactor = be_plain_font->Size() / 11.0f;
598
599	if (fIconView->Bitmap() == NULL)
600		fIconView->SetBitmap(_CreateTypeIcon());
601
602	if (fButtonWidth == B_WIDTH_AS_USUAL) {
603		float usualWidth = kButtonUsualWidth * fontFactor;
604
605		for (int32 index = 0; index < CountButtons(); index++) {
606			BButton* button = ButtonAt(index);
607			if (button->MinSize().width < usualWidth)
608				button->SetExplicitSize(BSize(usualWidth, B_SIZE_UNSET));
609		}
610	} else if (fButtonWidth == B_WIDTH_FROM_WIDEST) {
611		// Get width of widest label
612		float maxWidth = 0;
613		for (int32 index = 0; index < CountButtons(); index++) {
614			BButton* button = ButtonAt(index);
615			float width;
616			button->GetPreferredSize(&width, NULL);
617
618			if (width > maxWidth)
619				maxWidth = width;
620		}
621		for (int32 index = 0; index < CountButtons(); index++) {
622			BButton* button = ButtonAt(index);
623			button->SetExplicitSize(BSize(maxWidth, B_SIZE_UNSET));
624		}
625	}
626
627	if (fButtonSpacing == B_OFFSET_SPACING && CountButtons() > 1) {
628		// Insert some strut
629		fButtonLayout->AddItem(1, BSpaceLayoutItem::CreateHorizontalStrut(
630			kButtonOffsetSpacing * fontFactor));
631	}
632
633	// Position the alert so that it is centered vertically but offset a bit
634	// horizontally in the parent window's frame or, if unavailable, the
635	// screen frame.
636	float minWindowWidth = (fButtonSpacing == B_OFFSET_SPACING
637		? kWindowOffsetMinWidth : kWindowMinWidth) * fontFactor;
638	GetLayout()->SetExplicitMinSize(BSize(minWindowWidth, B_SIZE_UNSET));
639
640	ResizeToPreferred();
641
642	// Return early if we've already been moved...
643	if (Frame().left != 0 && Frame().right != 0)
644		return;
645
646	// otherwise center ourselves on-top of parent window/screen
647	BWindow* parent = dynamic_cast<BWindow*>(BLooper::LooperForThread(
648		find_thread(NULL)));
649	const BRect frame = parent != NULL ? parent->Frame()
650		: BScreen(this).Frame();
651
652	MoveTo(static_cast<BWindow*>(this)->AlertPosition(frame));
653		// Hidden by BAlert::AlertPosition()
654}
655
656
657//	#pragma mark - TAlertView
658
659
660TAlertView::TAlertView()
661	:
662	BView("TAlertView", B_WILL_DRAW),
663	fIconBitmap(NULL)
664{
665	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
666}
667
668
669TAlertView::TAlertView(BMessage* archive)
670	:
671	BView(archive),
672	fIconBitmap(NULL)
673{
674}
675
676
677TAlertView::~TAlertView()
678{
679	delete fIconBitmap;
680}
681
682
683TAlertView*
684TAlertView::Instantiate(BMessage* archive)
685{
686	if (!validate_instantiation(archive, "TAlertView"))
687		return NULL;
688
689	return new(std::nothrow) TAlertView(archive);
690}
691
692
693status_t
694TAlertView::Archive(BMessage* archive, bool deep) const
695{
696	return BView::Archive(archive, deep);
697}
698
699
700void
701TAlertView::SetBitmap(BBitmap* icon)
702{
703	if (icon == NULL && fIconBitmap == NULL)
704		return;
705
706	ASSERT(icon != fIconBitmap);
707
708	BBitmap* oldBitmap = fIconBitmap;
709	fIconBitmap = icon;
710	Invalidate();
711
712	if (oldBitmap == NULL || icon == NULL || oldBitmap->Bounds() != icon->Bounds())
713		InvalidateLayout();
714
715	delete oldBitmap;
716}
717
718
719void
720TAlertView::GetPreferredSize(float* _width, float* _height)
721{
722	if (_width != NULL) {
723		*_width = be_control_look->DefaultLabelSpacing() * 3;
724		if (fIconBitmap != NULL)
725			*_width += fIconBitmap->Bounds().Width();
726		else
727			*_width += be_control_look->ComposeIconSize(B_LARGE_ICON).Width();
728	}
729
730	if (_height != NULL) {
731		*_height = be_control_look->DefaultLabelSpacing();
732		if (fIconBitmap != NULL)
733			*_height += fIconBitmap->Bounds().Height();
734		else
735			*_height += be_control_look->ComposeIconSize(B_LARGE_ICON).Height();
736	}
737}
738
739
740BSize
741TAlertView::MaxSize()
742{
743	return BSize(MinSize().width, B_SIZE_UNLIMITED);
744}
745
746
747void
748TAlertView::Draw(BRect updateRect)
749{
750	// Here's the fun stuff
751	BRect stripeRect = Bounds();
752	stripeRect.right = kIconStripeWidthFactor * be_control_look->DefaultLabelSpacing();
753	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
754	FillRect(stripeRect);
755
756	if (fIconBitmap == NULL)
757		return;
758
759	SetDrawingMode(B_OP_ALPHA);
760	SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
761	DrawBitmapAsync(fIconBitmap, BPoint(be_control_look->DefaultLabelSpacing() * 3,
762		be_control_look->DefaultLabelSpacing()));
763}
764
765
766//	#pragma mark - _BAlertFilter_
767
768
769_BAlertFilter_::_BAlertFilter_(BAlert* alert)
770	: BMessageFilter(B_KEY_DOWN),
771	fAlert(alert)
772{
773}
774
775
776_BAlertFilter_::~_BAlertFilter_()
777{
778}
779
780
781filter_result
782_BAlertFilter_::Filter(BMessage* msg, BHandler** target)
783{
784	if (msg->what == B_KEY_DOWN) {
785		char byte;
786		if (msg->FindInt8("byte", (int8*)&byte) == B_OK) {
787			for (int i = 0; i < fAlert->CountButtons(); ++i) {
788				if (byte == fAlert->Shortcut(i) && fAlert->ButtonAt(i)) {
789					char space = ' ';
790					fAlert->ButtonAt(i)->KeyDown(&space, 1);
791
792					return B_SKIP_MESSAGE;
793				}
794			}
795		}
796	}
797
798	return B_DISPATCH_MESSAGE;
799}
800