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 47 class TAlertView : public BView { 48 public: 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 65 private: 66 BBitmap* fIconBitmap; 67 }; 68 69 70 class _BAlertFilter_ : public BMessageFilter { 71 public: 72 _BAlertFilter_(BAlert* Alert); 73 ~_BAlertFilter_(); 74 75 virtual filter_result Filter(BMessage* msg, BHandler** target); 76 77 private: 78 BAlert* fAlert; 79 }; 80 81 82 static const unsigned int kAlertButtonMsg = 'ALTB'; 83 static const int kSemTimeOut = 50000; 84 85 static const int kButtonOffsetSpacing = 62; 86 static const int kButtonUsualWidth = 55; 87 static const int kIconStripeWidthFactor = 5; 88 89 static const int kWindowMinWidth = 310; 90 static const int kWindowOffsetMinWidth = 335; 91 92 93 // #pragma mark - 94 95 96 BAlert::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 106 BAlert::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 117 BAlert::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 128 BAlert::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 156 BAlert::~BAlert() 157 { 158 // Probably not necessary, but it makes me feel better. 159 if (fAlertSem >= B_OK) 160 delete_sem(fAlertSem); 161 } 162 163 164 BArchivable* 165 BAlert::Instantiate(BMessage* data) 166 { 167 if (!validate_instantiation(data, "BAlert")) 168 return NULL; 169 170 return new(std::nothrow) BAlert(data); 171 } 172 173 174 status_t 175 BAlert::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 207 alert_type 208 BAlert::Type() const 209 { 210 return (alert_type)fType; 211 } 212 213 214 void 215 BAlert::SetType(alert_type type) 216 { 217 fType = type; 218 } 219 220 221 void 222 BAlert::SetText(const char* text) 223 { 224 TextView()->SetText(text); 225 } 226 227 228 void 229 BAlert::SetIcon(BBitmap* bitmap) 230 { 231 fIconView->SetBitmap(bitmap); 232 } 233 234 235 void 236 BAlert::SetButtonSpacing(button_spacing spacing) 237 { 238 fButtonSpacing = spacing; 239 } 240 241 242 void 243 BAlert::SetButtonWidth(button_width width) 244 { 245 fButtonWidth = width; 246 } 247 248 249 void 250 BAlert::SetShortcut(int32 index, char key) 251 { 252 if (index >= 0 && (size_t)index < fKeys.size()) 253 fKeys[index] = key; 254 } 255 256 257 char 258 BAlert::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 267 int32 268 BAlert::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 313 status_t 314 BAlert::Go(BInvoker* invoker) 315 { 316 fInvoker = invoker; 317 _Prepare(); 318 Show(); 319 return B_OK; 320 } 321 322 323 void 324 BAlert::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 356 void 357 BAlert::FrameResized(float newWidth, float newHeight) 358 { 359 BWindow::FrameResized(newWidth, newHeight); 360 } 361 362 363 void 364 BAlert::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 378 int32 379 BAlert::CountButtons() const 380 { 381 return (int32)fButtons.size(); 382 } 383 384 385 BButton* 386 BAlert::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 395 BTextView* 396 BAlert::TextView() const 397 { 398 return fTextView; 399 } 400 401 402 BHandler* 403 BAlert::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 410 status_t 411 BAlert::GetSupportedSuites(BMessage* data) 412 { 413 return BWindow::GetSupportedSuites(data); 414 } 415 416 417 void 418 BAlert::DispatchMessage(BMessage* msg, BHandler* handler) 419 { 420 BWindow::DispatchMessage(msg, handler); 421 } 422 423 424 void 425 BAlert::Quit() 426 { 427 BWindow::Quit(); 428 } 429 430 431 bool 432 BAlert::QuitRequested() 433 { 434 return BWindow::QuitRequested(); 435 } 436 437 438 //! This method is deprecated, do not use - use BWindow::CenterIn() instead. 439 BPoint 440 BAlert::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 462 status_t 463 BAlert::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 476 void BAlert::_ReservedAlert1() {} 477 void BAlert::_ReservedAlert2() {} 478 void BAlert::_ReservedAlert3() {} 479 480 481 void 482 BAlert::_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 525 BBitmap* 526 BAlert::_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 572 BButton* 573 BAlert::_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 */ 590 void 591 BAlert::_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 660 TAlertView::TAlertView() 661 : 662 BView("TAlertView", B_WILL_DRAW), 663 fIconBitmap(NULL) 664 { 665 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 666 } 667 668 669 TAlertView::TAlertView(BMessage* archive) 670 : 671 BView(archive), 672 fIconBitmap(NULL) 673 { 674 } 675 676 677 TAlertView::~TAlertView() 678 { 679 delete fIconBitmap; 680 } 681 682 683 TAlertView* 684 TAlertView::Instantiate(BMessage* archive) 685 { 686 if (!validate_instantiation(archive, "TAlertView")) 687 return NULL; 688 689 return new(std::nothrow) TAlertView(archive); 690 } 691 692 693 status_t 694 TAlertView::Archive(BMessage* archive, bool deep) const 695 { 696 return BView::Archive(archive, deep); 697 } 698 699 700 void 701 TAlertView::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 719 void 720 TAlertView::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 740 BSize 741 TAlertView::MaxSize() 742 { 743 return BSize(MinSize().width, B_SIZE_UNLIMITED); 744 } 745 746 747 void 748 TAlertView::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 781 filter_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