1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2018, Andrew Lindesay <apl@lindesay.co.nz>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "PackageInfoView.h" 8 9 #include <algorithm> 10 #include <stdio.h> 11 12 #include <Alert.h> 13 #include <Autolock.h> 14 #include <Bitmap.h> 15 #include <Button.h> 16 #include <CardLayout.h> 17 #include <Catalog.h> 18 #include <ColumnListView.h> 19 #include <DateFormat.h> 20 #include <Font.h> 21 #include <GridView.h> 22 #include <LayoutBuilder.h> 23 #include <LayoutUtils.h> 24 #include <LocaleRoster.h> 25 #include <Message.h> 26 #include <OutlineListView.h> 27 #include <ScrollView.h> 28 #include <SpaceLayoutItem.h> 29 #include <StatusBar.h> 30 #include <StringView.h> 31 #include <TabView.h> 32 #include <Url.h> 33 34 #include <package/hpkg/PackageReader.h> 35 #include <package/hpkg/NoErrorOutput.h> 36 #include <package/hpkg/PackageContentHandler.h> 37 #include <package/hpkg/PackageEntry.h> 38 39 #include "BitmapButton.h" 40 #include "BitmapView.h" 41 #include "LinkView.h" 42 #include "LinkedBitmapView.h" 43 #include "MarkupTextView.h" 44 #include "MessagePackageListener.h" 45 #include "PackageActionHandler.h" 46 #include "PackageContentsView.h" 47 #include "PackageInfo.h" 48 #include "PackageManager.h" 49 #include "RatingView.h" 50 #include "ScrollableGroupView.h" 51 #include "TextView.h" 52 53 54 #undef B_TRANSLATION_CONTEXT 55 #define B_TRANSLATION_CONTEXT "PackageInfoView" 56 57 58 static const rgb_color kLightBlack = (rgb_color) { 60, 60, 60, 255 }; 59 static const float kContentTint = (B_NO_TINT + B_LIGHTEN_1_TINT) / 2.0f; 60 61 62 //! Layouts the scrollbar so it looks nice with no border and the document 63 // window look. 64 class CustomScrollView : public BScrollView { 65 public: 66 CustomScrollView(const char* name, BView* target) 67 : 68 BScrollView(name, target, 0, false, true, B_NO_BORDER) 69 { 70 } 71 72 virtual void DoLayout() 73 { 74 BRect innerFrame = Bounds(); 75 innerFrame.right -= B_V_SCROLL_BAR_WIDTH + 1; 76 77 BView* target = Target(); 78 if (target != NULL) { 79 Target()->MoveTo(innerFrame.left, innerFrame.top); 80 Target()->ResizeTo(innerFrame.Width(), innerFrame.Height()); 81 } 82 83 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 84 if (scrollBar != NULL) { 85 BRect rect = innerFrame; 86 rect.left = rect.right + 1; 87 rect.right = rect.left + B_V_SCROLL_BAR_WIDTH; 88 rect.bottom -= B_H_SCROLL_BAR_HEIGHT; 89 90 scrollBar->MoveTo(rect.left, rect.top); 91 scrollBar->ResizeTo(rect.Width(), rect.Height()); 92 } 93 } 94 }; 95 96 97 class RatingsScrollView : public CustomScrollView { 98 public: 99 RatingsScrollView(const char* name, BView* target) 100 : 101 CustomScrollView(name, target) 102 { 103 } 104 105 virtual void DoLayout() 106 { 107 CustomScrollView::DoLayout(); 108 109 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 110 BView* target = Target(); 111 if (target != NULL && scrollBar != NULL) { 112 // Set the scroll steps 113 BView* item = target->ChildAt(0); 114 if (item != NULL) { 115 scrollBar->SetSteps(item->MinSize().height + 1, 116 item->MinSize().height + 1); 117 } 118 } 119 } 120 }; 121 122 123 // #pragma mark - rating stats 124 125 126 class DiagramBarView : public BView { 127 public: 128 DiagramBarView() 129 : 130 BView("diagram bar view", B_WILL_DRAW), 131 fValue(0.0f) 132 { 133 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 134 SetHighUIColor(B_CONTROL_MARK_COLOR); 135 } 136 137 virtual ~DiagramBarView() 138 { 139 } 140 141 virtual void AttachedToWindow() 142 { 143 } 144 145 virtual void Draw(BRect updateRect) 146 { 147 FillRect(updateRect, B_SOLID_LOW); 148 149 if (fValue <= 0.0f) 150 return; 151 152 BRect rect(Bounds()); 153 rect.right = ceilf(rect.left + fValue * rect.Width()); 154 155 FillRect(rect, B_SOLID_HIGH); 156 } 157 158 virtual BSize MinSize() 159 { 160 return BSize(64, 10); 161 } 162 163 virtual BSize PreferredSize() 164 { 165 return MinSize(); 166 } 167 168 virtual BSize MaxSize() 169 { 170 return BSize(64, 10); 171 } 172 173 void SetValue(float value) 174 { 175 if (fValue != value) { 176 fValue = value; 177 Invalidate(); 178 } 179 } 180 181 private: 182 float fValue; 183 }; 184 185 186 // #pragma mark - TitleView 187 188 189 enum { 190 MSG_PACKAGE_ACTION = 'pkga', 191 MSG_MOUSE_ENTERED_RATING = 'menr', 192 MSG_MOUSE_EXITED_RATING = 'mexr', 193 }; 194 195 196 class TransitReportingButton : public BButton { 197 public: 198 TransitReportingButton(const char* name, const char* label, 199 BMessage* message) 200 : 201 BButton(name, label, message), 202 fTransitMessage(NULL) 203 { 204 } 205 206 virtual ~TransitReportingButton() 207 { 208 SetTransitMessage(NULL); 209 } 210 211 virtual void MouseMoved(BPoint point, uint32 transit, 212 const BMessage* dragMessage) 213 { 214 BButton::MouseMoved(point, transit, dragMessage); 215 216 if (fTransitMessage != NULL && transit == B_EXITED_VIEW) 217 Invoke(fTransitMessage); 218 } 219 220 void SetTransitMessage(BMessage* message) 221 { 222 if (fTransitMessage != message) { 223 delete fTransitMessage; 224 fTransitMessage = message; 225 } 226 } 227 228 private: 229 BMessage* fTransitMessage; 230 }; 231 232 233 class TransitReportingRatingView : public RatingView, public BInvoker { 234 public: 235 TransitReportingRatingView(BMessage* transitMessage) 236 : 237 RatingView("package rating view"), 238 fTransitMessage(transitMessage) 239 { 240 } 241 242 virtual ~TransitReportingRatingView() 243 { 244 delete fTransitMessage; 245 } 246 247 virtual void MouseMoved(BPoint point, uint32 transit, 248 const BMessage* dragMessage) 249 { 250 RatingView::MouseMoved(point, transit, dragMessage); 251 252 if (fTransitMessage != NULL && transit == B_ENTERED_VIEW) 253 Invoke(fTransitMessage); 254 } 255 256 private: 257 BMessage* fTransitMessage; 258 }; 259 260 261 class TitleView : public BGroupView { 262 public: 263 TitleView() 264 : 265 BGroupView("title view", B_HORIZONTAL) 266 { 267 fIconView = new BitmapView("package icon view"); 268 fTitleView = new BStringView("package title view", ""); 269 fPublisherView = new BStringView("package publisher view", ""); 270 271 // Title font 272 BFont font; 273 GetFont(&font); 274 font_family family; 275 font_style style; 276 font.SetSize(ceilf(font.Size() * 1.5f)); 277 font.GetFamilyAndStyle(&family, &style); 278 font.SetFamilyAndStyle(family, "Bold"); 279 fTitleView->SetFont(&font); 280 281 // Publisher font 282 GetFont(&font); 283 font.SetSize(std::max(9.0f, floorf(font.Size() * 0.92f))); 284 font.SetFamilyAndStyle(family, "Italic"); 285 fPublisherView->SetFont(&font); 286 fPublisherView->SetHighColor(kLightBlack); 287 288 // slightly bigger font 289 GetFont(&font); 290 font.SetSize(ceilf(font.Size() * 1.2f)); 291 292 // Version info 293 fVersionInfo = new BStringView("package version info", ""); 294 fVersionInfo->SetFont(&font); 295 fVersionInfo->SetHighColor(kLightBlack); 296 297 // Rating view 298 fRatingView = new TransitReportingRatingView( 299 new BMessage(MSG_MOUSE_ENTERED_RATING)); 300 301 fAvgRating = new BStringView("package average rating", ""); 302 fAvgRating->SetFont(&font); 303 fAvgRating->SetHighColor(kLightBlack); 304 305 fVoteInfo = new BStringView("package vote info", ""); 306 // small font 307 GetFont(&font); 308 font.SetSize(std::max(9.0f, floorf(font.Size() * 0.85f))); 309 fVoteInfo->SetFont(&font); 310 fVoteInfo->SetHighColor(kLightBlack); 311 312 // Rate button 313 fRateButton = new TransitReportingButton("rate", 314 B_TRANSLATE("Rate package" B_UTF8_ELLIPSIS), 315 new BMessage(MSG_RATE_PACKAGE)); 316 fRateButton->SetTransitMessage(new BMessage(MSG_MOUSE_EXITED_RATING)); 317 fRateButton->SetExplicitAlignment(BAlignment(B_ALIGN_LEFT, 318 B_ALIGN_VERTICAL_CENTER)); 319 320 // Rating group 321 BView* ratingStack = new BView("rating stack", 0); 322 fRatingLayout = new BCardLayout(); 323 ratingStack->SetLayout(fRatingLayout); 324 ratingStack->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 325 ratingStack->SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 326 327 BGroupView* ratingGroup = new BGroupView(B_HORIZONTAL, 328 B_USE_SMALL_SPACING); 329 BLayoutBuilder::Group<>(ratingGroup) 330 .Add(fRatingView) 331 .Add(fAvgRating) 332 .Add(fVoteInfo) 333 ; 334 335 ratingStack->AddChild(ratingGroup); 336 ratingStack->AddChild(fRateButton); 337 fRatingLayout->SetVisibleItem((int32)0); 338 339 BLayoutBuilder::Group<>(this) 340 .Add(fIconView) 341 .AddGroup(B_VERTICAL, 1.0f, 2.2f) 342 .Add(fTitleView) 343 .Add(fPublisherView) 344 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)) 345 .End() 346 .AddGlue(0.1f) 347 .Add(ratingStack, 0.8f) 348 .AddGlue(0.2f) 349 .AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING, 2.0f) 350 .Add(fVersionInfo) 351 .AddGlue() 352 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)) 353 .End() 354 ; 355 356 Clear(); 357 } 358 359 virtual ~TitleView() 360 { 361 } 362 363 virtual void AttachedToWindow() 364 { 365 fRateButton->SetTarget(this); 366 fRatingView->SetTarget(this); 367 } 368 369 virtual void MessageReceived(BMessage* message) 370 { 371 switch (message->what) { 372 case MSG_RATE_PACKAGE: 373 // Forward to window (The button has us as target so 374 // we receive the message below.) 375 Window()->PostMessage(MSG_RATE_PACKAGE); 376 break; 377 378 case MSG_MOUSE_ENTERED_RATING: 379 fRatingLayout->SetVisibleItem(1); 380 break; 381 382 case MSG_MOUSE_EXITED_RATING: 383 fRatingLayout->SetVisibleItem((int32)0); 384 break; 385 } 386 } 387 388 void SetPackage(const PackageInfo& package) 389 { 390 if (package.Icon().Get() != NULL) 391 fIconView->SetBitmap(package.Icon(), SharedBitmap::SIZE_32); 392 else 393 fIconView->UnsetBitmap(); 394 395 fTitleView->SetText(package.Title()); 396 397 BString publisher = package.Publisher().Name(); 398 if (publisher.CountChars() > 45) { 399 fPublisherView->SetToolTip(publisher); 400 fPublisherView->SetText(publisher.TruncateChars(45) 401 .Append(B_UTF8_ELLIPSIS)); 402 } else 403 fPublisherView->SetText(publisher); 404 405 BString version = B_TRANSLATE("%Version%"); 406 version.ReplaceAll("%Version%", package.Version().ToString()); 407 fVersionInfo->SetText(version); 408 409 RatingSummary ratingSummary = package.CalculateRatingSummary(); 410 411 fRatingView->SetRating(ratingSummary.averageRating); 412 413 if (ratingSummary.ratingCount > 0) { 414 BString avgRating; 415 avgRating.SetToFormat("%.1f", ratingSummary.averageRating); 416 fAvgRating->SetText(avgRating); 417 418 BString votes; 419 votes.SetToFormat("%d", ratingSummary.ratingCount); 420 421 BString voteInfo(B_TRANSLATE("(%Votes%)")); 422 voteInfo.ReplaceAll("%Votes%", votes); 423 424 fVoteInfo->SetText(voteInfo); 425 } else { 426 fAvgRating->SetText(""); 427 fVoteInfo->SetText(B_TRANSLATE("n/a")); 428 } 429 430 InvalidateLayout(); 431 Invalidate(); 432 } 433 434 void Clear() 435 { 436 fIconView->UnsetBitmap(); 437 fTitleView->SetText(""); 438 fPublisherView->SetText(""); 439 fVersionInfo->SetText(""); 440 fRatingView->SetRating(-1.0f); 441 fAvgRating->SetText(""); 442 fVoteInfo->SetText(""); 443 } 444 445 private: 446 BitmapView* fIconView; 447 448 BStringView* fTitleView; 449 BStringView* fPublisherView; 450 451 BStringView* fVersionInfo; 452 453 BCardLayout* fRatingLayout; 454 455 TransitReportingRatingView* fRatingView; 456 BStringView* fAvgRating; 457 BStringView* fVoteInfo; 458 459 TransitReportingButton* fRateButton; 460 }; 461 462 463 // #pragma mark - PackageActionView 464 465 466 class PackageActionView : public BView { 467 public: 468 PackageActionView(PackageActionHandler* handler) 469 : 470 BView("about view", B_WILL_DRAW), 471 fLayout(new BGroupLayout(B_HORIZONTAL)), 472 fPackageActionHandler(handler), 473 fStatusLabel(NULL), 474 fStatusBar(NULL) 475 { 476 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 477 478 SetLayout(fLayout); 479 fLayout->AddItem(BSpaceLayoutItem::CreateGlue()); 480 } 481 482 virtual ~PackageActionView() 483 { 484 Clear(); 485 } 486 487 virtual void MessageReceived(BMessage* message) 488 { 489 switch (message->what) { 490 case MSG_PACKAGE_ACTION: 491 _RunPackageAction(message); 492 break; 493 494 default: 495 BView::MessageReceived(message); 496 break; 497 } 498 } 499 500 void SetPackage(const PackageInfo& package) 501 { 502 if (package.State() == DOWNLOADING) { 503 AdoptDownloadProgress(package); 504 } else { 505 AdoptActions(package); 506 } 507 508 } 509 510 void AdoptActions(const PackageInfo& package) 511 { 512 PackageManager manager( 513 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME); 514 515 // TODO: if the given package is either a system package 516 // or a system dependency, show a message indicating that status 517 // so the user knows why no actions are presented 518 PackageActionList actions = manager.GetPackageActions( 519 const_cast<PackageInfo*>(&package), 520 fPackageActionHandler->GetModel()); 521 522 bool clearNeeded = fStatusBar != NULL; 523 if (!clearNeeded) { 524 if (actions.CountItems() != fPackageActions.CountItems()) 525 clearNeeded = true; 526 else { 527 for (int32 i = 0; i < actions.CountItems(); i++) { 528 if (actions.ItemAtFast(i)->Type() 529 != fPackageActions.ItemAtFast(i)->Type()) { 530 clearNeeded = true; 531 break; 532 } 533 } 534 } 535 } 536 537 fPackageActions = actions; 538 if (!clearNeeded && fButtons.CountItems() == actions.CountItems()) { 539 int32 index = 0; 540 for (int32 i = fPackageActions.CountItems() - 1; i >= 0; i--) { 541 const PackageActionRef& action = fPackageActions.ItemAtFast(i); 542 BButton* button = (BButton*)fButtons.ItemAtFast(index++); 543 button->SetLabel(action->Label()); 544 } 545 return; 546 } 547 548 Clear(); 549 550 // Add Buttons in reverse action order 551 for (int32 i = fPackageActions.CountItems() - 1; i >= 0; i--) { 552 const PackageActionRef& action = fPackageActions.ItemAtFast(i); 553 554 BMessage* message = new BMessage(MSG_PACKAGE_ACTION); 555 message->AddInt32("index", i); 556 557 BButton* button = new BButton(action->Label(), message); 558 fLayout->AddView(button); 559 button->SetTarget(this); 560 561 fButtons.AddItem(button); 562 } 563 } 564 565 void AdoptDownloadProgress(const PackageInfo& package) 566 { 567 if (fButtons.CountItems() > 0) 568 Clear(); 569 570 if (fStatusBar == NULL) { 571 fStatusLabel = new BStringView("progress label", 572 B_TRANSLATE("Downloading:")); 573 fLayout->AddView(fStatusLabel); 574 575 fStatusBar = new BStatusBar("progress"); 576 fStatusBar->SetMaxValue(100.0); 577 fStatusBar->SetExplicitMinSize( 578 BSize(StringWidth("XXX") * 5, B_SIZE_UNSET)); 579 580 fLayout->AddView(fStatusBar); 581 } 582 583 fStatusBar->SetTo(package.DownloadProgress() * 100.0); 584 } 585 586 void Clear() 587 { 588 for (int32 i = fButtons.CountItems() - 1; i >= 0; i--) { 589 BButton* button = (BButton*)fButtons.ItemAtFast(i); 590 button->RemoveSelf(); 591 delete button; 592 } 593 fButtons.MakeEmpty(); 594 595 if (fStatusBar != NULL) { 596 fStatusBar->RemoveSelf(); 597 delete fStatusBar; 598 fStatusBar = NULL; 599 } 600 if (fStatusLabel != NULL) { 601 fStatusLabel->RemoveSelf(); 602 delete fStatusLabel; 603 fStatusLabel = NULL; 604 } 605 } 606 607 private: 608 void _RunPackageAction(BMessage* message) 609 { 610 int32 index; 611 if (message->FindInt32("index", &index) != B_OK) 612 return; 613 614 const PackageActionRef& action = fPackageActions.ItemAt(index); 615 if (action.Get() == NULL) 616 return; 617 618 PackageActionList actions; 619 actions.Add(action); 620 status_t result 621 = fPackageActionHandler->SchedulePackageActions(actions); 622 623 if (result != B_OK) { 624 fprintf(stderr, "Failed to schedule action: " 625 "%s '%s': %s\n", action->Label(), 626 action->Package()->Name().String(), 627 strerror(result)); 628 BString message(B_TRANSLATE("The package action " 629 "could not be scheduled: %Error%")); 630 message.ReplaceAll("%Error%", strerror(result)); 631 BAlert* alert = new(std::nothrow) BAlert( 632 B_TRANSLATE("Package action failed"), 633 message, B_TRANSLATE("OK"), NULL, NULL, 634 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 635 if (alert != NULL) 636 alert->Go(); 637 } else { 638 // Find the button for this action and disable it. 639 // Actually search the matching button instead of just using 640 // fButtons.ItemAt((fButtons.CountItems() - 1) - index) to 641 // make this robust against for example changing the order of 642 // buttons from right -> left to left -> right... 643 for (int32 i = 0; i < fButtons.CountItems(); i++) { 644 BButton* button = (BButton*)fButtons.ItemAt(index); 645 if (button == NULL) 646 continue; 647 BMessage* buttonMessage = button->Message(); 648 if (buttonMessage == NULL) 649 continue; 650 int32 buttonIndex; 651 if (buttonMessage->FindInt32("index", &buttonIndex) != B_OK) 652 continue; 653 if (buttonIndex == index) { 654 button->SetEnabled(false); 655 break; 656 } 657 } 658 } 659 } 660 661 private: 662 BGroupLayout* fLayout; 663 PackageActionList fPackageActions; 664 PackageActionHandler* fPackageActionHandler; 665 BList fButtons; 666 667 BStringView* fStatusLabel; 668 BStatusBar* fStatusBar; 669 }; 670 671 672 // #pragma mark - AboutView 673 674 675 enum { 676 MSG_EMAIL_PUBLISHER = 'emlp', 677 MSG_VISIT_PUBLISHER_WEBSITE = 'vpws', 678 }; 679 680 681 class AboutView : public BView { 682 public: 683 AboutView() 684 : 685 BView("about view", 0), 686 fEmailIcon("text/x-email"), 687 fWebsiteIcon("text/html") 688 { 689 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 690 691 fDescriptionView = new MarkupTextView("description view"); 692 fDescriptionView->SetViewUIColor(ViewUIColor(), kContentTint); 693 fDescriptionView->SetInsets(be_plain_font->Size()); 694 695 BScrollView* scrollView = new CustomScrollView( 696 "description scroll view", fDescriptionView); 697 698 BFont smallFont; 699 GetFont(&smallFont); 700 smallFont.SetSize(std::max(9.0f, ceilf(smallFont.Size() * 0.85f))); 701 702 // TODO: Clicking the screen shot view should open ShowImage with the 703 // the screen shot. This could be done by writing the screen shot to 704 // a temporary folder, launching ShowImage to display it, and writing 705 // all other screenshots associated with the package to the same folder 706 // so the user can use the ShowImage navigation to view the other 707 // screenshots. 708 fScreenshotView = new LinkedBitmapView("screenshot view", 709 new BMessage(MSG_SHOW_SCREENSHOT)); 710 fScreenshotView->SetExplicitMinSize(BSize(64.0f, 64.0f)); 711 fScreenshotView->SetExplicitMaxSize( 712 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)); 713 fScreenshotView->SetExplicitAlignment( 714 BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP)); 715 716 fEmailIconView = new BitmapView("email icon view"); 717 fEmailLinkView = new LinkView("email link view", "", 718 new BMessage(MSG_EMAIL_PUBLISHER)); 719 fEmailLinkView->SetFont(&smallFont); 720 721 fWebsiteIconView = new BitmapView("website icon view"); 722 fWebsiteLinkView = new LinkView("website link view", "", 723 new BMessage(MSG_VISIT_PUBLISHER_WEBSITE)); 724 fWebsiteLinkView->SetFont(&smallFont); 725 726 BGroupView* leftGroup = new BGroupView(B_VERTICAL, 727 B_USE_DEFAULT_SPACING); 728 729 fScreenshotView->SetViewUIColor(ViewUIColor(), kContentTint); 730 fEmailLinkView->SetViewUIColor(ViewUIColor(), kContentTint); 731 fWebsiteLinkView->SetViewUIColor(ViewUIColor(), kContentTint); 732 733 BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0.0f) 734 .AddGroup(leftGroup, 1.0f) 735 .Add(fScreenshotView) 736 .AddGroup(B_HORIZONTAL) 737 .AddGrid(B_USE_HALF_ITEM_SPACING, B_USE_HALF_ITEM_SPACING) 738 .Add(fEmailIconView, 0, 0) 739 .Add(fEmailLinkView, 1, 0) 740 .Add(fWebsiteIconView, 0, 1) 741 .Add(fWebsiteLinkView, 1, 1) 742 .End() 743 .End() 744 .SetInsets(B_USE_DEFAULT_SPACING) 745 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)) 746 .End() 747 .Add(scrollView, 2.0f) 748 749 .SetExplicitMaxSize(BSize(B_SIZE_UNSET, B_SIZE_UNLIMITED)) 750 .SetInsets(0.0f, -1.0f, -1.0f, -1.0f) 751 ; 752 } 753 754 virtual ~AboutView() 755 { 756 Clear(); 757 } 758 759 virtual void AttachedToWindow() 760 { 761 fScreenshotView->SetTarget(this); 762 fEmailLinkView->SetTarget(this); 763 fWebsiteLinkView->SetTarget(this); 764 } 765 766 virtual void AllAttached() 767 { 768 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 769 770 for (int32 index = 0; index < CountChildren(); ++index) 771 ChildAt(index)->AdoptParentColors(); 772 } 773 774 virtual void MessageReceived(BMessage* message) 775 { 776 switch (message->what) { 777 case MSG_SHOW_SCREENSHOT: 778 { 779 // Forward to window for now 780 Window()->PostMessage(message, Window()); 781 break; 782 } 783 784 case MSG_EMAIL_PUBLISHER: 785 { 786 // TODO: Implement. If memory serves, there is a 787 // standard command line interface which mail apps should 788 // support, i.e. to open a compose window with the TO: field 789 // already set. 790 break; 791 } 792 793 case MSG_VISIT_PUBLISHER_WEBSITE: 794 { 795 BUrl url(fWebsiteLinkView->Text()); 796 url.OpenWithPreferredApplication(); 797 break; 798 } 799 800 default: 801 BView::MessageReceived(message); 802 break; 803 } 804 } 805 806 void SetPackage(const PackageInfo& package) 807 { 808 fDescriptionView->SetText(package.ShortDescription(), 809 package.FullDescription()); 810 811 fEmailIconView->SetBitmap(&fEmailIcon, SharedBitmap::SIZE_16); 812 _SetContactInfo(fEmailLinkView, package.Publisher().Email()); 813 fWebsiteIconView->SetBitmap(&fWebsiteIcon, SharedBitmap::SIZE_16); 814 _SetContactInfo(fWebsiteLinkView, package.Publisher().Website()); 815 816 bool hasScreenshot = false; 817 const BitmapList& screenShots = package.Screenshots(); 818 if (screenShots.CountItems() > 0) { 819 const BitmapRef& bitmapRef = screenShots.ItemAtFast(0); 820 if (bitmapRef.Get() != NULL) { 821 hasScreenshot = true; 822 fScreenshotView->SetBitmap(bitmapRef); 823 } 824 } 825 826 if (!hasScreenshot) 827 fScreenshotView->UnsetBitmap(); 828 829 fScreenshotView->SetEnabled(hasScreenshot); 830 } 831 832 void Clear() 833 { 834 fDescriptionView->SetText(""); 835 fEmailIconView->UnsetBitmap(); 836 fEmailLinkView->SetText(""); 837 fWebsiteIconView->UnsetBitmap(); 838 fWebsiteLinkView->SetText(""); 839 840 fScreenshotView->UnsetBitmap(); 841 fScreenshotView->SetEnabled(false); 842 } 843 844 private: 845 void _SetContactInfo(LinkView* view, const BString& string) 846 { 847 if (string.Length() > 0) { 848 view->SetText(string); 849 view->SetEnabled(true); 850 } else { 851 view->SetText(B_TRANSLATE("<no info>")); 852 view->SetEnabled(false); 853 } 854 } 855 856 private: 857 MarkupTextView* fDescriptionView; 858 859 LinkedBitmapView* fScreenshotView; 860 861 SharedBitmap fEmailIcon; 862 BitmapView* fEmailIconView; 863 LinkView* fEmailLinkView; 864 865 SharedBitmap fWebsiteIcon; 866 BitmapView* fWebsiteIconView; 867 LinkView* fWebsiteLinkView; 868 }; 869 870 871 // #pragma mark - UserRatingsView 872 873 874 class RatingItemView : public BGroupView { 875 public: 876 RatingItemView(const UserRating& rating) 877 : 878 BGroupView(B_HORIZONTAL, 0.0f) 879 { 880 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 881 882 BGroupLayout* verticalGroup = new BGroupLayout(B_VERTICAL, 0.0f); 883 GroupLayout()->AddItem(verticalGroup); 884 885 { 886 BStringView* userNicknameView = new BStringView("user-nickname", 887 rating.User().NickName()); 888 userNicknameView->SetFont(be_bold_font); 889 verticalGroup->AddView(userNicknameView); 890 } 891 892 BGroupLayout* ratingGroup = 893 new BGroupLayout(B_HORIZONTAL, B_USE_DEFAULT_SPACING); 894 verticalGroup->AddItem(ratingGroup); 895 896 if (rating.Rating() >= 0) { 897 RatingView* ratingView = new RatingView("package rating view"); 898 ratingView->SetRating(rating.Rating()); 899 ratingGroup->AddView(ratingView); 900 } 901 902 { 903 BDateFormat dateFormat; 904 BString createTimestampPresentation; 905 906 dateFormat.Format(createTimestampPresentation, 907 rating.CreateTimestamp().Date(), B_MEDIUM_DATE_FORMAT); 908 909 BString ratingContextDescription( 910 B_TRANSLATE("%hd.timestamp% (version %hd.version%)")); 911 ratingContextDescription.ReplaceAll("%hd.timestamp%", 912 createTimestampPresentation); 913 ratingContextDescription.ReplaceAll("%hd.version%", 914 rating.PackageVersion()); 915 916 BStringView* ratingContextView = new BStringView("rating-context", 917 ratingContextDescription); 918 BFont versionFont(be_plain_font); 919 ratingContextView->SetFont(&versionFont); 920 ratingGroup->AddView(ratingContextView); 921 } 922 923 ratingGroup->AddItem(BSpaceLayoutItem::CreateGlue()); 924 925 if (rating.Comment() > 0) { 926 TextView* textView = new TextView("rating-text"); 927 ParagraphStyle paragraphStyle(textView->ParagraphStyle()); 928 paragraphStyle.SetJustify(true); 929 textView->SetParagraphStyle(paragraphStyle); 930 textView->SetText(rating.Comment()); 931 verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f)); 932 verticalGroup->AddView(textView); 933 verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f)); 934 } 935 936 verticalGroup->SetInsets(B_USE_DEFAULT_SPACING); 937 938 SetFlags(Flags() | B_WILL_DRAW); 939 } 940 941 void AllAttached() 942 { 943 for (int32 index = 0; index < CountChildren(); ++index) 944 ChildAt(index)->AdoptParentColors(); 945 } 946 947 void Draw(BRect rect) 948 { 949 rgb_color color = mix_color(ViewColor(), ui_color(B_PANEL_TEXT_COLOR), 64); 950 SetHighColor(color); 951 StrokeLine(Bounds().LeftBottom(), Bounds().RightBottom()); 952 } 953 954 }; 955 956 957 class RatingSummaryView : public BGridView { 958 public: 959 RatingSummaryView() 960 : 961 BGridView("rating summary view", B_USE_HALF_ITEM_SPACING, 0.0f) 962 { 963 float tint = kContentTint - 0.1; 964 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, tint); 965 966 BLayoutBuilder::Grid<> layoutBuilder(this); 967 968 BFont smallFont; 969 GetFont(&smallFont); 970 smallFont.SetSize(std::max(9.0f, floorf(smallFont.Size() * 0.85f))); 971 972 for (int32 i = 0; i < 5; i++) { 973 BString label; 974 label.SetToFormat("%" B_PRId32, 5 - i); 975 fLabelViews[i] = new BStringView("", label); 976 fLabelViews[i]->SetFont(&smallFont); 977 fLabelViews[i]->SetViewUIColor(ViewUIColor(), tint); 978 layoutBuilder.Add(fLabelViews[i], 0, i); 979 980 fDiagramBarViews[i] = new DiagramBarView(); 981 layoutBuilder.Add(fDiagramBarViews[i], 1, i); 982 983 fCountViews[i] = new BStringView("", ""); 984 fCountViews[i]->SetFont(&smallFont); 985 fCountViews[i]->SetViewUIColor(ViewUIColor(), tint); 986 fCountViews[i]->SetAlignment(B_ALIGN_RIGHT); 987 layoutBuilder.Add(fCountViews[i], 2, i); 988 } 989 990 layoutBuilder.SetInsets(5); 991 } 992 993 void SetToSummary(const RatingSummary& summary) { 994 for (int32 i = 0; i < 5; i++) { 995 int32 count = summary.ratingCountByStar[4 - i]; 996 997 BString label; 998 label.SetToFormat("%" B_PRId32, count); 999 fCountViews[i]->SetText(label); 1000 1001 if (summary.ratingCount > 0) { 1002 fDiagramBarViews[i]->SetValue( 1003 (float)count / summary.ratingCount); 1004 } else 1005 fDiagramBarViews[i]->SetValue(0.0f); 1006 } 1007 } 1008 1009 void Clear() { 1010 for (int32 i = 0; i < 5; i++) { 1011 fCountViews[i]->SetText(""); 1012 fDiagramBarViews[i]->SetValue(0.0f); 1013 } 1014 } 1015 1016 private: 1017 BStringView* fLabelViews[5]; 1018 DiagramBarView* fDiagramBarViews[5]; 1019 BStringView* fCountViews[5]; 1020 }; 1021 1022 1023 class UserRatingsView : public BGroupView { 1024 public: 1025 UserRatingsView() 1026 : 1027 BGroupView("package ratings view", B_HORIZONTAL) 1028 { 1029 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1030 1031 fRatingSummaryView = new RatingSummaryView(); 1032 1033 ScrollableGroupView* ratingsContainerView = new ScrollableGroupView(); 1034 ratingsContainerView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR, 1035 kContentTint); 1036 fRatingContainerLayout = ratingsContainerView->GroupLayout(); 1037 1038 BScrollView* scrollView = new RatingsScrollView( 1039 "ratings scroll view", ratingsContainerView); 1040 scrollView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1041 B_SIZE_UNLIMITED)); 1042 1043 BLayoutBuilder::Group<>(this) 1044 .AddGroup(B_VERTICAL) 1045 .Add(fRatingSummaryView, 0.0f) 1046 .AddGlue() 1047 .SetInsets(0.0f, B_USE_DEFAULT_SPACING, 0.0f, 0.0f) 1048 .End() 1049 .AddStrut(64.0) 1050 .Add(scrollView, 1.0f) 1051 .SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f) 1052 ; 1053 1054 _InitPreferredLanguages(); 1055 } 1056 1057 virtual ~UserRatingsView() 1058 { 1059 Clear(); 1060 } 1061 1062 void SetPackage(const PackageInfo& package) 1063 { 1064 ClearRatings(); 1065 1066 // TODO: Re-use rating summary already used for TitleView... 1067 fRatingSummaryView->SetToSummary(package.CalculateRatingSummary()); 1068 1069 const UserRatingList& userRatings = package.UserRatings(); 1070 1071 int count = userRatings.CountItems(); 1072 if (count == 0) { 1073 BStringView* noRatingsView = new BStringView("no ratings", 1074 B_TRANSLATE("No user ratings available.")); 1075 noRatingsView->SetViewUIColor(ViewUIColor(), kContentTint); 1076 noRatingsView->SetAlignment(B_ALIGN_CENTER); 1077 noRatingsView->SetHighColor(disable_color(ui_color(B_PANEL_TEXT_COLOR), 1078 ViewColor())); 1079 noRatingsView->SetExplicitMaxSize( 1080 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)); 1081 fRatingContainerLayout->AddView(0, noRatingsView); 1082 return; 1083 } 1084 1085 // TODO: Sort by age or usefullness rating 1086 for (int i = count - 1; i >= 0; i--) { 1087 const UserRating& rating = userRatings.ItemAtFast(i); 1088 // was previously filtering comments just for the current 1089 // user's language, but as there are not so many comments at 1090 // the moment, just show all of them for now. 1091 RatingItemView* view = new RatingItemView(rating); 1092 fRatingContainerLayout->AddView(0, view); 1093 } 1094 } 1095 1096 void Clear() 1097 { 1098 fRatingSummaryView->Clear(); 1099 ClearRatings(); 1100 } 1101 1102 void ClearRatings() 1103 { 1104 for (int32 i = fRatingContainerLayout->CountItems() - 1; 1105 BLayoutItem* item = fRatingContainerLayout->ItemAt(i); i--) { 1106 BView* view = dynamic_cast<RatingItemView*>(item->View()); 1107 if (view == NULL) 1108 view = dynamic_cast<BStringView*>(item->View()); 1109 if (view != NULL) { 1110 view->RemoveSelf(); 1111 delete view; 1112 } 1113 } 1114 } 1115 1116 private: 1117 void _InitPreferredLanguages() 1118 { 1119 fPreferredLanguages.Clear(); 1120 1121 BLocaleRoster* localeRoster = BLocaleRoster::Default(); 1122 if (localeRoster == NULL) 1123 return; 1124 1125 BMessage preferredLanguages; 1126 if (localeRoster->GetPreferredLanguages(&preferredLanguages) != B_OK) 1127 return; 1128 1129 BString language; 1130 int32 index = 0; 1131 while (preferredLanguages.FindString("language", index++, 1132 &language) == B_OK) { 1133 BString languageCode; 1134 language.CopyInto(languageCode, 0, 2); 1135 fPreferredLanguages.Add(languageCode); 1136 } 1137 } 1138 1139 private: 1140 BGroupLayout* fRatingContainerLayout; 1141 RatingSummaryView* fRatingSummaryView; 1142 StringList fPreferredLanguages; 1143 }; 1144 1145 1146 // #pragma mark - ContentsView 1147 1148 1149 class ContentsView : public BGroupView { 1150 public: 1151 ContentsView() 1152 : 1153 BGroupView("package contents view", B_HORIZONTAL) 1154 { 1155 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1156 1157 fPackageContents = new PackageContentsView("contents_list"); 1158 AddChild(fPackageContents); 1159 1160 } 1161 1162 virtual ~ContentsView() 1163 { 1164 } 1165 1166 virtual void Draw(BRect updateRect) 1167 { 1168 } 1169 1170 void SetPackage(const PackageInfoRef& package) 1171 { 1172 fPackageContents->SetPackage(package); 1173 } 1174 1175 void Clear() 1176 { 1177 fPackageContents->Clear(); 1178 } 1179 1180 private: 1181 PackageContentsView* fPackageContents; 1182 }; 1183 1184 1185 // #pragma mark - ChangelogView 1186 1187 1188 class ChangelogView : public BGroupView { 1189 public: 1190 ChangelogView() 1191 : 1192 BGroupView("package changelog view", B_HORIZONTAL) 1193 { 1194 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1195 1196 fTextView = new MarkupTextView("changelog view"); 1197 fTextView->SetLowUIColor(ViewUIColor()); 1198 fTextView->SetInsets(be_plain_font->Size()); 1199 1200 BScrollView* scrollView = new CustomScrollView( 1201 "changelog scroll view", fTextView); 1202 1203 BLayoutBuilder::Group<>(this) 1204 .Add(BSpaceLayoutItem::CreateHorizontalStrut(32.0f)) 1205 .Add(scrollView, 1.0f) 1206 .SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f) 1207 ; 1208 } 1209 1210 virtual ~ChangelogView() 1211 { 1212 } 1213 1214 virtual void Draw(BRect updateRect) 1215 { 1216 } 1217 1218 void SetPackage(const PackageInfo& package) 1219 { 1220 const BString& changelog = package.Changelog(); 1221 if (changelog.Length() > 0) 1222 fTextView->SetText(changelog); 1223 else 1224 fTextView->SetDisabledText(B_TRANSLATE("No changelog available.")); 1225 } 1226 1227 void Clear() 1228 { 1229 fTextView->SetText(""); 1230 } 1231 1232 private: 1233 MarkupTextView* fTextView; 1234 }; 1235 1236 1237 // #pragma mark - PagesView 1238 1239 1240 class PagesView : public BTabView { 1241 public: 1242 PagesView() 1243 : 1244 BTabView("pages view", B_WIDTH_FROM_WIDEST), 1245 fLayout(new BCardLayout()) 1246 { 1247 SetBorder(B_NO_BORDER); 1248 1249 fAboutView = new AboutView(); 1250 fUserRatingsView = new UserRatingsView(); 1251 fChangelogView = new ChangelogView(); 1252 fContentsView = new ContentsView(); 1253 1254 AddTab(fAboutView); 1255 AddTab(fUserRatingsView); 1256 AddTab(fChangelogView); 1257 AddTab(fContentsView); 1258 1259 TabAt(0)->SetLabel(B_TRANSLATE("About")); 1260 TabAt(1)->SetLabel(B_TRANSLATE("Ratings")); 1261 TabAt(2)->SetLabel(B_TRANSLATE("Changelog")); 1262 TabAt(3)->SetLabel(B_TRANSLATE("Contents")); 1263 1264 Select(0); 1265 } 1266 1267 virtual ~PagesView() 1268 { 1269 Clear(); 1270 } 1271 1272 void SetPackage(const PackageInfoRef& package, bool switchToDefaultTab) 1273 { 1274 if (switchToDefaultTab) 1275 Select(0); 1276 fAboutView->SetPackage(*package.Get()); 1277 fUserRatingsView->SetPackage(*package.Get()); 1278 fChangelogView->SetPackage(*package.Get()); 1279 fContentsView->SetPackage(package); 1280 } 1281 1282 void Clear() 1283 { 1284 fAboutView->Clear(); 1285 fUserRatingsView->Clear(); 1286 fChangelogView->Clear(); 1287 fContentsView->Clear(); 1288 } 1289 1290 private: 1291 BCardLayout* fLayout; 1292 1293 AboutView* fAboutView; 1294 UserRatingsView* fUserRatingsView; 1295 ChangelogView* fChangelogView; 1296 ContentsView* fContentsView; 1297 }; 1298 1299 1300 // #pragma mark - PackageInfoView 1301 1302 1303 PackageInfoView::PackageInfoView(BLocker* modelLock, 1304 PackageActionHandler* handler) 1305 : 1306 BView("package info view", 0), 1307 fModelLock(modelLock), 1308 fPackageListener(new(std::nothrow) OnePackageMessagePackageListener(this)) 1309 { 1310 fCardLayout = new BCardLayout(); 1311 SetLayout(fCardLayout); 1312 1313 BGroupView* noPackageCard = new BGroupView("no package card", B_VERTICAL); 1314 AddChild(noPackageCard); 1315 1316 BStringView* noPackageView = new BStringView("no package view", 1317 B_TRANSLATE("Click a package to view information")); 1318 noPackageView->SetHighColor(kLightBlack); 1319 noPackageView->SetExplicitAlignment(BAlignment( 1320 B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER)); 1321 1322 BLayoutBuilder::Group<>(noPackageCard) 1323 .Add(noPackageView) 1324 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)) 1325 ; 1326 1327 BGroupView* packageCard = new BGroupView("package card", B_VERTICAL); 1328 AddChild(packageCard); 1329 1330 fCardLayout->SetVisibleItem((int32)0); 1331 1332 fTitleView = new TitleView(); 1333 fPackageActionView = new PackageActionView(handler); 1334 fPackageActionView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1335 B_SIZE_UNSET)); 1336 fPagesView = new PagesView(); 1337 1338 BLayoutBuilder::Group<>(packageCard) 1339 .AddGroup(B_HORIZONTAL, 0.0f) 1340 .Add(fTitleView, 6.0f) 1341 .Add(fPackageActionView, 1.0f) 1342 .SetInsets( 1343 B_USE_DEFAULT_SPACING, 0.0f, 1344 B_USE_DEFAULT_SPACING, 0.0f) 1345 .End() 1346 .Add(fPagesView) 1347 ; 1348 1349 Clear(); 1350 } 1351 1352 1353 PackageInfoView::~PackageInfoView() 1354 { 1355 fPackageListener->SetPackage(PackageInfoRef(NULL)); 1356 delete fPackageListener; 1357 } 1358 1359 1360 void 1361 PackageInfoView::AttachedToWindow() 1362 { 1363 } 1364 1365 1366 void 1367 PackageInfoView::MessageReceived(BMessage* message) 1368 { 1369 switch (message->what) { 1370 case MSG_UPDATE_PACKAGE: 1371 { 1372 if (fPackageListener->Package().Get() == NULL) 1373 break; 1374 1375 BString name; 1376 uint32 changes; 1377 if (message->FindString("name", &name) != B_OK 1378 || message->FindUInt32("changes", &changes) != B_OK) { 1379 break; 1380 } 1381 1382 const PackageInfoRef& package = fPackageListener->Package(); 1383 if (package->Name() != name) 1384 break; 1385 1386 BAutolock _(fModelLock); 1387 1388 if ((changes & PKG_CHANGED_SUMMARY) != 0 1389 || (changes & PKG_CHANGED_DESCRIPTION) != 0 1390 || (changes & PKG_CHANGED_SCREENSHOTS) != 0 1391 || (changes & PKG_CHANGED_TITLE) != 0 1392 || (changes & PKG_CHANGED_RATINGS) != 0 1393 || (changes & PKG_CHANGED_STATE) != 0 1394 || (changes & PKG_CHANGED_CHANGELOG) != 0) { 1395 fPagesView->SetPackage(package, false); 1396 } 1397 1398 if ((changes & PKG_CHANGED_TITLE) != 0 1399 || (changes & PKG_CHANGED_RATINGS) != 0) { 1400 fTitleView->SetPackage(*package.Get()); 1401 } 1402 1403 if ((changes & PKG_CHANGED_STATE) != 0) { 1404 fPackageActionView->SetPackage(*package.Get()); 1405 } 1406 1407 break; 1408 } 1409 default: 1410 BView::MessageReceived(message); 1411 break; 1412 } 1413 } 1414 1415 1416 void 1417 PackageInfoView::SetPackage(const PackageInfoRef& packageRef) 1418 { 1419 BAutolock _(fModelLock); 1420 1421 if (packageRef.Get() == NULL) { 1422 Clear(); 1423 return; 1424 } 1425 1426 bool switchToDefaultTab = true; 1427 if (fPackage == packageRef) { 1428 // When asked to display the already showing package ref, 1429 // don't switch to the default tab. 1430 switchToDefaultTab = false; 1431 } else if (fPackage.Get() != NULL && packageRef.Get() != NULL 1432 && fPackage->Name() == packageRef->Name()) { 1433 // When asked to display a different PackageInfo instance, 1434 // but it has the same package title as the already showing 1435 // instance, this probably means there was a repository 1436 // refresh and we are in fact still requested to show the 1437 // same package as before the refresh. 1438 switchToDefaultTab = false; 1439 } 1440 1441 const PackageInfo& package = *packageRef.Get(); 1442 1443 fTitleView->SetPackage(package); 1444 fPackageActionView->SetPackage(package); 1445 fPagesView->SetPackage(packageRef, switchToDefaultTab); 1446 1447 fCardLayout->SetVisibleItem(1); 1448 1449 fPackageListener->SetPackage(packageRef); 1450 1451 // Set the fPackage reference last, so we keep a reference to the 1452 // previous package before switching all the views to the new package. 1453 // Otherwise the PackageInfo instance may go away because we had the 1454 // last reference. And some of the views, the PackageActionView for 1455 // example, keeps references to stuff from the previous package and 1456 // access it while switching to the new package. 1457 fPackage = packageRef; 1458 } 1459 1460 1461 void 1462 PackageInfoView::Clear() 1463 { 1464 BAutolock _(fModelLock); 1465 1466 fTitleView->Clear(); 1467 fPackageActionView->Clear(); 1468 fPagesView->Clear(); 1469 1470 fCardLayout->SetVisibleItem((int32)0); 1471 1472 fPackageListener->SetPackage(PackageInfoRef(NULL)); 1473 1474 fPackage.Unset(); 1475 }