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