1 /* 2 * Copyright 2001-2006, Haiku. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Erik Jaesler (erik@cgsoftware.com) 7 * Axel Dörfler, axeld@pinc-software.de 8 */ 9 10 //! BAlert displays a modal alert window. 11 12 #include <Alert.h> 13 #include <Autolock.h> 14 #include <Beep.h> 15 #include <Bitmap.h> 16 #include <Button.h> 17 #include <File.h> 18 #include <FindDirectory.h> 19 #include <Font.h> 20 #include <IconUtils.h> 21 #include <Invoker.h> 22 #include <Looper.h> 23 #include <Message.h> 24 #include <MessageFilter.h> 25 #include <Path.h> 26 #include <Resources.h> 27 #include <Screen.h> 28 #include <TextView.h> 29 #include <View.h> 30 31 #include <new> 32 #include <stdio.h> 33 #include <string.h> 34 35 //#define DEBUG_ALERT 36 #ifdef DEBUG_ALERT 37 # define FTRACE(x) fprintf(x) 38 #else 39 # define FTRACE(x) /* nothing */ 40 #endif 41 42 // Default size of the Alert window. 43 #define DEFAULT_RECT BRect(0, 0, 310, 75) 44 #define max(LHS, RHS) ((LHS) > (RHS) ? (LHS) : (RHS)) 45 46 static const unsigned int kAlertButtonMsg = 'ALTB'; 47 static const int kSemTimeOut = 50000; 48 49 static const int kLeftOffset = 10; 50 static const int kTopOffset = 6; 51 static const int kBottomOffset = 8; 52 static const int kRightOffset = 8; 53 54 static const int kButtonSpacing = 6; 55 static const int kButtonOffsetSpacing = 62; 56 static const int kButtonUsualWidth = 75; 57 58 static const int kWindowIconOffset = 27; 59 static const int kWindowMinOffset = 12; 60 static const int kWindowMinWidth = 310; 61 static const int kWindowOffsetMinWidth = 335; 62 63 static const int kIconStripeWidth = 30; 64 65 static const int kTextButtonOffset = 10; 66 67 static inline int32 68 icon_layout_scale() 69 { 70 #ifdef __HAIKU__ 71 return max_c(1, ((int32)be_plain_font->Size() + 15) / 16); 72 #endif 73 return 1; 74 } 75 76 77 class TAlertView : public BView { 78 public: 79 TAlertView(BRect frame); 80 TAlertView(BMessage* archive); 81 ~TAlertView(); 82 83 static TAlertView* Instantiate(BMessage* archive); 84 virtual status_t Archive(BMessage* archive, bool deep = true) const; 85 86 virtual void Draw(BRect updateRect); 87 88 // These functions (or something analogous) are missing from libbe.so's 89 // dump. I can only assume that the bitmap is a public var in the 90 // original implementation -- or BAlert is a friend of TAlertView. 91 // Neither one is necessary, since I can just add these. 92 void SetBitmap(BBitmap* Icon) { fIconBitmap = Icon; } 93 BBitmap* Bitmap() { return fIconBitmap; } 94 95 private: 96 BBitmap* fIconBitmap; 97 }; 98 99 class _BAlertFilter_ : public BMessageFilter { 100 public: 101 _BAlertFilter_(BAlert* Alert); 102 ~_BAlertFilter_(); 103 104 virtual filter_result Filter(BMessage* msg, BHandler** target); 105 106 private: 107 BAlert* fAlert; 108 }; 109 110 111 // #pragma mark - BAlert 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 : BWindow(DEFAULT_RECT, title, B_MODAL_WINDOW, 118 B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS) 119 { 120 _InitObject(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 : BWindow(DEFAULT_RECT, title, B_MODAL_WINDOW, 128 B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS) 129 { 130 _InitObject(text, button1, button2, button3, width, spacing, type); 131 } 132 133 134 BAlert::~BAlert() 135 { 136 // Probably not necessary, but it makes me feel better. 137 if (fAlertSem >= B_OK) 138 delete_sem(fAlertSem); 139 } 140 141 142 BAlert::BAlert(BMessage* data) 143 : BWindow(data) 144 { 145 fInvoker = NULL; 146 fAlertSem = -1; 147 fAlertValue = -1; 148 149 fTextView = (BTextView*)FindView("_tv_"); 150 151 fButtons[0] = (BButton*)FindView("_b0_"); 152 fButtons[1] = (BButton*)FindView("_b1_"); 153 fButtons[2] = (BButton*)FindView("_b2_"); 154 155 if (fButtons[2]) 156 SetDefaultButton(fButtons[2]); 157 else if (fButtons[1]) 158 SetDefaultButton(fButtons[1]); 159 else if (fButtons[0]) 160 SetDefaultButton(fButtons[0]); 161 162 TAlertView* view = (TAlertView*)FindView("_master_"); 163 if (view) 164 view->SetBitmap(_InitIcon()); 165 166 // Get keys 167 char key; 168 for (int32 i = 0; i < 3; ++i) { 169 if (data->FindInt8("_but_key", i, (int8*)&key) == B_OK) 170 fKeys[i] = key; 171 } 172 173 int32 temp; 174 // Get alert type 175 if (data->FindInt32("_atype", &temp) == B_OK) 176 fMsgType = (alert_type)temp; 177 178 // Get button width 179 if (data->FindInt32("_but_width", &temp) == B_OK) 180 fButtonWidth = (button_width)temp; 181 182 AddCommonFilter(new _BAlertFilter_(this)); 183 } 184 185 186 BArchivable* 187 BAlert::Instantiate(BMessage* data) 188 { 189 if (!validate_instantiation(data, "BAlert")) 190 return NULL; 191 192 return new BAlert(data); 193 } 194 195 196 status_t 197 BAlert::Archive(BMessage* data, bool deep) const 198 { 199 status_t ret = BWindow::Archive(data, deep); 200 201 // Stow the text 202 if (ret == B_OK) 203 ret = data->AddString("_text", fTextView->Text()); 204 205 // Stow the alert type 206 if (ret == B_OK) 207 ret = data->AddInt32("_atype", fMsgType); 208 209 // Stow the button width 210 if (ret == B_OK) 211 ret = data->AddInt32("_but_width", fButtonWidth); 212 213 // Stow the shortcut keys 214 if (fKeys[0] || fKeys[1] || fKeys[2]) { 215 // If we have any to save, we must save something for everyone so it 216 // doesn't get confusing on the unarchive. 217 if (ret == B_OK) 218 ret = data->AddInt8("_but_key", fKeys[0]); 219 if (ret == B_OK) 220 ret = data->AddInt8("_but_key", fKeys[1]); 221 if (ret == B_OK) 222 ret = data->AddInt8("_but_key", fKeys[2]); 223 } 224 225 return ret; 226 } 227 228 229 void 230 BAlert::SetShortcut(int32 index, char key) 231 { 232 if (index >= 0 && index < 3) 233 fKeys[index] = key; 234 } 235 236 237 char 238 BAlert::Shortcut(int32 index) const 239 { 240 if (index >= 0 && index < 3) 241 return fKeys[index]; 242 243 return 0; 244 } 245 246 247 int32 248 BAlert::Go() 249 { 250 fAlertSem = create_sem(0, "AlertSem"); 251 if (fAlertSem < B_OK) { 252 Quit(); 253 return -1; 254 } 255 256 // Get the originating window, if it exists 257 BWindow* window = 258 dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL))); 259 260 Show(); 261 262 // Heavily modified from TextEntryAlert code; the original didn't let the 263 // blocked window ever draw. 264 if (window) { 265 status_t err; 266 for (;;) { 267 do { 268 err = acquire_sem_etc(fAlertSem, 1, B_RELATIVE_TIMEOUT, 269 kSemTimeOut); 270 // We've (probably) had our time slice taken away from us 271 } while (err == B_INTERRUPTED); 272 273 if (err == B_BAD_SEM_ID) { 274 // Semaphore was finally nuked in MessageReceived 275 break; 276 } 277 window->UpdateIfNeeded(); 278 } 279 } else { 280 // No window to update, so just hang out until we're done. 281 while (acquire_sem(fAlertSem) == B_INTERRUPTED) { 282 } 283 } 284 285 // Have to cache the value since we delete on Quit() 286 int32 value = fAlertValue; 287 if (Lock()) 288 Quit(); 289 290 return value; 291 } 292 293 294 status_t 295 BAlert::Go(BInvoker* invoker) 296 { 297 fInvoker = invoker; 298 Show(); 299 return B_OK; 300 } 301 302 303 void 304 BAlert::MessageReceived(BMessage* msg) 305 { 306 if (msg->what != kAlertButtonMsg) 307 return BWindow::MessageReceived(msg); 308 309 int32 which; 310 if (msg->FindInt32("which", &which) == B_OK) { 311 if (fAlertSem < B_OK) { 312 // Semaphore hasn't been created; we're running asynchronous 313 if (fInvoker) { 314 BMessage* out = fInvoker->Message(); 315 if (out && (out->ReplaceInt32("which", which) == B_OK 316 || out->AddInt32("which", which) == B_OK)) 317 fInvoker->Invoke(); 318 } 319 PostMessage(B_QUIT_REQUESTED); 320 } else { 321 // Created semaphore means were running synchronously 322 fAlertValue = which; 323 324 // TextAlertVar does release_sem() below, and then sets the 325 // member var. That doesn't make much sense to me, since we 326 // want to be able to clean up at some point. Better to just 327 // nuke the semaphore now; we don't need it any more and this 328 // lets synchronous Go() continue just as well. 329 delete_sem(fAlertSem); 330 fAlertSem = -1; 331 } 332 } 333 } 334 335 336 void 337 BAlert::FrameResized(float newWidth, float newHeight) 338 { 339 BWindow::FrameResized(newWidth, newHeight); 340 } 341 342 343 BButton* 344 BAlert::ButtonAt(int32 index) const 345 { 346 if (index >= 0 && index < 3) 347 return fButtons[index]; 348 349 return NULL; 350 } 351 352 353 BTextView* 354 BAlert::TextView() const 355 { 356 return fTextView; 357 } 358 359 360 BHandler* 361 BAlert::ResolveSpecifier(BMessage* msg, int32 index, 362 BMessage* specifier, int32 form, const char* property) 363 { 364 return BWindow::ResolveSpecifier(msg, index, specifier, form, property); 365 } 366 367 368 status_t 369 BAlert::GetSupportedSuites(BMessage* data) 370 { 371 return BWindow::GetSupportedSuites(data); 372 } 373 374 375 void 376 BAlert::DispatchMessage(BMessage* msg, BHandler* handler) 377 { 378 BWindow::DispatchMessage(msg, handler); 379 } 380 381 382 void 383 BAlert::Quit() 384 { 385 BWindow::Quit(); 386 } 387 388 389 bool 390 BAlert::QuitRequested() 391 { 392 return BWindow::QuitRequested(); 393 } 394 395 396 BPoint 397 BAlert::AlertPosition(float width, float height) 398 { 399 BPoint result(100, 100); 400 401 BWindow* window = 402 dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL))); 403 404 BScreen screen(window); 405 BRect screenFrame(0, 0, 640, 480); 406 if (screen.IsValid()) 407 screenFrame = screen.Frame(); 408 409 // Horizontally, we're smack in the middle 410 result.x = screenFrame.left + (screenFrame.Width() / 2.0) - (width / 2.0); 411 412 // This is probably sooo wrong, but it looks right on 1024 x 768 413 result.y = screenFrame.top + (screenFrame.Height() / 4.0) - ceil(height / 3.0); 414 415 return result; 416 } 417 418 419 status_t 420 BAlert::Perform(perform_code d, void* arg) 421 { 422 return BWindow::Perform(d, arg); 423 } 424 425 426 void BAlert::_ReservedAlert1() {} 427 void BAlert::_ReservedAlert2() {} 428 void BAlert::_ReservedAlert3() {} 429 430 431 void 432 BAlert::_InitObject(const char* text, const char* button0, const char* button1, 433 const char* button2, button_width buttonWidth, button_spacing spacing, 434 alert_type type) 435 { 436 fInvoker = NULL; 437 fAlertSem = -1; 438 fAlertValue = -1; 439 fButtons[0] = fButtons[1] = fButtons[2] = NULL; 440 fTextView = NULL; 441 fKeys[0] = fKeys[1] = fKeys[2] = 0; 442 fMsgType = type; 443 fButtonWidth = buttonWidth; 444 445 // Set up the "_master_" view 446 TAlertView* view = new TAlertView(Bounds()); 447 AddChild(view); 448 view->SetBitmap(_InitIcon()); 449 450 // Must have at least one button 451 if (button0 == NULL) { 452 debugger("BAlert's must have at least one button."); 453 button0 = ""; 454 } 455 456 // Set up the buttons 457 458 int32 buttonCount = 1; 459 view->AddChild(fButtons[0] = _CreateButton(0, button0)); 460 461 if (button1 != NULL) { 462 view->AddChild(fButtons[1] = _CreateButton(1, button1)); 463 buttonCount++; 464 } 465 466 if (button2 != NULL) { 467 view->AddChild(fButtons[2] = _CreateButton(2, button2)); 468 buttonCount++; 469 } 470 471 // Find the widest button only if the widest value needs to be known. 472 473 if (fButtonWidth == B_WIDTH_FROM_WIDEST) { 474 float maxWidth = 0; 475 for (int i = 0; i < buttonCount; i++) { 476 float width = fButtons[i]->Bounds().Width(); 477 if (width > maxWidth) 478 maxWidth = width; 479 } 480 481 // resize buttons 482 for (int i = 0; i < buttonCount; i++) { 483 fButtons[i]->ResizeTo(maxWidth, fButtons[i]->Bounds().Height()); 484 } 485 } 486 487 float defaultButtonFrameWidth = -fButtons[buttonCount - 1]->Bounds().Width() / 2.0f; 488 SetDefaultButton(fButtons[buttonCount - 1]); 489 defaultButtonFrameWidth += fButtons[buttonCount - 1]->Bounds().Width() / 2.0f; 490 491 // Layout buttons 492 493 float fontFactor = be_plain_font->Size() / 11.0f; 494 495 for (int i = buttonCount - 1; i >= 0; --i) { 496 float x = -fButtons[i]->Bounds().Width(); 497 if (i + 1 == buttonCount) 498 x += Bounds().right - kRightOffset + defaultButtonFrameWidth; 499 else 500 x += fButtons[i + 1]->Frame().left - kButtonSpacing; 501 502 if (buttonCount > 1 && i == 0 && spacing == B_OFFSET_SPACING) 503 x -= kButtonOffsetSpacing * fontFactor; 504 505 fButtons[i]->MoveTo(x, fButtons[i]->Frame().top); 506 } 507 508 // Adjust the window's width, if necessary 509 510 int32 iconLayoutScale = icon_layout_scale(); 511 float totalWidth = kRightOffset + fButtons[buttonCount - 1]->Frame().right 512 - defaultButtonFrameWidth - fButtons[0]->Frame().left; 513 if (view->Bitmap()) { 514 totalWidth += (kIconStripeWidth + kWindowIconOffset) * iconLayoutScale; 515 } else 516 totalWidth += kWindowMinOffset; 517 518 float width = (spacing == B_OFFSET_SPACING 519 ? kWindowOffsetMinWidth : kWindowMinWidth) * fontFactor; 520 521 ResizeTo(max_c(totalWidth, width), Bounds().Height()); 522 523 // Set up the text view 524 525 BRect textViewRect(kLeftOffset, kTopOffset, 526 Bounds().right - kRightOffset, 527 fButtons[0]->Frame().top - kTextButtonOffset); 528 if (view->Bitmap()) 529 textViewRect.left = (kWindowIconOffset 530 + kIconStripeWidth) * iconLayoutScale - 2; 531 532 fTextView = new BTextView(textViewRect, "_tv_", 533 textViewRect.OffsetByCopy(B_ORIGIN), 534 B_FOLLOW_LEFT | B_FOLLOW_TOP, B_WILL_DRAW); 535 fTextView->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 536 rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); 537 fTextView->SetFontAndColor(be_plain_font, B_FONT_ALL, &textColor); 538 fTextView->SetText(text, strlen(text)); 539 fTextView->MakeEditable(false); 540 fTextView->MakeSelectable(false); 541 fTextView->SetWordWrap(true); 542 view->AddChild(fTextView); 543 544 // Now resize the TextView vertically so that all the text is visible 545 float textHeight = fTextView->TextHeight(0, fTextView->CountLines()); 546 textViewRect.OffsetTo(0, 0); 547 textHeight -= textViewRect.Height(); 548 ResizeBy(0, textHeight); 549 fTextView->ResizeBy(0, textHeight); 550 textViewRect.bottom += textHeight; 551 fTextView->SetTextRect(textViewRect); 552 553 AddCommonFilter(new _BAlertFilter_(this)); 554 555 MoveTo(AlertPosition(Frame().Width(), Frame().Height())); 556 } 557 558 559 BBitmap* 560 BAlert::_InitIcon() 561 { 562 // Save the desired alert type and set it to "empty" until 563 // loading the icon was successful 564 alert_type alertType = fMsgType; 565 fMsgType = B_EMPTY_ALERT; 566 567 // After a bit of a search, I found the icons in app_server. =P 568 BBitmap* icon = NULL; 569 BPath path; 570 status_t status = find_directory(B_BEOS_SERVERS_DIRECTORY, &path); 571 if (status < B_OK) { 572 FTRACE((stderr, "BAlert::_InitIcon() - find_directory failed: %s\n", 573 strerror(status))); 574 return NULL; 575 } 576 577 path.Append("app_server"); 578 BFile file; 579 status = file.SetTo(path.Path(), B_READ_ONLY); 580 if (status < B_OK) { 581 FTRACE((stderr, "BAlert::_InitIcon() - BFile init failed: %s\n", 582 strerror(status))); 583 return NULL; 584 } 585 586 BResources resources; 587 status = resources.SetTo(&file); 588 if (status < B_OK) { 589 FTRACE((stderr, "BAlert::_InitIcon() - BResources init failed: %s\n", 590 strerror(status))); 591 return NULL; 592 } 593 594 // Which icon are we trying to load? 595 const char* iconName = ""; // Don't want any seg faults 596 switch (alertType) { 597 case B_INFO_ALERT: 598 iconName = "info"; 599 break; 600 case B_IDEA_ALERT: 601 iconName = "idea"; 602 break; 603 case B_WARNING_ALERT: 604 iconName = "warn"; 605 break; 606 case B_STOP_ALERT: 607 iconName = "stop"; 608 break; 609 610 default: 611 // Alert type is either invalid or B_EMPTY_ALERT; 612 // either way, we're not going to load an icon 613 return NULL; 614 } 615 616 int32 iconSize = 32 * icon_layout_scale(); 617 // Allocate the icon bitmap 618 icon = new (std::nothrow) BBitmap(BRect(0, 0, iconSize - 1, iconSize - 1), 619 0, B_RGBA32); 620 if (icon == NULL || icon->InitCheck() < B_OK) { 621 FTRACE((stderr, "BAlert::_InitIcon() - No memory for bitmap\n")); 622 delete icon; 623 return NULL; 624 } 625 626 // Load the raw icon data 627 size_t size = 0; 628 const uint8* rawIcon; 629 630 #ifdef __HAIKU__ 631 // Try to load vector icon 632 rawIcon = (const uint8*)resources.LoadResource(B_VECTOR_ICON_TYPE, 633 iconName, &size); 634 if (rawIcon != NULL 635 && BIconUtils::GetVectorIcon(rawIcon, size, icon) == B_OK) { 636 // We have an icon, restore the saved alert type 637 fMsgType = alertType; 638 return icon; 639 } 640 #endif 641 642 // Fall back to bitmap icon 643 rawIcon = (const uint8*)resources.LoadResource(B_LARGE_ICON_TYPE, 644 iconName, &size); 645 if (rawIcon == NULL) { 646 FTRACE((stderr, "BAlert::_InitIcon() - Icon resource not found\n")); 647 delete icon; 648 return NULL; 649 } 650 651 // Handle color space conversion 652 #ifdef __HAIKU__ 653 if (icon->ColorSpace() != B_CMAP8) { 654 BIconUtils::ConvertFromCMAP8(rawIcon, iconSize, iconSize, 655 iconSize, icon); 656 } 657 #else 658 icon->SetBits(rawIcon, iconSize, 0, B_CMAP8); 659 #endif 660 661 // We have an icon, restore the saved alert type 662 fMsgType = alertType; 663 664 return icon; 665 } 666 667 668 BButton* 669 BAlert::_CreateButton(int32 which, const char* label) 670 { 671 BMessage* message = new BMessage(kAlertButtonMsg); 672 if (message == NULL) 673 return NULL; 674 675 message->AddInt32("which", which); 676 677 BRect rect; 678 rect.top = Bounds().bottom - kBottomOffset; 679 rect.bottom = rect.top; 680 681 char name[32]; 682 snprintf(name, sizeof(name), "_b%ld_", which); 683 684 BButton* button = new (std::nothrow) BButton(rect, name, label, message, 685 B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM); 686 if (button == NULL) 687 return NULL; 688 689 float width, height; 690 button->GetPreferredSize(&width, &height); 691 692 if (fButtonWidth == B_WIDTH_AS_USUAL) { 693 float fontFactor = be_plain_font->Size() / 11.0f; 694 width = max_c(width, kButtonUsualWidth * fontFactor); 695 } 696 697 button->ResizeTo(width, height); 698 button->MoveBy(0.0f, -height); 699 return button; 700 } 701 702 703 // #pragma mark - TAlertView 704 705 706 TAlertView::TAlertView(BRect frame) 707 : BView(frame, "TAlertView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW), 708 fIconBitmap(NULL) 709 { 710 SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 711 } 712 713 714 TAlertView::TAlertView(BMessage* archive) 715 : BView(archive), 716 fIconBitmap(NULL) 717 { 718 } 719 720 721 TAlertView::~TAlertView() 722 { 723 delete fIconBitmap; 724 } 725 726 727 TAlertView* 728 TAlertView::Instantiate(BMessage* archive) 729 { 730 if (!validate_instantiation(archive, "TAlertView")) 731 return NULL; 732 733 return new TAlertView(archive); 734 } 735 736 737 status_t 738 TAlertView::Archive(BMessage* archive, bool deep) const 739 { 740 return BView::Archive(archive, deep); 741 } 742 743 744 void 745 TAlertView::Draw(BRect updateRect) 746 { 747 if (!fIconBitmap) 748 return; 749 750 // Here's the fun stuff 751 BRect stripeRect = Bounds(); 752 int32 iconLayoutScale = icon_layout_scale(); 753 stripeRect.right = kIconStripeWidth * iconLayoutScale; 754 SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT)); 755 FillRect(stripeRect); 756 757 SetDrawingMode(B_OP_ALPHA); 758 SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 759 DrawBitmapAsync(fIconBitmap, BPoint(18 * iconLayoutScale, 760 6 * iconLayoutScale)); 761 762 } 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 < 3; ++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 801