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