1 /* 2 * Copyright 2013-214, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>. 4 * Copyright 2020-2024, Andrew Lindesay <apl@lindesay.co.nz>. 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 8 #include "FeaturedPackagesView.h" 9 10 #include <algorithm> 11 #include <vector> 12 13 #include <Bitmap.h> 14 #include <Catalog.h> 15 #include <ControlLook.h> 16 #include <Font.h> 17 #include <LayoutBuilder.h> 18 #include <LayoutItem.h> 19 #include <Message.h> 20 #include <ScrollView.h> 21 #include <StringView.h> 22 23 #include "BitmapView.h" 24 #include "HaikuDepotConstants.h" 25 #include "LocaleUtils.h" 26 #include "Logger.h" 27 #include "MainWindow.h" 28 #include "MarkupTextView.h" 29 #include "MessagePackageListener.h" 30 #include "PackageUtils.h" 31 #include "RatingUtils.h" 32 #include "RatingView.h" 33 #include "SharedIcons.h" 34 35 36 #undef B_TRANSLATION_CONTEXT 37 #define B_TRANSLATION_CONTEXT "FeaturedPackagesView" 38 39 #define SIZE_ICON 64.0f 40 41 // If the space for the summary has less than this many "M" characters then the summary will not be 42 // displayed. 43 #define MINIMUM_M_COUNT_SUMMARY 10.0f 44 45 // The title area will be this many times the width of an "M". 46 #define M_COUNT_TITLE 10 47 48 // The fraction of the icon width that is left of the trailing icon 49 #define TRAILING_ICON_PADDING_LEFT_FACTOR 0.1 50 51 // The faction of an M-space that is left between the title and the first trailing icon. 52 #define TITLE_RIGHT_TRAILING_ICON_PADDING_M_FACTOR 0.1 53 54 55 // #pragma mark - PackageView 56 57 58 class StackedFeaturesPackageBandMetrics 59 { 60 public: 61 StackedFeaturesPackageBandMetrics(float width, BFont* titleFont, BFont* metadataFont) 62 { 63 float padding = be_control_look->DefaultItemSpacing(); 64 BSize iconSize = BControlLook::ComposeIconSize(SIZE_ICON); 65 66 font_height titleFontHeight; 67 font_height metadataFontHeight; 68 titleFont->GetHeight(&titleFontHeight); 69 metadataFont->GetHeight(&metadataFontHeight); 70 71 float totalTitleAndMetadataHeight = titleFontHeight.ascent + titleFontHeight.descent 72 + titleFontHeight.leading 73 + metadataFontHeight.leading 74 + 2.0 * (metadataFontHeight.ascent + metadataFontHeight.descent); 75 76 fHeight = fmaxf(totalTitleAndMetadataHeight, iconSize.Height()) + 2.0 * padding; 77 78 { 79 float iconInset = (fHeight - iconSize.Width()) / 2.0; 80 fIconRect = BRect(padding, iconInset, iconSize.Width() + padding, 81 iconSize.Height() + iconInset); 82 } 83 84 { 85 float titleWidthM = titleFont->StringWidth("M"); 86 87 float leftTitlePublisherAndChronologicalInfo = fIconRect.right + padding; 88 float rightTitlePublisherAndChronologicalInfo = fminf(width, fIconRect.Size().Width() 89 + (2.0 * padding) + (titleWidthM * M_COUNT_TITLE)); 90 91 // left, top, right bottom 92 fTitleRect = BRect(leftTitlePublisherAndChronologicalInfo, 93 (fHeight - totalTitleAndMetadataHeight) / 2.0, 94 rightTitlePublisherAndChronologicalInfo, 95 ((fHeight - totalTitleAndMetadataHeight) / 2.0) 96 + titleFontHeight.ascent + titleFontHeight.descent); 97 98 fPublisherRect = BRect(leftTitlePublisherAndChronologicalInfo, 99 fTitleRect.bottom + titleFontHeight.leading, 100 rightTitlePublisherAndChronologicalInfo, 101 fTitleRect.bottom + titleFontHeight.leading 102 + metadataFontHeight.ascent + metadataFontHeight.descent); 103 104 fChronologicalInfoRect = BRect(leftTitlePublisherAndChronologicalInfo, 105 fPublisherRect.bottom + metadataFontHeight.leading, 106 rightTitlePublisherAndChronologicalInfo, 107 fPublisherRect.bottom + metadataFontHeight.leading 108 + metadataFontHeight.ascent + metadataFontHeight.descent); 109 } 110 111 // sort out the ratings display 112 113 { 114 BSize ratingStarSize = SharedIcons::IconStarBlue16Scaled()->Bitmap()->Bounds().Size(); 115 RatingStarsMetrics ratingStarsMetrics(ratingStarSize); 116 117 fRatingStarsRect = BRect(BPoint(fTitleRect.right + padding, 118 (fHeight - ratingStarsMetrics.Size().Height()) / 2), ratingStarsMetrics.Size()); 119 120 if (fRatingStarsRect.right > width) 121 fRatingStarsRect = BRect(); 122 else { 123 // Now sort out the position for the summary. This is reckoned as a container 124 // rect because it would be nice to layout the text with newlines and not just a 125 // single line. 126 127 fSummaryContainerRect = BRect(fRatingStarsRect.right + (padding * 2.0), padding, 128 width - padding, fHeight - (padding * 2.0)); 129 130 float metadataWidthM = metadataFont->StringWidth("M"); 131 132 if (fSummaryContainerRect.Size().Width() < MINIMUM_M_COUNT_SUMMARY * metadataWidthM) 133 fSummaryContainerRect = BRect(); 134 } 135 } 136 } 137 138 float Height() 139 { 140 return fHeight; 141 } 142 143 BRect IconRect() 144 { 145 return fIconRect; 146 } 147 148 BRect TitleRect() 149 { 150 return fTitleRect; 151 } 152 153 BRect PublisherRect() 154 { 155 return fPublisherRect; 156 } 157 158 BRect ChronologicalInfoRect() 159 { 160 return fChronologicalInfoRect; 161 } 162 163 BRect RatingStarsRect() 164 { 165 return fRatingStarsRect; 166 } 167 168 BRect SummaryContainerRect() 169 { 170 return fSummaryContainerRect; 171 } 172 173 private: 174 float fHeight; 175 176 BRect fIconRect; 177 BRect fTitleRect; 178 BRect fPublisherRect; 179 BRect fChronologicalInfoRect; 180 181 BRect fRatingStarsRect; 182 183 BRect fSummaryContainerRect; 184 }; 185 186 187 class StackedFeaturedPackagesView : public BView { 188 public: 189 StackedFeaturedPackagesView(Model& model) 190 : 191 BView("stacked featured packages view", B_WILL_DRAW | B_FRAME_EVENTS), 192 fModel(model), 193 fSelectedIndex(-1), 194 fPackageListener( 195 new(std::nothrow) OnePackageMessagePackageListener(this)), 196 fLowestIndexAddedOrRemoved(-1) 197 { 198 SetEventMask(B_POINTER_EVENTS); 199 200 fTitleFont = StackedFeaturedPackagesView::CreateTitleFont(); 201 fMetadataFont = StackedFeaturedPackagesView::CreateMetadataFont(); 202 fSummaryFont = StackedFeaturedPackagesView::CreateSummaryFont(); 203 fBandMetrics = CreateBandMetrics(); 204 205 Clear(); 206 } 207 208 209 virtual ~StackedFeaturedPackagesView() 210 { 211 fPackageListener->SetPackage(PackageInfoRef(NULL)); 212 fPackageListener->ReleaseReference(); 213 delete fBandMetrics; 214 delete fTitleFont; 215 delete fMetadataFont; 216 delete fSummaryFont; 217 } 218 219 // #pragma mark - message handling and events 220 221 virtual void MessageReceived(BMessage* message) 222 { 223 switch (message->what) { 224 case MSG_UPDATE_PACKAGE: 225 { 226 BString name; 227 if (message->FindString("name", &name) != B_OK) 228 HDINFO("expected 'name' key on package update message"); 229 else 230 _HandleUpdatePackage(name); 231 break; 232 } 233 234 case B_COLORS_UPDATED: 235 { 236 Invalidate(); 237 break; 238 } 239 240 default: 241 BView::MessageReceived(message); 242 break; 243 } 244 } 245 246 247 virtual void MouseDown(BPoint where) 248 { 249 if (Window()->IsActive() && !IsHidden()) { 250 BRect bounds = Bounds(); 251 BRect parentBounds = Parent()->Bounds(); 252 ConvertFromParent(&parentBounds); 253 bounds = bounds & parentBounds; 254 if (bounds.Contains(where)) { 255 _MessageSelectIndex(_IndexOfY(where.y)); 256 MakeFocus(); 257 } 258 } 259 } 260 261 262 virtual void KeyDown(const char* bytes, int32 numBytes) 263 { 264 char key = bytes[0]; 265 266 switch (key) { 267 case B_RIGHT_ARROW: 268 case B_DOWN_ARROW: 269 { 270 int32 lastIndex = static_cast<int32>(fPackages.size()) - 1; 271 if (!IsEmpty() && fSelectedIndex != -1 272 && fSelectedIndex < lastIndex) { 273 _MessageSelectIndex(fSelectedIndex + 1); 274 } 275 break; 276 } 277 case B_LEFT_ARROW: 278 case B_UP_ARROW: 279 if (fSelectedIndex > 0) 280 _MessageSelectIndex( fSelectedIndex - 1); 281 break; 282 case B_PAGE_UP: 283 { 284 BRect bounds = Bounds(); 285 ScrollTo(0, fmaxf(0, bounds.top - bounds.Height())); 286 break; 287 } 288 case B_PAGE_DOWN: 289 { 290 BRect bounds = Bounds(); 291 float height = fPackages.size() * fBandMetrics->Height(); 292 float maxScrollY = height - bounds.Height(); 293 float pageDownScrollY = bounds.top + bounds.Height(); 294 ScrollTo(0, fminf(maxScrollY, pageDownScrollY)); 295 break; 296 } 297 default: 298 BView::KeyDown(bytes, numBytes); 299 break; 300 } 301 } 302 303 304 /*! This method will send a message to the Window so that it can signal 305 back to this and other views that a package has been selected. This 306 method won't actually change the state of this view directly. 307 */ 308 309 void _MessageSelectIndex(int32 index) const 310 { 311 if (index != -1) { 312 BMessage message(MSG_PACKAGE_SELECTED); 313 BString packageName = fPackages[index]->Name(); 314 message.AddString("name", packageName); 315 Window()->PostMessage(&message); 316 } 317 } 318 319 320 virtual void FrameResized(float width, float height) 321 { 322 BView::FrameResized(width, height); 323 324 delete fBandMetrics; 325 fBandMetrics = CreateBandMetrics(); 326 327 Invalidate(); 328 } 329 330 331 // #pragma mark - update / add / remove / clear data 332 333 334 void UpdatePackage(uint32 changeMask, const PackageInfoRef& package) 335 { 336 // TODO; could optimize the invalidation? 337 int32 index = _IndexOfPackage(package); 338 if (index >= 0) { 339 fPackages[index] = package; 340 Invalidate(_RectOfIndex(index)); 341 } 342 } 343 344 345 void Clear() 346 { 347 for (std::vector<PackageInfoRef>::iterator it = fPackages.begin(); 348 it != fPackages.end(); it++) { 349 (*it)->RemoveListener(fPackageListener); 350 } 351 fPackages.clear(); 352 fSelectedIndex = -1; 353 354 Invalidate(); 355 } 356 357 358 static BFont* CreateTitleFont() 359 { 360 BFont* font = new BFont(be_plain_font); 361 font_family family; 362 font_style style; 363 font->SetSize(ceilf(font->Size() * 1.8f)); 364 font->GetFamilyAndStyle(&family, &style); 365 font->SetFamilyAndStyle(family, "Bold"); 366 return font; 367 } 368 369 370 static BFont* CreateMetadataFont() 371 { 372 BFont* font = new BFont(be_plain_font); 373 font_family family; 374 font_style style; 375 font->GetFamilyAndStyle(&family, &style); 376 font->SetFamilyAndStyle(family, "Italic"); 377 return font; 378 } 379 380 381 static BFont* CreateSummaryFont() 382 { 383 return new BFont(be_plain_font); 384 } 385 386 387 StackedFeaturesPackageBandMetrics* CreateBandMetrics() 388 { 389 return new StackedFeaturesPackageBandMetrics(Bounds().Width(), fTitleFont, fMetadataFont); 390 } 391 392 393 bool IsEmpty() const 394 { 395 return fPackages.size() == 0; 396 } 397 398 399 void _HandleUpdatePackage(const BString& name) 400 { 401 int32 index = _IndexOfName(name); 402 if (index != -1) 403 Invalidate(_RectOfIndex(index)); 404 } 405 406 407 static int _CmpProminences(int64 a, int64 b) 408 { 409 if (a <= 0) 410 a = PROMINANCE_ORDERING_MAX; 411 if (b <= 0) 412 b = PROMINANCE_ORDERING_MAX; 413 if (a == b) 414 return 0; 415 if (a > b) 416 return 1; 417 return -1; 418 } 419 420 421 /*! This method will return true if the packageA is ordered before 422 packageB. 423 */ 424 425 static bool _IsPackageBefore(const PackageInfoRef& packageA, const PackageInfoRef& packageB) 426 { 427 if (!packageA.IsSet() || !packageB.IsSet()) 428 HDFATAL("unexpected NULL reference in a referencable"); 429 430 uint32 prominenceA = 0; 431 uint32 prominenceB = 0; 432 433 PackageClassificationInfoRef classificationInfoA = packageA->PackageClassificationInfo(); 434 PackageClassificationInfoRef classificationInfoB = packageB->PackageClassificationInfo(); 435 436 if (classificationInfoA.IsSet()) 437 prominenceA = classificationInfoA->Prominence(); 438 439 if (classificationInfoB.IsSet()) 440 prominenceB = classificationInfoB->Prominence(); 441 442 int c = static_cast<int>(prominenceA) - static_cast<int>(prominenceB); 443 444 if (c == 0) { 445 BString titleA; 446 BString titleB; 447 PackageUtils::Title(packageA, titleA); 448 PackageUtils::Title(packageB, titleB); 449 c = titleA.ICompare(titleB); 450 } 451 452 if (c == 0) 453 c = packageA->Name().Compare(packageB->Name()); 454 455 return c < 0; 456 } 457 458 459 void BeginAddRemove() 460 { 461 fLowestIndexAddedOrRemoved = INT32_MAX; 462 } 463 464 465 void EndAddRemove() 466 { 467 if (fLowestIndexAddedOrRemoved < INT32_MAX) { 468 if (fPackages.empty()) 469 Invalidate(); 470 else { 471 BRect invalidRect = Bounds(); 472 invalidRect.top = _YOfIndex(fLowestIndexAddedOrRemoved); 473 Invalidate(invalidRect); 474 } 475 } 476 } 477 478 479 void AddPackage(const PackageInfoRef& package) 480 { 481 // fPackages is sorted and for this reason it is possible to find the 482 // insertion point by identifying the first item in fPackages that does 483 // not return true from the method '_IsPackageBefore'. 484 485 std::vector<PackageInfoRef>::iterator itInsertionPt 486 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 487 &_IsPackageBefore); 488 489 if (itInsertionPt == fPackages.end() 490 || package->Name() != (*itInsertionPt)->Name()) { 491 int32 insertionIndex = 492 std::distance<std::vector<PackageInfoRef>::const_iterator>( 493 fPackages.begin(), itInsertionPt); 494 if (fSelectedIndex >= insertionIndex) 495 fSelectedIndex++; 496 fPackages.insert(itInsertionPt, package); 497 package->AddListener(fPackageListener); 498 if (insertionIndex < fLowestIndexAddedOrRemoved) 499 fLowestIndexAddedOrRemoved = insertionIndex; 500 } 501 } 502 503 504 void RemovePackage(const PackageInfoRef& package) 505 { 506 int32 index = _IndexOfPackage(package); 507 if (index >= 0) { 508 if (fSelectedIndex == index) 509 fSelectedIndex = -1; 510 if (fSelectedIndex > index) 511 fSelectedIndex--; 512 fPackages[index]->RemoveListener(fPackageListener); 513 fPackages.erase(fPackages.begin() + index); 514 if (index < fLowestIndexAddedOrRemoved) 515 fLowestIndexAddedOrRemoved = index; 516 } 517 } 518 519 520 // #pragma mark - selection and index handling 521 522 523 void SelectPackage(const PackageInfoRef& package) 524 { 525 _SelectIndex(_IndexOfPackage(package)); 526 } 527 528 529 void _SelectIndex(int32 index) 530 { 531 if (index != fSelectedIndex) { 532 int32 previousSelectedIndex = fSelectedIndex; 533 fSelectedIndex = index; 534 if (fSelectedIndex >= 0) 535 Invalidate(_RectOfIndex(fSelectedIndex)); 536 if (previousSelectedIndex >= 0) 537 Invalidate(_RectOfIndex(previousSelectedIndex)); 538 _EnsureIndexVisible(index); 539 } 540 } 541 542 543 int32 _IndexOfPackage(PackageInfoRef package) const 544 { 545 std::vector<PackageInfoRef>::const_iterator it 546 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 547 &_IsPackageBefore); 548 549 return (it == fPackages.end() || (*it)->Name() != package->Name()) 550 ? -1 : it - fPackages.begin(); 551 } 552 553 554 int32 _IndexOfName(const BString& name) const 555 { 556 // TODO; slow linear search. 557 // the fPackages is not sorted on name and for this reason it is not 558 // possible to do a binary search. 559 for (uint32 i = 0; i < fPackages.size(); i++) { 560 if (fPackages[i]->Name() == name) 561 return i; 562 } 563 return -1; 564 } 565 566 567 // #pragma mark - drawing and rendering 568 569 570 virtual void Draw(BRect updateRect) 571 { 572 SetHighUIColor(B_LIST_BACKGROUND_COLOR); 573 FillRect(updateRect); 574 575 int32 iStart = _IndexRoundedOfY(updateRect.top); 576 577 if (iStart != -1) { 578 int32 iEnd = _IndexRoundedOfY(updateRect.bottom); 579 for (int32 i = iStart; i <= iEnd; i++) 580 _DrawPackageAtIndex(updateRect, i); 581 } 582 } 583 584 585 void _DrawPackageAtIndex(BRect updateRect, int32 index) 586 { 587 _DrawPackage(updateRect, fPackages[index], _YOfIndex(index), index == fSelectedIndex); 588 } 589 590 591 void _DrawPackage(BRect updateRect, PackageInfoRef pkg, float y, bool selected) 592 { 593 if (selected) { 594 SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR); 595 FillRect(_RectOfY(y), B_SOLID_LOW); 596 } else { 597 SetLowUIColor(B_LIST_BACKGROUND_COLOR); 598 } 599 600 BRect iconRect = fBandMetrics->IconRect(); 601 BRect titleRect = fBandMetrics->TitleRect(); 602 BRect publisherRect = fBandMetrics->PublisherRect(); 603 BRect chronologicalInfoRect = fBandMetrics->ChronologicalInfoRect(); 604 BRect ratingStarsRect = fBandMetrics->RatingStarsRect(); 605 BRect summaryContainerRect = fBandMetrics->SummaryContainerRect(); 606 607 iconRect.OffsetBy(0.0, y); 608 titleRect.OffsetBy(0.0, y); 609 publisherRect.OffsetBy(0.0, y); 610 chronologicalInfoRect.OffsetBy(0.0, y); 611 ratingStarsRect.OffsetBy(0.0, y); 612 summaryContainerRect.OffsetBy(0.0, y); 613 614 // TODO; optimization; the updateRect may only cover some of this? 615 _DrawPackageIcon(iconRect, pkg, selected); 616 _DrawPackageTitle(titleRect, pkg, selected); 617 _DrawPackagePublisher(publisherRect, pkg, selected); 618 _DrawPackageChronologicalInfo(chronologicalInfoRect, pkg, selected); 619 _DrawPackageRating(ratingStarsRect, pkg); 620 _DrawPackageSummary(summaryContainerRect, pkg, selected); 621 } 622 623 624 void _DrawPackageIcon(BRect iconRect, PackageInfoRef pkg, bool selected) 625 { 626 if (!iconRect.IsValid()) 627 return; 628 629 BitmapHolderRef icon; 630 status_t iconResult = fModel.GetPackageIconRepository().GetIcon(pkg->Name(), 631 iconRect.Width(), icon); 632 633 if (iconResult == B_OK) { 634 if (icon.IsSet()) { 635 const BBitmap* bitmap = icon->Bitmap(); 636 637 if (bitmap != NULL && bitmap->IsValid()) { 638 SetDrawingMode(B_OP_ALPHA); 639 DrawBitmap(bitmap, bitmap->Bounds(), iconRect, B_FILTER_BITMAP_BILINEAR); 640 } 641 } 642 } 643 } 644 645 646 void _DrawPackageTitle(BRect textRect, PackageInfoRef pkg, bool selected) 647 { 648 if (!textRect.IsValid()) 649 return; 650 651 std::vector<BitmapHolderRef> trailingIconBitmaps; 652 653 if (PackageUtils::State(pkg) == ACTIVATED) 654 trailingIconBitmaps.push_back(SharedIcons::IconInstalled16Scaled()); 655 656 if (PackageUtils::IsNativeDesktop(pkg)) 657 trailingIconBitmaps.push_back(SharedIcons::IconNative16Scaled()); 658 659 float trailingIconsWidth = 0.0f; 660 661 for (std::vector<BitmapHolderRef>::iterator it = trailingIconBitmaps.begin(); 662 it != trailingIconBitmaps.end(); it++) { 663 trailingIconsWidth += (*it)->Bitmap()->Bounds().Width() 664 * (1.0 + TRAILING_ICON_PADDING_LEFT_FACTOR); 665 } 666 667 SetDrawingMode(B_OP_COPY); 668 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR); 669 SetFont(fTitleFont); 670 671 float titleRightTrailingIconsPadding = 0.0f; 672 673 if (!trailingIconBitmaps.empty()) { 674 titleRightTrailingIconsPadding = StringWidth("M") 675 * TITLE_RIGHT_TRAILING_ICON_PADDING_M_FACTOR; 676 } 677 678 font_height fontHeight; 679 fTitleFont->GetHeight(&fontHeight); 680 BPoint pt = textRect.LeftTop() + BPoint(0.0, + fontHeight.ascent); 681 682 BString title; 683 PackageUtils::TitleOrName(pkg, title); 684 685 BString renderedText = title; 686 TruncateString(&renderedText, B_TRUNCATE_END, textRect.Width() 687 - (titleRightTrailingIconsPadding + trailingIconsWidth)); 688 689 DrawString(renderedText, pt); 690 691 // now draw the trailing icons. 692 693 float stringWidth = StringWidth(title); 694 float trailingIconX = textRect.left + stringWidth + titleRightTrailingIconsPadding; 695 float trailingIconMidY = textRect.top + (textRect.Height() / 2.0); 696 697 SetDrawingMode(B_OP_ALPHA); 698 699 for (std::vector<BitmapHolderRef>::iterator it = trailingIconBitmaps.begin(); 700 it != trailingIconBitmaps.end(); it++) { 701 const BBitmap* bitmap = (*it)->Bitmap(); 702 BRect bitmapBounds = bitmap->Bounds(); 703 704 float trailingIconTopLeftPtX = ceilf( 705 trailingIconX + (bitmapBounds.Width() * TRAILING_ICON_PADDING_LEFT_FACTOR)) + 0.5; 706 float trailingIconTopLeftPtY = ceilf( 707 trailingIconMidY - (bitmapBounds.Height() / 2.0)) + 0.5; 708 709 BRect trailingIconRect(BPoint(trailingIconTopLeftPtX, trailingIconTopLeftPtY), 710 bitmap->Bounds().Size()); 711 712 DrawBitmap(bitmap, bitmapBounds, trailingIconRect, B_FILTER_BITMAP_BILINEAR); 713 714 trailingIconX = trailingIconRect.right; 715 } 716 } 717 718 719 void _DrawPackageGenericTextSlug(BRect textRect, const BString& text, bool selected) 720 { 721 if (!textRect.IsValid()) 722 return; 723 724 SetDrawingMode(B_OP_COPY); 725 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR); 726 SetFont(fMetadataFont); 727 728 font_height fontHeight; 729 fMetadataFont->GetHeight(&fontHeight); 730 BPoint pt = textRect.LeftTop() + BPoint(0.0, + fontHeight.ascent); 731 732 BString renderedText(text); 733 TruncateString(&renderedText, B_TRUNCATE_END, textRect.Width()); 734 735 DrawString(renderedText, pt); 736 } 737 738 739 void _DrawPackagePublisher(BRect textRect, PackageInfoRef pkg, bool selected) 740 { 741 _DrawPackageGenericTextSlug(textRect, pkg->Publisher().Name(), selected); 742 } 743 744 745 void _DrawPackageChronologicalInfo(BRect textRect, PackageInfoRef pkg, bool selected) 746 { 747 BString versionCreateTimestampPresentation 748 = LocaleUtils::TimestampToDateString(pkg->VersionCreateTimestamp()); 749 _DrawPackageGenericTextSlug(textRect, versionCreateTimestampPresentation, selected); 750 } 751 752 753 // TODO; show the sample size 754 void _DrawPackageRating(BRect ratingRect, PackageInfoRef pkg) 755 { 756 if (!ratingRect.IsValid()) 757 return; 758 759 UserRatingInfoRef userRatingInfo = pkg->UserRatingInfo(); 760 761 if (userRatingInfo.IsSet()) { 762 UserRatingSummaryRef userRatingSummary = userRatingInfo->Summary(); 763 764 if (userRatingSummary.IsSet()) 765 RatingUtils::Draw(this, ratingRect.LeftTop(), userRatingSummary->AverageRating()); 766 } 767 } 768 769 770 // TODO; handle multi-line rendering of the text 771 void _DrawPackageSummary(BRect textRect, PackageInfoRef pkg, bool selected) 772 { 773 if (!textRect.IsValid()) 774 return; 775 776 SetDrawingMode(B_OP_COPY); 777 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR); 778 SetFont(fSummaryFont); 779 780 font_height fontHeight; 781 fSummaryFont->GetHeight(&fontHeight); 782 783 // The text rect is a container into which later text can be made to flow multi-line. For 784 // now just draw one line of the summary. 785 786 BPoint pt = textRect.LeftTop() + BPoint(0.0, 787 (textRect.Size().Height() / 2.0) - ((fontHeight.ascent + fontHeight.descent) / 2.0) 788 + fontHeight.ascent); 789 790 BString summary; 791 PackageUtils::Summary(pkg, summary); 792 TruncateString(&summary, B_TRUNCATE_END, textRect.Width()); 793 794 DrawString(summary, pt); 795 } 796 797 798 // #pragma mark - geometry and scrolling 799 800 801 /*! This method will make sure that the package at the given index is 802 visible. If the whole of the package can be seen already then it will 803 do nothing. If the package is located above the visible region then it 804 will scroll up to it. If the package is located below the visible 805 region then it will scroll down to it. 806 */ 807 808 void _EnsureIndexVisible(int32 index) 809 { 810 if (!_IsIndexEntirelyVisible(index)) { 811 BRect bounds = Bounds(); 812 int32 indexOfCentreVisible = _IndexOfY(bounds.top + bounds.Height() / 2); 813 if (index < indexOfCentreVisible) 814 ScrollTo(0, _YOfIndex(index)); 815 else { 816 float scrollPointY = (_YOfIndex(index) + fBandMetrics->Height()) - bounds.Height(); 817 ScrollTo(0, scrollPointY); 818 } 819 } 820 } 821 822 823 /*! This method will return true if the package at the supplied index is 824 entirely visible. 825 */ 826 827 bool _IsIndexEntirelyVisible(int32 index) 828 { 829 BRect bounds = Bounds(); 830 return bounds == (bounds | _RectOfIndex(index)); 831 } 832 833 834 BRect _RectOfIndex(int32 index) const 835 { 836 if (index < 0) 837 return BRect(0, 0, 0, 0); 838 return _RectOfY(_YOfIndex(index)); 839 } 840 841 842 /*! Provides the top coordinate (offset from the top of view) of the package 843 supplied. If the package does not exist in the view then the coordinate 844 returned will be B_SIZE_UNSET. 845 */ 846 847 float TopOfPackage(const PackageInfoRef& package) 848 { 849 if (package.IsSet()) { 850 int index = _IndexOfPackage(package); 851 if (-1 != index) 852 return _YOfIndex(index); 853 } 854 return B_SIZE_UNSET; 855 } 856 857 858 BRect _RectOfY(float y) const 859 { 860 return BRect(0, y, Bounds().Width(), y + fBandMetrics->Height()); 861 } 862 863 864 float _YOfIndex(int32 i) const 865 { 866 return i * fBandMetrics->Height(); 867 } 868 869 870 /*! Finds the offset into the list of packages for the y-coord in the view's 871 coordinate space. If the y is above or below the list of packages then 872 this will return -1 to signal this. 873 */ 874 875 int32 _IndexOfY(float y) const 876 { 877 if (fPackages.empty()) 878 return -1; 879 int32 i = static_cast<int32>(y / fBandMetrics->Height()); 880 if (i < 0 || i >= static_cast<int32>(fPackages.size())) 881 return -1; 882 return i; 883 } 884 885 886 /*! Find the offset into the list of packages for the y-coord in the view's 887 coordinate space. If the y is above or below the list of packages then 888 this will return the first or last package index respectively. If there 889 are no packages then this will return -1; 890 */ 891 892 int32 _IndexRoundedOfY(float y) const 893 { 894 if (fPackages.empty()) 895 return -1; 896 int32 i = static_cast<int32>(y / fBandMetrics->Height()); 897 if (i < 0) 898 return 0; 899 return std::min(i, (int32) (fPackages.size() - 1)); 900 } 901 902 903 virtual BSize PreferredSize() 904 { 905 return BSize(B_SIZE_UNLIMITED, 906 fBandMetrics->Height() * static_cast<float>(fPackages.size())); 907 } 908 909 910 private: 911 Model& fModel; 912 std::vector<PackageInfoRef> 913 fPackages; 914 int32 fSelectedIndex; 915 OnePackageMessagePackageListener* 916 fPackageListener; 917 int32 fLowestIndexAddedOrRemoved; 918 StackedFeaturesPackageBandMetrics* 919 fBandMetrics; 920 921 BFont* fTitleFont; 922 BFont* fMetadataFont; 923 BFont* fSummaryFont; 924 }; 925 926 927 // #pragma mark - FeaturedPackagesView 928 929 930 FeaturedPackagesView::FeaturedPackagesView(Model& model) 931 : 932 BView(B_TRANSLATE("Featured packages"), 0), 933 fModel(model) 934 { 935 fPackagesView = new StackedFeaturedPackagesView(fModel); 936 937 fScrollView = new BScrollView("featured packages scroll view", 938 fPackagesView, 0, false, true, B_FANCY_BORDER); 939 940 BLayoutBuilder::Group<>(this) 941 .Add(fScrollView, 1.0f); 942 } 943 944 945 FeaturedPackagesView::~FeaturedPackagesView() 946 { 947 } 948 949 950 void 951 FeaturedPackagesView::BeginAddRemove() 952 { 953 fPackagesView->BeginAddRemove(); 954 } 955 956 957 void 958 FeaturedPackagesView::EndAddRemove() 959 { 960 fPackagesView->EndAddRemove(); 961 _AdjustViews(); 962 } 963 964 965 /*! This method will add the package into the list to be displayed. The 966 insertion will occur in alphabetical order. 967 */ 968 969 void 970 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 971 { 972 fPackagesView->AddPackage(package); 973 } 974 975 976 void 977 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 978 { 979 fPackagesView->RemovePackage(package); 980 } 981 982 983 void 984 FeaturedPackagesView::Clear() 985 { 986 HDINFO("did clear the featured packages view"); 987 fPackagesView->Clear(); 988 _AdjustViews(); 989 } 990 991 992 void 993 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 994 bool scrollToEntry) 995 { 996 fPackagesView->SelectPackage(package); 997 998 if (scrollToEntry) { 999 float offset = fPackagesView->TopOfPackage(package); 1000 if (offset != B_SIZE_UNSET) 1001 fPackagesView->ScrollTo(0, offset); 1002 } 1003 } 1004 1005 1006 void 1007 FeaturedPackagesView::DoLayout() 1008 { 1009 BView::DoLayout(); 1010 _AdjustViews(); 1011 } 1012 1013 1014 void 1015 FeaturedPackagesView::_AdjustViews() 1016 { 1017 fScrollView->FrameResized(fScrollView->Frame().Width(), 1018 fScrollView->Frame().Height()); 1019 } 1020