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