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