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