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