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