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