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