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