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