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