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