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 "BitmapView.h" 40 #include "LinkView.h" 41 #include "LinkedBitmapView.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 BString version = B_TRANSLATE("%Version%"); 405 version.ReplaceAll("%Version%", package.Version().ToString()); 406 fVersionInfo->SetText(version); 407 408 RatingSummary ratingSummary = package.CalculateRatingSummary(); 409 410 fRatingView->SetRating(ratingSummary.averageRating); 411 412 if (ratingSummary.ratingCount > 0) { 413 BString avgRating; 414 avgRating.SetToFormat("%.1f", ratingSummary.averageRating); 415 fAvgRating->SetText(avgRating); 416 417 BString votes; 418 votes.SetToFormat("%d", ratingSummary.ratingCount); 419 420 BString voteInfo(B_TRANSLATE("(%Votes%)")); 421 voteInfo.ReplaceAll("%Votes%", votes); 422 423 fVoteInfo->SetText(voteInfo); 424 } else { 425 fAvgRating->SetText(""); 426 fVoteInfo->SetText(B_TRANSLATE("n/a")); 427 } 428 429 InvalidateLayout(); 430 Invalidate(); 431 } 432 433 void Clear() 434 { 435 fIconView->UnsetBitmap(); 436 fTitleView->SetText(""); 437 fPublisherView->SetText(""); 438 fVersionInfo->SetText(""); 439 fRatingView->SetRating(-1.0f); 440 fAvgRating->SetText(""); 441 fVoteInfo->SetText(""); 442 } 443 444 private: 445 BitmapView* fIconView; 446 447 BStringView* fTitleView; 448 BStringView* fPublisherView; 449 450 BStringView* fVersionInfo; 451 452 BCardLayout* fRatingLayout; 453 454 TransitReportingRatingView* fRatingView; 455 BStringView* fAvgRating; 456 BStringView* fVoteInfo; 457 458 TransitReportingButton* fRateButton; 459 }; 460 461 462 // #pragma mark - PackageActionView 463 464 465 class PackageActionView : public BView { 466 public: 467 PackageActionView(PackageActionHandler* handler) 468 : 469 BView("about view", B_WILL_DRAW), 470 fLayout(new BGroupLayout(B_HORIZONTAL)), 471 fPackageActionHandler(handler), 472 fStatusLabel(NULL), 473 fStatusBar(NULL) 474 { 475 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 476 477 SetLayout(fLayout); 478 fLayout->AddItem(BSpaceLayoutItem::CreateGlue()); 479 } 480 481 virtual ~PackageActionView() 482 { 483 Clear(); 484 } 485 486 virtual void MessageReceived(BMessage* message) 487 { 488 switch (message->what) { 489 case MSG_PACKAGE_ACTION: 490 _RunPackageAction(message); 491 break; 492 493 default: 494 BView::MessageReceived(message); 495 break; 496 } 497 } 498 499 void SetPackage(const PackageInfo& package) 500 { 501 if (package.State() == DOWNLOADING) { 502 AdoptDownloadProgress(package); 503 } else { 504 AdoptActions(package); 505 } 506 507 } 508 509 void AdoptActions(const PackageInfo& package) 510 { 511 PackageManager manager( 512 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME); 513 514 // TODO: if the given package is either a system package 515 // or a system dependency, show a message indicating that status 516 // so the user knows why no actions are presented 517 PackageActionList actions = manager.GetPackageActions( 518 const_cast<PackageInfo*>(&package), 519 fPackageActionHandler->GetModel()); 520 521 bool clearNeeded = fStatusBar != NULL; 522 if (!clearNeeded) { 523 if (actions.CountItems() != fPackageActions.CountItems()) 524 clearNeeded = true; 525 else { 526 for (int32 i = 0; i < actions.CountItems(); i++) { 527 if (actions.ItemAtFast(i)->Type() 528 != fPackageActions.ItemAtFast(i)->Type()) { 529 clearNeeded = true; 530 break; 531 } 532 } 533 } 534 } 535 536 fPackageActions = actions; 537 if (!clearNeeded && fButtons.CountItems() == actions.CountItems()) { 538 int32 index = 0; 539 for (int32 i = fPackageActions.CountItems() - 1; i >= 0; i--) { 540 const PackageActionRef& action = fPackageActions.ItemAtFast(i); 541 BButton* button = (BButton*)fButtons.ItemAtFast(index++); 542 button->SetLabel(action->Label()); 543 } 544 return; 545 } 546 547 Clear(); 548 549 // Add Buttons in reverse action order 550 for (int32 i = fPackageActions.CountItems() - 1; i >= 0; i--) { 551 const PackageActionRef& action = fPackageActions.ItemAtFast(i); 552 553 BMessage* message = new BMessage(MSG_PACKAGE_ACTION); 554 message->AddInt32("index", i); 555 556 BButton* button = new BButton(action->Label(), message); 557 fLayout->AddView(button); 558 button->SetTarget(this); 559 560 fButtons.AddItem(button); 561 } 562 } 563 564 void AdoptDownloadProgress(const PackageInfo& package) 565 { 566 if (fButtons.CountItems() > 0) 567 Clear(); 568 569 if (fStatusBar == NULL) { 570 fStatusLabel = new BStringView("progress label", 571 B_TRANSLATE("Downloading:")); 572 fLayout->AddView(fStatusLabel); 573 574 fStatusBar = new BStatusBar("progress"); 575 fStatusBar->SetMaxValue(100.0); 576 fStatusBar->SetExplicitMinSize( 577 BSize(StringWidth("XXX") * 5, B_SIZE_UNSET)); 578 579 fLayout->AddView(fStatusBar); 580 } 581 582 fStatusBar->SetTo(package.DownloadProgress() * 100.0); 583 } 584 585 void Clear() 586 { 587 for (int32 i = fButtons.CountItems() - 1; i >= 0; i--) { 588 BButton* button = (BButton*)fButtons.ItemAtFast(i); 589 button->RemoveSelf(); 590 delete button; 591 } 592 fButtons.MakeEmpty(); 593 594 if (fStatusBar != NULL) { 595 fStatusBar->RemoveSelf(); 596 delete fStatusBar; 597 fStatusBar = NULL; 598 } 599 if (fStatusLabel != NULL) { 600 fStatusLabel->RemoveSelf(); 601 delete fStatusLabel; 602 fStatusLabel = NULL; 603 } 604 } 605 606 private: 607 void _RunPackageAction(BMessage* message) 608 { 609 int32 index; 610 if (message->FindInt32("index", &index) != B_OK) 611 return; 612 613 const PackageActionRef& action = fPackageActions.ItemAt(index); 614 if (action.Get() == NULL) 615 return; 616 617 PackageActionList actions; 618 actions.Add(action); 619 status_t result 620 = fPackageActionHandler->SchedulePackageActions(actions); 621 622 if (result != B_OK) { 623 fprintf(stderr, "Failed to schedule action: " 624 "%s '%s': %s\n", action->Label(), 625 action->Package()->Name().String(), 626 strerror(result)); 627 BString message(B_TRANSLATE("The package action " 628 "could not be scheduled: %Error%")); 629 message.ReplaceAll("%Error%", strerror(result)); 630 BAlert* alert = new(std::nothrow) BAlert( 631 B_TRANSLATE("Package action failed"), 632 message, B_TRANSLATE("OK"), NULL, NULL, 633 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 634 if (alert != NULL) 635 alert->Go(); 636 } else { 637 // Find the button for this action and disable it. 638 // Actually search the matching button instead of just using 639 // fButtons.ItemAt((fButtons.CountItems() - 1) - index) to 640 // make this robust against for example changing the order of 641 // buttons from right -> left to left -> right... 642 for (int32 i = 0; i < fButtons.CountItems(); i++) { 643 BButton* button = (BButton*)fButtons.ItemAt(index); 644 if (button == NULL) 645 continue; 646 BMessage* buttonMessage = button->Message(); 647 if (buttonMessage == NULL) 648 continue; 649 int32 buttonIndex; 650 if (buttonMessage->FindInt32("index", &buttonIndex) != B_OK) 651 continue; 652 if (buttonIndex == index) { 653 button->SetEnabled(false); 654 break; 655 } 656 } 657 } 658 } 659 660 private: 661 BGroupLayout* fLayout; 662 PackageActionList fPackageActions; 663 PackageActionHandler* fPackageActionHandler; 664 BList fButtons; 665 666 BStringView* fStatusLabel; 667 BStatusBar* fStatusBar; 668 }; 669 670 671 // #pragma mark - AboutView 672 673 674 enum { 675 MSG_EMAIL_PUBLISHER = 'emlp', 676 MSG_VISIT_PUBLISHER_WEBSITE = 'vpws', 677 }; 678 679 680 class AboutView : public BView { 681 public: 682 AboutView() 683 : 684 BView("about view", 0), 685 fEmailIcon("text/x-email"), 686 fWebsiteIcon("text/html") 687 { 688 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 689 690 fDescriptionView = new MarkupTextView("description view"); 691 fDescriptionView->SetViewUIColor(ViewUIColor(), kContentTint); 692 fDescriptionView->SetInsets(be_plain_font->Size()); 693 694 BScrollView* scrollView = new CustomScrollView( 695 "description scroll view", fDescriptionView); 696 697 BFont smallFont; 698 GetFont(&smallFont); 699 smallFont.SetSize(std::max(9.0f, ceilf(smallFont.Size() * 0.85f))); 700 701 // TODO: Clicking the screen shot view should open ShowImage with the 702 // the screen shot. This could be done by writing the screen shot to 703 // a temporary folder, launching ShowImage to display it, and writing 704 // all other screenshots associated with the package to the same folder 705 // so the user can use the ShowImage navigation to view the other 706 // screenshots. 707 fScreenshotView = new LinkedBitmapView("screenshot view", 708 new BMessage(MSG_SHOW_SCREENSHOT)); 709 fScreenshotView->SetExplicitMinSize(BSize(64.0f, 64.0f)); 710 fScreenshotView->SetExplicitMaxSize( 711 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)); 712 fScreenshotView->SetExplicitAlignment( 713 BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP)); 714 715 fEmailIconView = new BitmapView("email icon view"); 716 fEmailLinkView = new LinkView("email link view", "", 717 new BMessage(MSG_EMAIL_PUBLISHER)); 718 fEmailLinkView->SetFont(&smallFont); 719 720 fWebsiteIconView = new BitmapView("website icon view"); 721 fWebsiteLinkView = new LinkView("website link view", "", 722 new BMessage(MSG_VISIT_PUBLISHER_WEBSITE)); 723 fWebsiteLinkView->SetFont(&smallFont); 724 725 BGroupView* leftGroup = new BGroupView(B_VERTICAL, 726 B_USE_DEFAULT_SPACING); 727 728 fScreenshotView->SetViewUIColor(ViewUIColor(), kContentTint); 729 fEmailLinkView->SetViewUIColor(ViewUIColor(), kContentTint); 730 fWebsiteLinkView->SetViewUIColor(ViewUIColor(), kContentTint); 731 732 BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0.0f) 733 .AddGroup(leftGroup, 1.0f) 734 .Add(fScreenshotView) 735 .AddGroup(B_HORIZONTAL) 736 .AddGrid(B_USE_HALF_ITEM_SPACING, B_USE_HALF_ITEM_SPACING) 737 .Add(fEmailIconView, 0, 0) 738 .Add(fEmailLinkView, 1, 0) 739 .Add(fWebsiteIconView, 0, 1) 740 .Add(fWebsiteLinkView, 1, 1) 741 .End() 742 .End() 743 .SetInsets(B_USE_DEFAULT_SPACING) 744 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)) 745 .End() 746 .Add(scrollView, 2.0f) 747 748 .SetExplicitMaxSize(BSize(B_SIZE_UNSET, B_SIZE_UNLIMITED)) 749 .SetInsets(0.0f, -1.0f, -1.0f, -1.0f) 750 ; 751 } 752 753 virtual ~AboutView() 754 { 755 Clear(); 756 } 757 758 virtual void AttachedToWindow() 759 { 760 fScreenshotView->SetTarget(this); 761 fEmailLinkView->SetTarget(this); 762 fWebsiteLinkView->SetTarget(this); 763 } 764 765 virtual void AllAttached() 766 { 767 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 768 769 for (int32 index = 0; index < CountChildren(); ++index) 770 ChildAt(index)->AdoptParentColors(); 771 } 772 773 virtual void MessageReceived(BMessage* message) 774 { 775 switch (message->what) { 776 case MSG_SHOW_SCREENSHOT: 777 { 778 // Forward to window for now 779 Window()->PostMessage(message, Window()); 780 break; 781 } 782 783 case MSG_EMAIL_PUBLISHER: 784 { 785 // TODO: Implement. If memory serves, there is a 786 // standard command line interface which mail apps should 787 // support, i.e. to open a compose window with the TO: field 788 // already set. 789 break; 790 } 791 792 case MSG_VISIT_PUBLISHER_WEBSITE: 793 { 794 BUrl url(fWebsiteLinkView->Text()); 795 url.OpenWithPreferredApplication(); 796 break; 797 } 798 799 default: 800 BView::MessageReceived(message); 801 break; 802 } 803 } 804 805 void SetPackage(const PackageInfo& package) 806 { 807 fDescriptionView->SetText(package.ShortDescription(), 808 package.FullDescription()); 809 810 fEmailIconView->SetBitmap(&fEmailIcon, SharedBitmap::SIZE_16); 811 _SetContactInfo(fEmailLinkView, package.Publisher().Email()); 812 fWebsiteIconView->SetBitmap(&fWebsiteIcon, SharedBitmap::SIZE_16); 813 _SetContactInfo(fWebsiteLinkView, package.Publisher().Website()); 814 815 bool hasScreenshot = false; 816 const BitmapList& screenShots = package.Screenshots(); 817 if (screenShots.CountItems() > 0) { 818 const BitmapRef& bitmapRef = screenShots.ItemAtFast(0); 819 if (bitmapRef.Get() != NULL) { 820 hasScreenshot = true; 821 fScreenshotView->SetBitmap(bitmapRef); 822 } 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 UserRating& 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 BDateFormat dateFormat; 903 BString createTimestampPresentation; 904 905 dateFormat.Format(createTimestampPresentation, 906 rating.CreateTimestamp().Date(), B_MEDIUM_DATE_FORMAT); 907 908 BString ratingContextDescription( 909 B_TRANSLATE("%hd.timestamp% (version %hd.version%)")); 910 ratingContextDescription.ReplaceAll("%hd.timestamp%", 911 createTimestampPresentation); 912 ratingContextDescription.ReplaceAll("%hd.version%", 913 rating.PackageVersion()); 914 915 BStringView* ratingContextView = new BStringView("rating-context", 916 ratingContextDescription); 917 BFont versionFont(be_plain_font); 918 ratingContextView->SetFont(&versionFont); 919 ratingGroup->AddView(ratingContextView); 920 } 921 922 ratingGroup->AddItem(BSpaceLayoutItem::CreateGlue()); 923 924 if (rating.Comment() > 0) { 925 TextView* textView = new TextView("rating-text"); 926 ParagraphStyle paragraphStyle(textView->ParagraphStyle()); 927 paragraphStyle.SetJustify(true); 928 textView->SetParagraphStyle(paragraphStyle); 929 textView->SetText(rating.Comment()); 930 verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f)); 931 verticalGroup->AddView(textView); 932 verticalGroup->AddItem(BSpaceLayoutItem::CreateVerticalStrut(8.0f)); 933 } 934 935 verticalGroup->SetInsets(B_USE_DEFAULT_SPACING); 936 937 SetFlags(Flags() | B_WILL_DRAW); 938 } 939 940 void AllAttached() 941 { 942 for (int32 index = 0; index < CountChildren(); ++index) 943 ChildAt(index)->AdoptParentColors(); 944 } 945 946 void Draw(BRect rect) 947 { 948 rgb_color color = mix_color(ViewColor(), ui_color(B_PANEL_TEXT_COLOR), 64); 949 SetHighColor(color); 950 StrokeLine(Bounds().LeftBottom(), Bounds().RightBottom()); 951 } 952 953 }; 954 955 956 class RatingSummaryView : public BGridView { 957 public: 958 RatingSummaryView() 959 : 960 BGridView("rating summary view", B_USE_HALF_ITEM_SPACING, 0.0f) 961 { 962 float tint = kContentTint - 0.1; 963 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, tint); 964 965 BLayoutBuilder::Grid<> layoutBuilder(this); 966 967 BFont smallFont; 968 GetFont(&smallFont); 969 smallFont.SetSize(std::max(9.0f, floorf(smallFont.Size() * 0.85f))); 970 971 for (int32 i = 0; i < 5; i++) { 972 BString label; 973 label.SetToFormat("%" B_PRId32, 5 - i); 974 fLabelViews[i] = new BStringView("", label); 975 fLabelViews[i]->SetFont(&smallFont); 976 fLabelViews[i]->SetViewUIColor(ViewUIColor(), tint); 977 layoutBuilder.Add(fLabelViews[i], 0, i); 978 979 fDiagramBarViews[i] = new DiagramBarView(); 980 layoutBuilder.Add(fDiagramBarViews[i], 1, i); 981 982 fCountViews[i] = new BStringView("", ""); 983 fCountViews[i]->SetFont(&smallFont); 984 fCountViews[i]->SetViewUIColor(ViewUIColor(), tint); 985 fCountViews[i]->SetAlignment(B_ALIGN_RIGHT); 986 layoutBuilder.Add(fCountViews[i], 2, i); 987 } 988 989 layoutBuilder.SetInsets(5); 990 } 991 992 void SetToSummary(const RatingSummary& summary) { 993 for (int32 i = 0; i < 5; i++) { 994 int32 count = summary.ratingCountByStar[4 - i]; 995 996 BString label; 997 label.SetToFormat("%" B_PRId32, count); 998 fCountViews[i]->SetText(label); 999 1000 if (summary.ratingCount > 0) { 1001 fDiagramBarViews[i]->SetValue( 1002 (float)count / summary.ratingCount); 1003 } else 1004 fDiagramBarViews[i]->SetValue(0.0f); 1005 } 1006 } 1007 1008 void Clear() { 1009 for (int32 i = 0; i < 5; i++) { 1010 fCountViews[i]->SetText(""); 1011 fDiagramBarViews[i]->SetValue(0.0f); 1012 } 1013 } 1014 1015 private: 1016 BStringView* fLabelViews[5]; 1017 DiagramBarView* fDiagramBarViews[5]; 1018 BStringView* fCountViews[5]; 1019 }; 1020 1021 1022 class UserRatingsView : public BGroupView { 1023 public: 1024 UserRatingsView() 1025 : 1026 BGroupView("package ratings view", B_HORIZONTAL) 1027 { 1028 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1029 1030 fRatingSummaryView = new RatingSummaryView(); 1031 1032 ScrollableGroupView* ratingsContainerView = new ScrollableGroupView(); 1033 ratingsContainerView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR, 1034 kContentTint); 1035 fRatingContainerLayout = ratingsContainerView->GroupLayout(); 1036 1037 BScrollView* scrollView = new RatingsScrollView( 1038 "ratings scroll view", ratingsContainerView); 1039 scrollView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1040 B_SIZE_UNLIMITED)); 1041 1042 BLayoutBuilder::Group<>(this) 1043 .AddGroup(B_VERTICAL) 1044 .Add(fRatingSummaryView, 0.0f) 1045 .AddGlue() 1046 .SetInsets(0.0f, B_USE_DEFAULT_SPACING, 0.0f, 0.0f) 1047 .End() 1048 .AddStrut(64.0) 1049 .Add(scrollView, 1.0f) 1050 .SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f) 1051 ; 1052 } 1053 1054 virtual ~UserRatingsView() 1055 { 1056 Clear(); 1057 } 1058 1059 void SetPackage(const PackageInfo& package) 1060 { 1061 ClearRatings(); 1062 1063 // TODO: Re-use rating summary already used for TitleView... 1064 fRatingSummaryView->SetToSummary(package.CalculateRatingSummary()); 1065 1066 const UserRatingList& userRatings = package.UserRatings(); 1067 1068 int count = userRatings.CountItems(); 1069 if (count == 0) { 1070 BStringView* noRatingsView = new BStringView("no ratings", 1071 B_TRANSLATE("No user ratings available.")); 1072 noRatingsView->SetViewUIColor(ViewUIColor(), kContentTint); 1073 noRatingsView->SetAlignment(B_ALIGN_CENTER); 1074 noRatingsView->SetHighColor(disable_color(ui_color(B_PANEL_TEXT_COLOR), 1075 ViewColor())); 1076 noRatingsView->SetExplicitMaxSize( 1077 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)); 1078 fRatingContainerLayout->AddView(0, noRatingsView); 1079 return; 1080 } 1081 1082 // TODO: Sort by age or usefullness rating 1083 for (int i = count - 1; i >= 0; i--) { 1084 const UserRating& rating = userRatings.ItemAtFast(i); 1085 // was previously filtering comments just for the current 1086 // user's language, but as there are not so many comments at 1087 // the moment, just show all of them for now. 1088 RatingItemView* view = new RatingItemView(rating); 1089 fRatingContainerLayout->AddView(0, view); 1090 } 1091 } 1092 1093 void Clear() 1094 { 1095 fRatingSummaryView->Clear(); 1096 ClearRatings(); 1097 } 1098 1099 void ClearRatings() 1100 { 1101 for (int32 i = fRatingContainerLayout->CountItems() - 1; 1102 BLayoutItem* item = fRatingContainerLayout->ItemAt(i); i--) { 1103 BView* view = dynamic_cast<RatingItemView*>(item->View()); 1104 if (view == NULL) 1105 view = dynamic_cast<BStringView*>(item->View()); 1106 if (view != NULL) { 1107 view->RemoveSelf(); 1108 delete view; 1109 } 1110 } 1111 } 1112 1113 private: 1114 BGroupLayout* fRatingContainerLayout; 1115 RatingSummaryView* fRatingSummaryView; 1116 }; 1117 1118 1119 // #pragma mark - ContentsView 1120 1121 1122 class ContentsView : public BGroupView { 1123 public: 1124 ContentsView() 1125 : 1126 BGroupView("package contents view", B_HORIZONTAL) 1127 { 1128 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1129 1130 fPackageContents = new PackageContentsView("contents_list"); 1131 AddChild(fPackageContents); 1132 1133 } 1134 1135 virtual ~ContentsView() 1136 { 1137 } 1138 1139 virtual void Draw(BRect updateRect) 1140 { 1141 } 1142 1143 void SetPackage(const PackageInfoRef& package) 1144 { 1145 fPackageContents->SetPackage(package); 1146 } 1147 1148 void Clear() 1149 { 1150 fPackageContents->Clear(); 1151 } 1152 1153 private: 1154 PackageContentsView* fPackageContents; 1155 }; 1156 1157 1158 // #pragma mark - ChangelogView 1159 1160 1161 class ChangelogView : public BGroupView { 1162 public: 1163 ChangelogView() 1164 : 1165 BGroupView("package changelog view", B_HORIZONTAL) 1166 { 1167 SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); 1168 1169 fTextView = new MarkupTextView("changelog view"); 1170 fTextView->SetLowUIColor(ViewUIColor()); 1171 fTextView->SetInsets(be_plain_font->Size()); 1172 1173 BScrollView* scrollView = new CustomScrollView( 1174 "changelog scroll view", fTextView); 1175 1176 BLayoutBuilder::Group<>(this) 1177 .Add(BSpaceLayoutItem::CreateHorizontalStrut(32.0f)) 1178 .Add(scrollView, 1.0f) 1179 .SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f) 1180 ; 1181 } 1182 1183 virtual ~ChangelogView() 1184 { 1185 } 1186 1187 virtual void Draw(BRect updateRect) 1188 { 1189 } 1190 1191 void SetPackage(const PackageInfo& package) 1192 { 1193 const BString& changelog = package.Changelog(); 1194 if (changelog.Length() > 0) 1195 fTextView->SetText(changelog); 1196 else 1197 fTextView->SetDisabledText(B_TRANSLATE("No changelog available.")); 1198 } 1199 1200 void Clear() 1201 { 1202 fTextView->SetText(""); 1203 } 1204 1205 private: 1206 MarkupTextView* fTextView; 1207 }; 1208 1209 1210 // #pragma mark - PagesView 1211 1212 1213 class PagesView : public BTabView { 1214 public: 1215 PagesView() 1216 : 1217 BTabView("pages view", B_WIDTH_FROM_WIDEST), 1218 fLayout(new BCardLayout()) 1219 { 1220 SetBorder(B_NO_BORDER); 1221 1222 fAboutView = new AboutView(); 1223 fUserRatingsView = new UserRatingsView(); 1224 fChangelogView = new ChangelogView(); 1225 fContentsView = new ContentsView(); 1226 1227 AddTab(fAboutView); 1228 AddTab(fUserRatingsView); 1229 AddTab(fChangelogView); 1230 AddTab(fContentsView); 1231 1232 TabAt(0)->SetLabel(B_TRANSLATE("About")); 1233 TabAt(1)->SetLabel(B_TRANSLATE("Ratings")); 1234 TabAt(2)->SetLabel(B_TRANSLATE("Changelog")); 1235 TabAt(3)->SetLabel(B_TRANSLATE("Contents")); 1236 1237 Select(0); 1238 } 1239 1240 virtual ~PagesView() 1241 { 1242 Clear(); 1243 } 1244 1245 void SetPackage(const PackageInfoRef& package, bool switchToDefaultTab) 1246 { 1247 if (switchToDefaultTab) 1248 Select(0); 1249 fAboutView->SetPackage(*package.Get()); 1250 fUserRatingsView->SetPackage(*package.Get()); 1251 fChangelogView->SetPackage(*package.Get()); 1252 fContentsView->SetPackage(package); 1253 } 1254 1255 void Clear() 1256 { 1257 fAboutView->Clear(); 1258 fUserRatingsView->Clear(); 1259 fChangelogView->Clear(); 1260 fContentsView->Clear(); 1261 } 1262 1263 private: 1264 BCardLayout* fLayout; 1265 1266 AboutView* fAboutView; 1267 UserRatingsView* fUserRatingsView; 1268 ChangelogView* fChangelogView; 1269 ContentsView* fContentsView; 1270 }; 1271 1272 1273 // #pragma mark - PackageInfoView 1274 1275 1276 PackageInfoView::PackageInfoView(BLocker* modelLock, 1277 PackageActionHandler* handler) 1278 : 1279 BView("package info view", 0), 1280 fModelLock(modelLock), 1281 fPackageListener(new(std::nothrow) OnePackageMessagePackageListener(this)) 1282 { 1283 fCardLayout = new BCardLayout(); 1284 SetLayout(fCardLayout); 1285 1286 BGroupView* noPackageCard = new BGroupView("no package card", B_VERTICAL); 1287 AddChild(noPackageCard); 1288 1289 BStringView* noPackageView = new BStringView("no package view", 1290 B_TRANSLATE("Click a package to view information")); 1291 noPackageView->SetHighColor(kLightBlack); 1292 noPackageView->SetExplicitAlignment(BAlignment( 1293 B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER)); 1294 1295 BLayoutBuilder::Group<>(noPackageCard) 1296 .Add(noPackageView) 1297 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)) 1298 ; 1299 1300 BGroupView* packageCard = new BGroupView("package card", B_VERTICAL); 1301 AddChild(packageCard); 1302 1303 fCardLayout->SetVisibleItem((int32)0); 1304 1305 fTitleView = new TitleView(); 1306 fPackageActionView = new PackageActionView(handler); 1307 fPackageActionView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1308 B_SIZE_UNSET)); 1309 fPagesView = new PagesView(); 1310 1311 BLayoutBuilder::Group<>(packageCard) 1312 .AddGroup(B_HORIZONTAL, 0.0f) 1313 .Add(fTitleView, 6.0f) 1314 .Add(fPackageActionView, 1.0f) 1315 .SetInsets( 1316 B_USE_DEFAULT_SPACING, 0.0f, 1317 B_USE_DEFAULT_SPACING, 0.0f) 1318 .End() 1319 .Add(fPagesView) 1320 ; 1321 1322 Clear(); 1323 } 1324 1325 1326 PackageInfoView::~PackageInfoView() 1327 { 1328 fPackageListener->SetPackage(PackageInfoRef(NULL)); 1329 delete fPackageListener; 1330 } 1331 1332 1333 void 1334 PackageInfoView::AttachedToWindow() 1335 { 1336 } 1337 1338 1339 void 1340 PackageInfoView::MessageReceived(BMessage* message) 1341 { 1342 switch (message->what) { 1343 case MSG_UPDATE_PACKAGE: 1344 { 1345 if (fPackageListener->Package().Get() == NULL) 1346 break; 1347 1348 BString name; 1349 uint32 changes; 1350 if (message->FindString("name", &name) != B_OK 1351 || message->FindUInt32("changes", &changes) != B_OK) { 1352 break; 1353 } 1354 1355 const PackageInfoRef& package = fPackageListener->Package(); 1356 if (package->Name() != name) 1357 break; 1358 1359 BAutolock _(fModelLock); 1360 1361 if ((changes & PKG_CHANGED_SUMMARY) != 0 1362 || (changes & PKG_CHANGED_DESCRIPTION) != 0 1363 || (changes & PKG_CHANGED_SCREENSHOTS) != 0 1364 || (changes & PKG_CHANGED_TITLE) != 0 1365 || (changes & PKG_CHANGED_RATINGS) != 0 1366 || (changes & PKG_CHANGED_STATE) != 0 1367 || (changes & PKG_CHANGED_CHANGELOG) != 0) { 1368 fPagesView->SetPackage(package, false); 1369 } 1370 1371 if ((changes & PKG_CHANGED_TITLE) != 0 1372 || (changes & PKG_CHANGED_RATINGS) != 0) { 1373 fTitleView->SetPackage(*package.Get()); 1374 } 1375 1376 if ((changes & PKG_CHANGED_STATE) != 0) { 1377 fPackageActionView->SetPackage(*package.Get()); 1378 } 1379 1380 break; 1381 } 1382 default: 1383 BView::MessageReceived(message); 1384 break; 1385 } 1386 } 1387 1388 1389 void 1390 PackageInfoView::SetPackage(const PackageInfoRef& packageRef) 1391 { 1392 BAutolock _(fModelLock); 1393 1394 if (packageRef.Get() == NULL) { 1395 Clear(); 1396 return; 1397 } 1398 1399 bool switchToDefaultTab = true; 1400 if (fPackage == packageRef) { 1401 // When asked to display the already showing package ref, 1402 // don't switch to the default tab. 1403 switchToDefaultTab = false; 1404 } else if (fPackage.Get() != NULL && packageRef.Get() != NULL 1405 && fPackage->Name() == packageRef->Name()) { 1406 // When asked to display a different PackageInfo instance, 1407 // but it has the same package title as the already showing 1408 // instance, this probably means there was a repository 1409 // refresh and we are in fact still requested to show the 1410 // same package as before the refresh. 1411 switchToDefaultTab = false; 1412 } 1413 1414 const PackageInfo& package = *packageRef.Get(); 1415 1416 fTitleView->SetPackage(package); 1417 fPackageActionView->SetPackage(package); 1418 fPagesView->SetPackage(packageRef, switchToDefaultTab); 1419 1420 fCardLayout->SetVisibleItem(1); 1421 1422 fPackageListener->SetPackage(packageRef); 1423 1424 // Set the fPackage reference last, so we keep a reference to the 1425 // previous package before switching all the views to the new package. 1426 // Otherwise the PackageInfo instance may go away because we had the 1427 // last reference. And some of the views, the PackageActionView for 1428 // example, keeps references to stuff from the previous package and 1429 // access it while switching to the new package. 1430 fPackage = packageRef; 1431 } 1432 1433 1434 void 1435 PackageInfoView::Clear() 1436 { 1437 BAutolock _(fModelLock); 1438 1439 fTitleView->Clear(); 1440 fPackageActionView->Clear(); 1441 fPagesView->Clear(); 1442 1443 fCardLayout->SetVisibleItem((int32)0); 1444 1445 fPackageListener->SetPackage(PackageInfoRef(NULL)); 1446 1447 fPackage.Unset(); 1448 } 1449