1 /* 2 * Copyright 2013-214, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>. 4 * Copyright 2020-2021, 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 <Font.h> 16 #include <LayoutBuilder.h> 17 #include <LayoutItem.h> 18 #include <Message.h> 19 #include <ScrollView.h> 20 #include <StringView.h> 21 #include <SpaceLayoutItem.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 "ScrollableGroupView.h" 33 #include "SharedBitmap.h" 34 35 36 #undef B_TRANSLATION_CONTEXT 37 #define B_TRANSLATION_CONTEXT "FeaturedPackagesView" 38 39 40 #define HEIGHT_PACKAGE 84.0f 41 #define SIZE_ICON 64.0f 42 #define X_POSITION_RATING 350.0f 43 #define X_POSITION_SUMMARY 500.0f 44 #define WIDTH_RATING 100.0f 45 #define Y_PROPORTION_TITLE 0.35f 46 #define Y_PROPORTION_PUBLISHER 0.60f 47 #define Y_PROPORTION_CHRONOLOGICAL_DATA 0.75f 48 #define PADDING 8.0f 49 50 51 static BitmapRef sInstalledIcon(new(std::nothrow) 52 SharedBitmap(RSRC_INSTALLED), true); 53 54 55 // #pragma mark - PackageView 56 57 58 class StackedFeaturedPackagesView : public BView { 59 public: 60 StackedFeaturedPackagesView(Model& model) 61 : 62 BView("stacked featured packages view", B_WILL_DRAW | B_FRAME_EVENTS), 63 fModel(model), 64 fSelectedIndex(-1), 65 fPackageListener( 66 new(std::nothrow) OnePackageMessagePackageListener(this)), 67 fLowestIndexAddedOrRemoved(-1) 68 { 69 SetEventMask(B_POINTER_EVENTS); 70 Clear(); 71 } 72 73 74 virtual ~StackedFeaturedPackagesView() 75 { 76 fPackageListener->SetPackage(PackageInfoRef(NULL)); 77 fPackageListener->ReleaseReference(); 78 } 79 80 // #pragma mark - message handling and events 81 82 virtual void MessageReceived(BMessage* message) 83 { 84 switch (message->what) { 85 case MSG_UPDATE_PACKAGE: 86 { 87 BString name; 88 if (message->FindString("name", &name) != B_OK) 89 HDINFO("expected 'name' key on package update message"); 90 else 91 _HandleUpdatePackage(name); 92 break; 93 } 94 95 case B_COLORS_UPDATED: 96 { 97 Invalidate(); 98 break; 99 } 100 101 default: 102 BView::MessageReceived(message); 103 break; 104 } 105 } 106 107 108 virtual void MouseDown(BPoint where) 109 { 110 if (Window()->IsActive() && !IsHidden()) { 111 BRect bounds = Bounds(); 112 BRect parentBounds = Parent()->Bounds(); 113 ConvertFromParent(&parentBounds); 114 bounds = bounds & parentBounds; 115 if (bounds.Contains(where)) { 116 _MessageSelectIndex(_IndexOfY(where.y)); 117 MakeFocus(); 118 } 119 } 120 } 121 122 123 virtual void KeyDown(const char* bytes, int32 numBytes) 124 { 125 char key = bytes[0]; 126 127 switch (key) { 128 case B_RIGHT_ARROW: 129 case B_DOWN_ARROW: 130 { 131 int32 lastIndex = static_cast<int32>(fPackages.size()) - 1; 132 if (!IsEmpty() && fSelectedIndex != -1 133 && fSelectedIndex < lastIndex) { 134 _MessageSelectIndex(fSelectedIndex + 1); 135 } 136 break; 137 } 138 case B_LEFT_ARROW: 139 case B_UP_ARROW: 140 if (fSelectedIndex > 0) 141 _MessageSelectIndex( fSelectedIndex - 1); 142 break; 143 case B_PAGE_UP: 144 { 145 BRect bounds = Bounds(); 146 ScrollTo(0, fmaxf(0, bounds.top - bounds.Height())); 147 break; 148 } 149 case B_PAGE_DOWN: 150 { 151 BRect bounds = Bounds(); 152 float height = fPackages.size() * HEIGHT_PACKAGE; 153 float maxScrollY = height - bounds.Height(); 154 float pageDownScrollY = bounds.top + bounds.Height(); 155 ScrollTo(0, fminf(maxScrollY, pageDownScrollY)); 156 break; 157 } 158 default: 159 BView::KeyDown(bytes, numBytes); 160 break; 161 } 162 } 163 164 165 /*! This method will send a message to the Window so that it can signal 166 back to this and other views that a package has been selected. This 167 method won't actually change the state of this view directly. 168 */ 169 170 void _MessageSelectIndex(int32 index) const 171 { 172 if (index != -1) { 173 BMessage message(MSG_PACKAGE_SELECTED); 174 BString packageName = fPackages[index]->Name(); 175 message.AddString("name", packageName); 176 Window()->PostMessage(&message); 177 } 178 } 179 180 181 virtual void FrameResized(float width, float height) 182 { 183 BView::FrameResized(width, height); 184 185 // because the summary text will wrap, a resize of the frame will 186 // result in all of the summary area needing to be redrawn. 187 188 BRect rectToInvalidate = Bounds(); 189 rectToInvalidate.left = X_POSITION_SUMMARY; 190 Invalidate(rectToInvalidate); 191 } 192 193 194 // #pragma mark - update / add / remove / clear data 195 196 197 void UpdatePackage(uint32 changeMask, const PackageInfoRef& package) 198 { 199 // TODO; could optimize the invalidation? 200 int32 index = _IndexOfPackage(package); 201 if (index >= 0) { 202 fPackages[index] = package; 203 Invalidate(_RectOfIndex(index)); 204 } 205 } 206 207 208 void Clear() 209 { 210 for (std::vector<PackageInfoRef>::iterator it = fPackages.begin(); 211 it != fPackages.end(); it++) { 212 (*it)->RemoveListener(fPackageListener); 213 } 214 fPackages.clear(); 215 fSelectedIndex = -1; 216 Invalidate(); 217 } 218 219 220 bool IsEmpty() const 221 { 222 return fPackages.size() == 0; 223 } 224 225 226 void _HandleUpdatePackage(const BString& name) 227 { 228 int32 index = _IndexOfName(name); 229 if (index != -1) 230 Invalidate(_RectOfIndex(index)); 231 } 232 233 234 static int _CmpProminences(int64 a, int64 b) 235 { 236 if (a <= 0) 237 a = PROMINANCE_ORDERING_MAX; 238 if (b <= 0) 239 b = PROMINANCE_ORDERING_MAX; 240 if (a == b) 241 return 0; 242 if (a > b) 243 return 1; 244 return -1; 245 } 246 247 248 /*! This method will return true if the packageA is ordered before 249 packageB. 250 */ 251 252 static bool _IsPackageBefore(const PackageInfoRef& packageA, 253 const PackageInfoRef& packageB) 254 { 255 if (!packageA.IsSet() || !packageB.IsSet()) 256 HDFATAL("unexpected NULL reference in a referencable"); 257 int c = _CmpProminences(packageA->Prominence(), packageB->Prominence()); 258 if (c == 0) 259 c = packageA->Title().ICompare(packageB->Title()); 260 if (c == 0) 261 c = packageA->Name().Compare(packageB->Name()); 262 return c < 0; 263 } 264 265 266 void BeginAddRemove() 267 { 268 fLowestIndexAddedOrRemoved = INT32_MAX; 269 } 270 271 272 void EndAddRemove() 273 { 274 if (fLowestIndexAddedOrRemoved < INT32_MAX) { 275 if (fPackages.empty()) 276 Invalidate(); 277 else { 278 BRect invalidRect = Bounds(); 279 invalidRect.top = _YOfIndex(fLowestIndexAddedOrRemoved); 280 Invalidate(invalidRect); 281 } 282 } 283 } 284 285 286 void AddPackage(const PackageInfoRef& package) 287 { 288 // fPackages is sorted and for this reason it is possible to find the 289 // insertion point by identifying the first item in fPackages that does 290 // not return true from the method '_IsPackageBefore'. 291 292 std::vector<PackageInfoRef>::iterator itInsertionPt 293 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 294 &_IsPackageBefore); 295 296 if (itInsertionPt == fPackages.end() 297 || package->Name() != (*itInsertionPt)->Name()) { 298 int32 insertionIndex = 299 std::distance<std::vector<PackageInfoRef>::const_iterator>( 300 fPackages.begin(), itInsertionPt); 301 if (fSelectedIndex >= insertionIndex) 302 fSelectedIndex++; 303 fPackages.insert(itInsertionPt, package); 304 package->AddListener(fPackageListener); 305 if (insertionIndex < fLowestIndexAddedOrRemoved) 306 fLowestIndexAddedOrRemoved = insertionIndex; 307 } 308 } 309 310 311 void RemovePackage(const PackageInfoRef& package) 312 { 313 int32 index = _IndexOfPackage(package); 314 if (index >= 0) { 315 if (fSelectedIndex == index) 316 fSelectedIndex = -1; 317 if (fSelectedIndex > index) 318 fSelectedIndex--; 319 fPackages[index]->RemoveListener(fPackageListener); 320 fPackages.erase(fPackages.begin() + index); 321 if (index < fLowestIndexAddedOrRemoved) 322 fLowestIndexAddedOrRemoved = index; 323 } 324 } 325 326 327 // #pragma mark - selection and index handling 328 329 330 void SelectPackage(const PackageInfoRef& package) 331 { 332 _SelectIndex(_IndexOfPackage(package)); 333 } 334 335 336 void _SelectIndex(int32 index) 337 { 338 if (index != fSelectedIndex) { 339 int32 previousSelectedIndex = fSelectedIndex; 340 fSelectedIndex = index; 341 if (fSelectedIndex >= 0) 342 Invalidate(_RectOfIndex(fSelectedIndex)); 343 if (previousSelectedIndex >= 0) 344 Invalidate(_RectOfIndex(previousSelectedIndex)); 345 _EnsureIndexVisible(index); 346 } 347 } 348 349 350 int32 _IndexOfPackage(PackageInfoRef package) const 351 { 352 std::vector<PackageInfoRef>::const_iterator it 353 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 354 &_IsPackageBefore); 355 356 return (it == fPackages.end() || (*it)->Name() != package->Name()) 357 ? -1 : it - fPackages.begin(); 358 } 359 360 361 int32 _IndexOfName(const BString& name) const 362 { 363 // TODO; slow linear search. 364 // the fPackages is not sorted on name and for this reason it is not 365 // possible to do a binary search. 366 for (uint32 i = 0; i < fPackages.size(); i++) { 367 if (fPackages[i]->Name() == name) 368 return i; 369 } 370 return -1; 371 } 372 373 374 // #pragma mark - drawing and rendering 375 376 377 virtual void Draw(BRect updateRect) 378 { 379 SetHighUIColor(B_LIST_BACKGROUND_COLOR); 380 FillRect(updateRect); 381 382 int32 iStart = _IndexRoundedOfY(updateRect.top); 383 384 if (iStart != -1) { 385 int32 iEnd = _IndexRoundedOfY(updateRect.bottom); 386 for (int32 i = iStart; i <= iEnd; i++) 387 _DrawPackageAtIndex(updateRect, i); 388 } 389 } 390 391 392 void _DrawPackageAtIndex(BRect updateRect, int32 index) 393 { 394 _DrawPackage(updateRect, fPackages[index], index, _YOfIndex(index), 395 index == fSelectedIndex); 396 } 397 398 399 void _DrawPackage(BRect updateRect, PackageInfoRef pkg, int index, float y, 400 bool selected) 401 { 402 if (selected) { 403 SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR); 404 FillRect(_RectOfY(y), B_SOLID_LOW); 405 } else { 406 SetLowUIColor(B_LIST_BACKGROUND_COLOR); 407 } 408 // TODO; optimization; the updateRect may only cover some of this? 409 _DrawPackageIcon(updateRect, pkg, y, selected); 410 _DrawPackageTitle(updateRect, pkg, y, selected); 411 _DrawPackagePublisher(updateRect, pkg, y, selected); 412 _DrawPackageCronologicalInfo(updateRect, pkg, y, selected); 413 _DrawPackageRating(updateRect, pkg, y, selected); 414 _DrawPackageSummary(updateRect, pkg, y, selected); 415 } 416 417 418 void _DrawPackageIcon(BRect updateRect, PackageInfoRef pkg, float y, 419 bool selected) 420 { 421 BitmapRef icon; 422 status_t iconResult = fModel.GetPackageIconRepository().GetIcon( 423 pkg->Name(), BITMAP_SIZE_64, icon); 424 425 if (iconResult == B_OK) { 426 if (icon.IsSet()) { 427 float inset = (HEIGHT_PACKAGE - SIZE_ICON) / 2.0; 428 BRect targetRect = BRect(inset, y + inset, SIZE_ICON + inset, 429 y + SIZE_ICON + inset); 430 const BBitmap* bitmap = icon->Bitmap(BITMAP_SIZE_64); 431 432 if (bitmap != NULL && bitmap->IsValid()) { 433 SetDrawingMode(B_OP_ALPHA); 434 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 435 B_FILTER_BITMAP_BILINEAR); 436 } 437 } 438 } 439 } 440 441 442 void _DrawPackageTitle(BRect updateRect, PackageInfoRef pkg, float y, 443 bool selected) 444 { 445 static BFont* sFont = NULL; 446 447 if (sFont == NULL) { 448 sFont = new BFont(be_plain_font); 449 GetFont(sFont); 450 font_family family; 451 font_style style; 452 sFont->SetSize(ceilf(sFont->Size() * 1.8f)); 453 sFont->GetFamilyAndStyle(&family, &style); 454 sFont->SetFamilyAndStyle(family, "Bold"); 455 } 456 457 SetDrawingMode(B_OP_COPY); 458 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 459 : B_LIST_ITEM_TEXT_COLOR); 460 SetFont(sFont); 461 BPoint pt(HEIGHT_PACKAGE, y + (HEIGHT_PACKAGE * Y_PROPORTION_TITLE)); 462 DrawString(pkg->Title(), pt); 463 464 if (pkg->State() == ACTIVATED) { 465 const BBitmap* bitmap = sInstalledIcon->Bitmap( 466 BITMAP_SIZE_16); 467 if (bitmap != NULL && bitmap->IsValid()) { 468 float stringWidth = StringWidth(pkg->Title()); 469 float offsetX = pt.x + stringWidth + PADDING; 470 BRect targetRect(offsetX, pt.y - 16, 471 offsetX + 16, pt.y); 472 SetDrawingMode(B_OP_ALPHA); 473 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 474 B_FILTER_BITMAP_BILINEAR); 475 } 476 } 477 } 478 479 480 void _DrawPackageGenericTextSlug(BRect updateRect, PackageInfoRef pkg, 481 const BString& text, float y, float yProportion, bool selected) 482 { 483 static BFont* sFont = NULL; 484 485 if (sFont == NULL) { 486 sFont = new BFont(be_plain_font); 487 font_family family; 488 font_style style; 489 sFont->SetSize(std::max(9.0f, floorf(sFont->Size() * 0.92f))); 490 sFont->GetFamilyAndStyle(&family, &style); 491 sFont->SetFamilyAndStyle(family, "Italic"); 492 } 493 494 SetDrawingMode(B_OP_COPY); 495 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 496 : B_LIST_ITEM_TEXT_COLOR); 497 SetFont(sFont); 498 499 float maxTextWidth = (X_POSITION_RATING - HEIGHT_PACKAGE) - PADDING; 500 BString renderedText(text); 501 TruncateString(&renderedText, B_TRUNCATE_END, maxTextWidth); 502 503 DrawString(renderedText, BPoint(HEIGHT_PACKAGE, 504 y + (HEIGHT_PACKAGE * yProportion))); 505 } 506 507 508 void _DrawPackagePublisher(BRect updateRect, PackageInfoRef pkg, float y, 509 bool selected) 510 { 511 _DrawPackageGenericTextSlug(updateRect, pkg, pkg->Publisher().Name(), y, 512 Y_PROPORTION_PUBLISHER, selected); 513 } 514 515 516 void _DrawPackageCronologicalInfo(BRect updateRect, PackageInfoRef pkg, 517 float y, bool selected) 518 { 519 BString versionCreateTimestampPresentation 520 = LocaleUtils::TimestampToDateString(pkg->VersionCreateTimestamp()); 521 _DrawPackageGenericTextSlug(updateRect, pkg, 522 versionCreateTimestampPresentation, y, 523 Y_PROPORTION_CHRONOLOGICAL_DATA, selected); 524 } 525 526 527 // TODO; show the sample size 528 void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y, 529 bool selected) 530 { 531 BPoint at(X_POSITION_RATING, 532 y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f); 533 RatingUtils::Draw(this, at, 534 pkg->CalculateRatingSummary().averageRating); 535 } 536 537 538 // TODO; handle multi-line rendering of the text 539 void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y, 540 bool selected) 541 { 542 BRect bounds = Bounds(); 543 544 SetDrawingMode(B_OP_COPY); 545 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 546 : B_LIST_ITEM_TEXT_COLOR); 547 SetFont(be_plain_font); 548 549 float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING; 550 BString summary(pkg->ShortDescription()); 551 TruncateString(&summary, B_TRUNCATE_END, maxTextWidth); 552 553 DrawString(summary, BPoint(X_POSITION_SUMMARY, 554 y + (HEIGHT_PACKAGE * 0.5))); 555 } 556 557 558 // #pragma mark - geometry and scrolling 559 560 561 /*! This method will make sure that the package at the given index is 562 visible. If the whole of the package can be seen already then it will 563 do nothing. If the package is located above the visible region then it 564 will scroll up to it. If the package is located below the visible 565 region then it will scroll down to it. 566 */ 567 568 void _EnsureIndexVisible(int32 index) 569 { 570 if (!_IsIndexEntirelyVisible(index)) { 571 BRect bounds = Bounds(); 572 int32 indexOfCentreVisible = _IndexOfY( 573 bounds.top + bounds.Height() / 2); 574 if (index < indexOfCentreVisible) 575 ScrollTo(0, _YOfIndex(index)); 576 else { 577 float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE) 578 - bounds.Height(); 579 ScrollTo(0, scrollPointY); 580 } 581 } 582 } 583 584 585 /*! This method will return true if the package at the supplied index is 586 entirely visible. 587 */ 588 589 bool _IsIndexEntirelyVisible(int32 index) 590 { 591 BRect bounds = Bounds(); 592 return bounds == (bounds | _RectOfIndex(index)); 593 } 594 595 596 BRect _RectOfIndex(int32 index) const 597 { 598 if (index < 0) 599 return BRect(0, 0, 0, 0); 600 return _RectOfY(_YOfIndex(index)); 601 } 602 603 604 /*! Provides the top coordinate (offset from the top of view) of the package 605 supplied. If the package does not exist in the view then the coordinate 606 returned will be B_SIZE_UNSET. 607 */ 608 609 float TopOfPackage(const PackageInfoRef& package) 610 { 611 if (package.IsSet()) { 612 int index = _IndexOfPackage(package); 613 if (-1 != index) 614 return _YOfIndex(index); 615 } 616 return B_SIZE_UNSET; 617 } 618 619 620 BRect _RectOfY(float y) const 621 { 622 return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE); 623 } 624 625 626 float _YOfIndex(int32 i) const 627 { 628 return i * HEIGHT_PACKAGE; 629 } 630 631 632 /*! Finds the offset into the list of packages for the y-coord in the view's 633 coordinate space. If the y is above or below the list of packages then 634 this will return -1 to signal this. 635 */ 636 637 int32 _IndexOfY(float y) const 638 { 639 if (fPackages.empty()) 640 return -1; 641 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 642 if (i < 0 || i >= static_cast<int32>(fPackages.size())) 643 return -1; 644 return i; 645 } 646 647 648 /*! Find the offset into the list of packages for the y-coord in the view's 649 coordinate space. If the y is above or below the list of packages then 650 this will return the first or last package index respectively. If there 651 are no packages then this will return -1; 652 */ 653 654 int32 _IndexRoundedOfY(float y) const 655 { 656 if (fPackages.empty()) 657 return -1; 658 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 659 if (i < 0) 660 return 0; 661 return std::min(i, (int32) (fPackages.size() - 1)); 662 } 663 664 665 virtual BSize PreferredSize() 666 { 667 return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size()); 668 } 669 670 671 private: 672 Model& fModel; 673 std::vector<PackageInfoRef> 674 fPackages; 675 int32 fSelectedIndex; 676 OnePackageMessagePackageListener* 677 fPackageListener; 678 int32 fLowestIndexAddedOrRemoved; 679 }; 680 681 682 // #pragma mark - FeaturedPackagesView 683 684 685 FeaturedPackagesView::FeaturedPackagesView(Model& model) 686 : 687 BView(B_TRANSLATE("Featured packages"), 0), 688 fModel(model) 689 { 690 fPackagesView = new StackedFeaturedPackagesView(fModel); 691 692 fScrollView = new BScrollView("featured packages scroll view", 693 fPackagesView, 0, false, true, B_FANCY_BORDER); 694 695 BLayoutBuilder::Group<>(this) 696 .Add(fScrollView, 1.0f); 697 } 698 699 700 FeaturedPackagesView::~FeaturedPackagesView() 701 { 702 } 703 704 705 void 706 FeaturedPackagesView::BeginAddRemove() 707 { 708 fPackagesView->BeginAddRemove(); 709 } 710 711 712 void 713 FeaturedPackagesView::EndAddRemove() 714 { 715 fPackagesView->EndAddRemove(); 716 _AdjustViews(); 717 } 718 719 720 /*! This method will add the package into the list to be displayed. The 721 insertion will occur in alphabetical order. 722 */ 723 724 void 725 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 726 { 727 fPackagesView->AddPackage(package); 728 } 729 730 731 void 732 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 733 { 734 fPackagesView->RemovePackage(package); 735 } 736 737 738 void 739 FeaturedPackagesView::Clear() 740 { 741 HDINFO("did clear the featured packages view"); 742 fPackagesView->Clear(); 743 _AdjustViews(); 744 } 745 746 747 void 748 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 749 bool scrollToEntry) 750 { 751 fPackagesView->SelectPackage(package); 752 753 if (scrollToEntry) { 754 float offset = fPackagesView->TopOfPackage(package); 755 if (offset != B_SIZE_UNSET) 756 fPackagesView->ScrollTo(0, offset); 757 } 758 } 759 760 761 void 762 FeaturedPackagesView::DoLayout() 763 { 764 BView::DoLayout(); 765 _AdjustViews(); 766 } 767 768 769 void 770 FeaturedPackagesView::_AdjustViews() 771 { 772 fScrollView->FrameResized(fScrollView->Frame().Width(), 773 fScrollView->Frame().Height()); 774 } 775 776 777 void 778 FeaturedPackagesView::CleanupIcons() 779 { 780 sInstalledIcon.Unset(); 781 } 782