1 /* 2 * Copyright 2007-2014, Haiku, Inc. 3 * Distributed under the terms of the MIT license. 4 * 5 * Author: 6 * Łukasz 'Sil2100' Zemczak <sil2100@vexillium.org> 7 * Stephan Aßmus <superstippi@gmx.de> 8 */ 9 10 11 #include "InstalledPackageInfo.h" 12 #include "PackageImageViewer.h" 13 #include "PackageTextViewer.h" 14 #include "PackageView.h" 15 16 #include <Alert.h> 17 #include <Box.h> 18 #include <Button.h> 19 #include <Catalog.h> 20 #include <Directory.h> 21 #include <FilePanel.h> 22 #include <FindDirectory.h> 23 #include <Locale.h> 24 #include <LayoutBuilder.h> 25 #include <MenuField.h> 26 #include <MenuItem.h> 27 #include <Path.h> 28 #include <PopUpMenu.h> 29 #include <ScrollView.h> 30 #include <StringForSize.h> 31 #include <TextView.h> 32 #include <Volume.h> 33 #include <VolumeRoster.h> 34 #include <Window.h> 35 36 #include <GroupLayout.h> 37 #include <GroupLayoutBuilder.h> 38 #include <GroupView.h> 39 40 #include <fs_info.h> 41 #include <stdio.h> // For debugging 42 43 44 #undef B_TRANSLATION_CONTEXT 45 #define B_TRANSLATION_CONTEXT "PackageView" 46 47 const float kMaxDescHeight = 125.0f; 48 const uint32 kSeparatorIndex = 3; 49 50 51 // #pragma mark - 52 53 54 PackageView::PackageView(const entry_ref* ref) 55 : 56 BView("package_view", 0), 57 fOpenPanel(new BFilePanel(B_OPEN_PANEL, NULL, NULL, B_DIRECTORY_NODE, 58 false)), 59 fExpectingOpenPanelResult(false), 60 fInfo(ref), 61 fInstallProcess(this) 62 { 63 _InitView(); 64 65 // Check whether the package has been successfuly parsed 66 status_t ret = fInfo.InitCheck(); 67 if (ret == B_OK) 68 _InitProfiles(); 69 } 70 71 72 PackageView::~PackageView() 73 { 74 delete fOpenPanel; 75 } 76 77 78 void 79 PackageView::AttachedToWindow() 80 { 81 status_t ret = fInfo.InitCheck(); 82 if (ret != B_OK && ret != B_NO_INIT) { 83 BAlert* warning = new BAlert("parsing_failed", 84 B_TRANSLATE("The package file is not readable.\nOne of the " 85 "possible reasons for this might be that the requested file " 86 "is not a valid BeOS .pkg package."), B_TRANSLATE("OK"), 87 NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT); 88 warning->SetFlags(warning->Flags() | B_CLOSE_ON_ESCAPE); 89 warning->Go(); 90 91 Window()->PostMessage(B_QUIT_REQUESTED); 92 return; 93 } 94 95 // Set the window title 96 BWindow* parent = Window(); 97 BString title; 98 BString name = fInfo.GetName(); 99 if (name.CountChars() == 0) { 100 title = B_TRANSLATE("Package installer"); 101 } else { 102 title = B_TRANSLATE("Install %name%"); 103 title.ReplaceAll("%name%", name); 104 } 105 parent->SetTitle(title.String()); 106 fBeginButton->SetTarget(this); 107 108 fOpenPanel->SetTarget(BMessenger(this)); 109 fInstallTypes->SetTargetForItems(this); 110 111 if (ret != B_OK) 112 return; 113 114 // If the package is valid, we can set up the default group and all 115 // other things. If not, then the application will close just after 116 // attaching the view to the window 117 _InstallTypeChanged(0); 118 119 fStatusWindow = new PackageStatus(B_TRANSLATE("Installation progress"), 120 NULL, NULL, this); 121 122 // Show the splash screen, if present 123 BMallocIO* image = fInfo.GetSplashScreen(); 124 if (image != NULL) { 125 PackageImageViewer* imageViewer = new PackageImageViewer(image); 126 imageViewer->Go(); 127 } 128 129 // Show the disclaimer/info text popup, if present 130 BString disclaimer = fInfo.GetDisclaimer(); 131 if (disclaimer.Length() != 0) { 132 PackageTextViewer* text = new PackageTextViewer( 133 disclaimer.String()); 134 int32 selection = text->Go(); 135 // The user didn't accept our disclaimer, this means we cannot 136 // continue. 137 if (selection == 0) 138 parent->Quit(); 139 } 140 } 141 142 143 void 144 PackageView::MessageReceived(BMessage* message) 145 { 146 switch (message->what) { 147 case P_MSG_INSTALL: 148 { 149 fBeginButton->SetEnabled(false); 150 fInstallTypes->SetEnabled(false); 151 fDestination->SetEnabled(false); 152 fStatusWindow->Show(); 153 154 fInstallProcess.Start(); 155 break; 156 } 157 158 case P_MSG_PATH_CHANGED: 159 { 160 BString path; 161 if (message->FindString("path", &path) == B_OK) 162 fCurrentPath.SetTo(path.String()); 163 break; 164 } 165 166 case P_MSG_OPEN_PANEL: 167 fExpectingOpenPanelResult = true; 168 fOpenPanel->Show(); 169 break; 170 171 case P_MSG_INSTALL_TYPE_CHANGED: 172 { 173 int32 index; 174 if (message->FindInt32("index", &index) == B_OK) 175 _InstallTypeChanged(index); 176 break; 177 } 178 179 case P_MSG_I_FINISHED: 180 { 181 BAlert* notify = new BAlert("installation_success", 182 B_TRANSLATE("The package you requested has been successfully " 183 "installed on your system."), 184 B_TRANSLATE("OK")); 185 notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE); 186 187 notify->Go(); 188 fStatusWindow->Hide(); 189 fBeginButton->SetEnabled(true); 190 fInstallTypes->SetEnabled(true); 191 fDestination->SetEnabled(true); 192 fInstallProcess.Stop(); 193 194 BWindow *parent = Window(); 195 if (parent && parent->Lock()) 196 parent->Quit(); 197 break; 198 } 199 200 case P_MSG_I_ABORT: 201 { 202 BAlert* notify = new BAlert("installation_aborted", 203 B_TRANSLATE( 204 "The installation of the package has been aborted."), 205 B_TRANSLATE("OK")); 206 notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE); 207 notify->Go(); 208 fStatusWindow->Hide(); 209 fBeginButton->SetEnabled(true); 210 fInstallTypes->SetEnabled(true); 211 fDestination->SetEnabled(true); 212 fInstallProcess.Stop(); 213 break; 214 } 215 216 case P_MSG_I_ERROR: 217 { 218 // TODO: Review this 219 BAlert* notify = new BAlert("installation_failed", 220 B_TRANSLATE("The requested package failed to install on your " 221 "system. This might be a problem with the target package " 222 "file. Please consult this issue with the package " 223 "distributor."), 224 B_TRANSLATE("OK"), 225 NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT); 226 fputs(B_TRANSLATE("Error while installing the package\n"), 227 stderr); 228 notify->SetFlags(notify->Flags() | B_CLOSE_ON_ESCAPE); 229 notify->Go(); 230 fStatusWindow->Hide(); 231 fBeginButton->SetEnabled(true); 232 fInstallTypes->SetEnabled(true); 233 fDestination->SetEnabled(true); 234 fInstallProcess.Stop(); 235 break; 236 } 237 238 case P_MSG_STOP: 239 { 240 // This message is sent to us by the PackageStatus window, informing 241 // user interruptions. 242 // We actually use this message only when a post installation script 243 // is running and we want to kill it while it's still running 244 fStatusWindow->Hide(); 245 fBeginButton->SetEnabled(true); 246 fInstallTypes->SetEnabled(true); 247 fDestination->SetEnabled(true); 248 fInstallProcess.Stop(); 249 break; 250 } 251 252 case B_REFS_RECEIVED: 253 { 254 if (!_ValidateFilePanelMessage(message)) 255 break; 256 257 entry_ref ref; 258 if (message->FindRef("refs", &ref) == B_OK) { 259 BPath path(&ref); 260 if (path.InitCheck() != B_OK) 261 break; 262 263 dev_t device = dev_for_path(path.Path()); 264 BVolume volume(device); 265 if (volume.InitCheck() != B_OK) 266 break; 267 268 BMenuItem* item = fDestField->MenuItem(); 269 270 BString name = _NamePlusSizeString(path.Path(), 271 volume.FreeBytes(), B_TRANSLATE("%name% (%size% free)")); 272 273 item->SetLabel(name.String()); 274 fCurrentPath.SetTo(path.Path()); 275 } 276 break; 277 } 278 279 case B_CANCEL: 280 { 281 if (!_ValidateFilePanelMessage(message)) 282 break; 283 284 // file panel aborted, select first suitable item 285 for (int32 i = 0; i < fDestination->CountItems(); i++) { 286 BMenuItem* item = fDestination->ItemAt(i); 287 BMessage* message = item->Message(); 288 if (message == NULL) 289 continue; 290 BString path; 291 if (message->FindString("path", &path) == B_OK) { 292 fCurrentPath.SetTo(path.String()); 293 item->SetMarked(true); 294 break; 295 } 296 } 297 break; 298 } 299 300 case B_SIMPLE_DATA: 301 if (message->WasDropped()) { 302 uint32 type; 303 int32 count; 304 status_t ret = message->GetInfo("refs", &type, &count); 305 // check whether the message means someone dropped a file 306 // to our view 307 if (ret == B_OK && type == B_REF_TYPE) { 308 // if it is, send it along with the refs to the application 309 message->what = B_REFS_RECEIVED; 310 be_app->PostMessage(message); 311 } 312 } 313 // fall-through 314 default: 315 BView::MessageReceived(message); 316 break; 317 } 318 } 319 320 321 int32 322 PackageView::ItemExists(PackageItem& item, BPath& path, int32& policy) 323 { 324 int32 choice = P_EXISTS_NONE; 325 326 switch (policy) { 327 case P_EXISTS_OVERWRITE: 328 choice = P_EXISTS_OVERWRITE; 329 break; 330 331 case P_EXISTS_SKIP: 332 choice = P_EXISTS_SKIP; 333 break; 334 335 case P_EXISTS_ASK: 336 case P_EXISTS_NONE: 337 { 338 const char* formatString; 339 switch (item.ItemKind()) { 340 case P_KIND_SCRIPT: 341 formatString = B_TRANSLATE("The script named \'%s\' " 342 "already exists in the given path.\nReplace the script " 343 "with the one from this package or skip it?"); 344 break; 345 case P_KIND_FILE: 346 formatString = B_TRANSLATE("The file named \'%s\' already " 347 "exists in the given path.\nReplace the file with the " 348 "one from this package or skip it?"); 349 break; 350 case P_KIND_DIRECTORY: 351 formatString = B_TRANSLATE("The directory named \'%s\' " 352 "already exists in the given path.\nReplace the " 353 "directory with one from this package or skip it?"); 354 break; 355 case P_KIND_SYM_LINK: 356 formatString = B_TRANSLATE("The symbolic link named \'%s\' " 357 "already exists in the given path.\nReplace the link " 358 "with the one from this package or skip it?"); 359 break; 360 default: 361 formatString = B_TRANSLATE("The item named \'%s\' already " 362 "exists in the given path.\nReplace the item with the " 363 "one from this package or skip it?"); 364 break; 365 } 366 char buffer[512]; 367 snprintf(buffer, sizeof(buffer), formatString, path.Leaf()); 368 369 BString alertString = buffer; 370 371 BAlert* alert = new BAlert("file_exists", alertString.String(), 372 B_TRANSLATE("Replace"), 373 B_TRANSLATE("Skip"), 374 B_TRANSLATE("Abort")); 375 alert->SetShortcut(2, B_ESCAPE); 376 377 choice = alert->Go(); 378 switch (choice) { 379 case 0: 380 choice = P_EXISTS_OVERWRITE; 381 break; 382 case 1: 383 choice = P_EXISTS_SKIP; 384 break; 385 default: 386 return P_EXISTS_ABORT; 387 } 388 389 if (policy == P_EXISTS_NONE) { 390 // TODO: Maybe add 'No, but ask again' type of choice as well? 391 alertString = B_TRANSLATE("Do you want to remember this " 392 "decision for the rest of this installation?\n"); 393 394 BString actionString; 395 if (choice == P_EXISTS_OVERWRITE) { 396 alertString << B_TRANSLATE( 397 "All existing files will be replaced?"); 398 actionString = B_TRANSLATE("Replace all"); 399 } else { 400 alertString << B_TRANSLATE( 401 "All existing files will be skipped?"); 402 actionString = B_TRANSLATE("Skip all"); 403 } 404 alert = new BAlert("policy_decision", alertString.String(), 405 actionString.String(), B_TRANSLATE("Ask again")); 406 407 int32 decision = alert->Go(); 408 if (decision == 0) 409 policy = choice; 410 else 411 policy = P_EXISTS_ASK; 412 } 413 break; 414 } 415 } 416 417 return choice; 418 } 419 420 421 // #pragma mark - 422 423 424 class DescriptionTextView : public BTextView { 425 public: 426 DescriptionTextView(const char* name, float minHeight) 427 : 428 BTextView(name) 429 { 430 SetExplicitMinSize(BSize(B_SIZE_UNSET, minHeight)); 431 } 432 433 virtual void AttachedToWindow() 434 { 435 BTextView::AttachedToWindow(); 436 _UpdateScrollBarVisibility(); 437 } 438 439 virtual void FrameResized(float width, float height) 440 { 441 BTextView::FrameResized(width, height); 442 _UpdateScrollBarVisibility(); 443 } 444 445 virtual void Draw(BRect updateRect) 446 { 447 BTextView::Draw(updateRect); 448 _UpdateScrollBarVisibility(); 449 } 450 451 private: 452 void _UpdateScrollBarVisibility() 453 { 454 BScrollBar* verticalBar = ScrollBar(B_VERTICAL); 455 if (verticalBar != NULL) { 456 float min; 457 float max; 458 verticalBar->GetRange(&min, &max); 459 if (min == max) { 460 if (!verticalBar->IsHidden(verticalBar)) 461 verticalBar->Hide(); 462 } else { 463 if (verticalBar->IsHidden(verticalBar)) 464 verticalBar->Show(); 465 } 466 } 467 } 468 }; 469 470 471 void 472 PackageView::_InitView() 473 { 474 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 475 476 float fontHeight = be_plain_font->Size(); 477 rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); 478 479 BTextView* packageDescriptionView = new DescriptionTextView( 480 "package description", fontHeight * 13); 481 packageDescriptionView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 482 packageDescriptionView->SetText(fInfo.GetDescription()); 483 packageDescriptionView->MakeEditable(false); 484 packageDescriptionView->MakeSelectable(false); 485 packageDescriptionView->SetFontAndColor(be_plain_font, B_FONT_ALL, 486 &textColor); 487 488 BScrollView* descriptionScrollView = new BScrollView( 489 "package description scroll view", packageDescriptionView, 490 0, false, true, B_NO_BORDER); 491 492 // Install type menu field 493 fInstallTypes = new BPopUpMenu(B_TRANSLATE("none")); 494 BMenuField* installType = new BMenuField("install_type", 495 B_TRANSLATE("Installation type:"), fInstallTypes); 496 497 // Install type description text view 498 fInstallTypeDescriptionView = new DescriptionTextView( 499 "install type description", fontHeight * 4); 500 fInstallTypeDescriptionView->MakeEditable(false); 501 fInstallTypeDescriptionView->MakeSelectable(false); 502 fInstallTypeDescriptionView->SetInsets(8, 0, 0, 0); 503 // Left inset needs to match BMenuField text offset 504 fInstallTypeDescriptionView->SetText( 505 B_TRANSLATE("No installation type selected")); 506 fInstallTypeDescriptionView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 507 BFont font(be_plain_font); 508 font.SetSize(ceilf(font.Size() * 0.85)); 509 fInstallTypeDescriptionView->SetFontAndColor(&font, B_FONT_ALL, 510 &textColor); 511 512 BScrollView* installTypeScrollView = new BScrollView( 513 "install type description scroll view", fInstallTypeDescriptionView, 514 0, false, true, B_NO_BORDER); 515 516 // Destination menu field 517 fDestination = new BPopUpMenu(B_TRANSLATE("none")); 518 fDestField = new BMenuField("install_to", B_TRANSLATE("Install to:"), 519 fDestination); 520 521 fBeginButton = new BButton("begin_button", B_TRANSLATE("Begin"), 522 new BMessage(P_MSG_INSTALL)); 523 524 BLayoutItem* typeLabelItem = installType->CreateLabelLayoutItem(); 525 BLayoutItem* typeMenuItem = installType->CreateMenuBarLayoutItem(); 526 527 BLayoutItem* destFieldLabelItem = fDestField->CreateLabelLayoutItem(); 528 BLayoutItem* destFieldMenuItem = fDestField->CreateMenuBarLayoutItem(); 529 530 float forcedMinWidth = be_plain_font->StringWidth("XXX") * 5; 531 destFieldMenuItem->SetExplicitMinSize(BSize(forcedMinWidth, B_SIZE_UNSET)); 532 typeMenuItem->SetExplicitMinSize(BSize(forcedMinWidth, B_SIZE_UNSET)); 533 534 BAlignment labelAlignment(B_ALIGN_RIGHT, B_ALIGN_VERTICAL_UNSET); 535 typeLabelItem->SetExplicitAlignment(labelAlignment); 536 destFieldLabelItem->SetExplicitAlignment(labelAlignment); 537 538 // Build the layout 539 BLayoutBuilder::Group<>(this, B_VERTICAL) 540 .Add(descriptionScrollView) 541 .AddGrid(B_USE_SMALL_SPACING, B_USE_DEFAULT_SPACING) 542 .Add(typeLabelItem, 0, 0) 543 .Add(typeMenuItem, 1, 0) 544 .Add(installTypeScrollView, 1, 1) 545 .Add(destFieldLabelItem, 0, 2) 546 .Add(destFieldMenuItem, 1, 2) 547 .End() 548 .AddGroup(B_HORIZONTAL) 549 .AddGlue() 550 .Add(fBeginButton) 551 .End() 552 .SetInsets(B_USE_DEFAULT_SPACING) 553 ; 554 555 fBeginButton->MakeDefault(true); 556 } 557 558 559 void 560 PackageView::_InitProfiles() 561 { 562 int count = fInfo.GetProfileCount(); 563 564 if (count > 0) { 565 // Add the default item 566 pkg_profile* profile = fInfo.GetProfile(0); 567 BMenuItem* item = _AddInstallTypeMenuItem(profile->name, 568 profile->space_needed, 0); 569 item->SetMarked(true); 570 fCurrentType = 0; 571 } 572 573 for (int i = 1; i < count; i++) { 574 pkg_profile* profile = fInfo.GetProfile(i); 575 576 if (profile != NULL) 577 _AddInstallTypeMenuItem(profile->name, profile->space_needed, i); 578 else 579 fInstallTypes->AddSeparatorItem(); 580 } 581 } 582 583 584 status_t 585 PackageView::_InstallTypeChanged(int32 index) 586 { 587 if (index < 0) 588 return B_ERROR; 589 590 // Clear the choice list 591 for (int32 i = fDestination->CountItems() - 1; i >= 0; i--) { 592 BMenuItem* item = fDestination->RemoveItem(i); 593 delete item; 594 } 595 596 fCurrentType = index; 597 pkg_profile* profile = fInfo.GetProfile(index); 598 599 if (profile == NULL) 600 return B_ERROR; 601 602 BString typeDescription = profile->description; 603 if (typeDescription.IsEmpty()) 604 typeDescription = profile->name; 605 606 fInstallTypeDescriptionView->SetText(typeDescription.String()); 607 608 BPath path; 609 BVolume volume; 610 611 if (profile->path_type == P_INSTALL_PATH) { 612 BMenuItem* item = NULL; 613 if (find_directory(B_SYSTEM_NONPACKAGED_DIRECTORY, &path) == B_OK) { 614 dev_t device = dev_for_path(path.Path()); 615 if (volume.SetTo(device) == B_OK && !volume.IsReadOnly() 616 && path.Append("apps") == B_OK) { 617 item = _AddDestinationMenuItem(path.Path(), volume.FreeBytes(), 618 path.Path()); 619 } 620 } 621 622 if (item != NULL) { 623 item->SetMarked(true); 624 fCurrentPath.SetTo(path.Path()); 625 fDestination->AddSeparatorItem(); 626 } 627 628 _AddMenuItem(B_TRANSLATE("Other" B_UTF8_ELLIPSIS), 629 new BMessage(P_MSG_OPEN_PANEL), fDestination); 630 631 fDestField->SetEnabled(true); 632 } else if (profile->path_type == P_USER_PATH) { 633 bool defaultPathSet = false; 634 BVolumeRoster roster; 635 636 while (roster.GetNextVolume(&volume) != B_BAD_VALUE) { 637 BDirectory mountPoint; 638 if (volume.IsReadOnly() || !volume.IsPersistent() 639 || volume.GetRootDirectory(&mountPoint) != B_OK) { 640 continue; 641 } 642 643 if (path.SetTo(&mountPoint, NULL) != B_OK) 644 continue; 645 646 char volumeName[B_FILE_NAME_LENGTH]; 647 volume.GetName(volumeName); 648 649 BMenuItem* item = _AddDestinationMenuItem(volumeName, 650 volume.FreeBytes(), path.Path()); 651 652 // The first volume becomes the default element 653 if (!defaultPathSet) { 654 item->SetMarked(true); 655 fCurrentPath.SetTo(path.Path()); 656 defaultPathSet = true; 657 } 658 } 659 660 fDestField->SetEnabled(true); 661 } else 662 fDestField->SetEnabled(false); 663 664 return B_OK; 665 } 666 667 668 BString 669 PackageView::_NamePlusSizeString(BString baseName, size_t size, 670 const char* format) const 671 { 672 char sizeString[48]; 673 string_for_size(size, sizeString, sizeof(sizeString)); 674 675 BString name(format); 676 name.ReplaceAll("%name%", baseName); 677 name.ReplaceAll("%size%", sizeString); 678 679 return name; 680 } 681 682 683 BMenuItem* 684 PackageView::_AddInstallTypeMenuItem(BString baseName, size_t size, 685 int32 index) const 686 { 687 BString name = _NamePlusSizeString(baseName, size, 688 B_TRANSLATE("%name% (%size%)")); 689 690 BMessage* message = new BMessage(P_MSG_INSTALL_TYPE_CHANGED); 691 message->AddInt32("index", index); 692 693 return _AddMenuItem(name, message, fInstallTypes); 694 } 695 696 697 BMenuItem* 698 PackageView::_AddDestinationMenuItem(BString baseName, size_t size, 699 const char* path) const 700 { 701 BString name = _NamePlusSizeString(baseName, size, 702 B_TRANSLATE("%name% (%size% free)")); 703 704 BMessage* message = new BMessage(P_MSG_PATH_CHANGED); 705 message->AddString("path", path); 706 707 return _AddMenuItem(name, message, fDestination); 708 } 709 710 711 BMenuItem* 712 PackageView::_AddMenuItem(const char* name, BMessage* message, 713 BMenu* menu) const 714 { 715 BMenuItem* item = new BMenuItem(name, message); 716 item->SetTarget(this); 717 menu->AddItem(item); 718 return item; 719 } 720 721 722 bool 723 PackageView::_ValidateFilePanelMessage(BMessage* message) 724 { 725 if (!fExpectingOpenPanelResult) 726 return false; 727 728 fExpectingOpenPanelResult = false; 729 return true; 730 } 731