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