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 SetDrawingMode(B_OP_ALPHA); 432 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 433 B_FILTER_BITMAP_BILINEAR); 434 } 435 } 436 } 437 438 439 void _DrawPackageTitle(BRect updateRect, PackageInfoRef pkg, float y, 440 bool selected) 441 { 442 static BFont* sFont = NULL; 443 444 if (sFont == NULL) { 445 sFont = new BFont(be_plain_font); 446 GetFont(sFont); 447 font_family family; 448 font_style style; 449 sFont->SetSize(ceilf(sFont->Size() * 1.8f)); 450 sFont->GetFamilyAndStyle(&family, &style); 451 sFont->SetFamilyAndStyle(family, "Bold"); 452 } 453 454 SetDrawingMode(B_OP_COPY); 455 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 456 : B_LIST_ITEM_TEXT_COLOR); 457 SetFont(sFont); 458 BPoint pt(HEIGHT_PACKAGE, y + (HEIGHT_PACKAGE * Y_PROPORTION_TITLE)); 459 DrawString(pkg->Title(), pt); 460 461 if (pkg->State() == ACTIVATED) { 462 const BBitmap* bitmap = sInstalledIcon->Bitmap( 463 BITMAP_SIZE_16); 464 float stringWidth = StringWidth(pkg->Title()); 465 float offsetX = pt.x + stringWidth + PADDING; 466 BRect targetRect(offsetX, pt.y - 16, offsetX + 16, pt.y); 467 SetDrawingMode(B_OP_ALPHA); 468 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 469 B_FILTER_BITMAP_BILINEAR); 470 } 471 } 472 473 474 void _DrawPackageGenericTextSlug(BRect updateRect, PackageInfoRef pkg, 475 const BString& text, float y, float yProportion, bool selected) 476 { 477 static BFont* sFont = NULL; 478 479 if (sFont == NULL) { 480 sFont = new BFont(be_plain_font); 481 font_family family; 482 font_style style; 483 sFont->SetSize(std::max(9.0f, floorf(sFont->Size() * 0.92f))); 484 sFont->GetFamilyAndStyle(&family, &style); 485 sFont->SetFamilyAndStyle(family, "Italic"); 486 } 487 488 SetDrawingMode(B_OP_COPY); 489 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 490 : B_LIST_ITEM_TEXT_COLOR); 491 SetFont(sFont); 492 493 float maxTextWidth = (X_POSITION_RATING - HEIGHT_PACKAGE) - PADDING; 494 BString renderedText(text); 495 TruncateString(&renderedText, B_TRUNCATE_END, maxTextWidth); 496 497 DrawString(renderedText, BPoint(HEIGHT_PACKAGE, 498 y + (HEIGHT_PACKAGE * yProportion))); 499 } 500 501 502 void _DrawPackagePublisher(BRect updateRect, PackageInfoRef pkg, float y, 503 bool selected) 504 { 505 _DrawPackageGenericTextSlug(updateRect, pkg, pkg->Publisher().Name(), y, 506 Y_PROPORTION_PUBLISHER, selected); 507 } 508 509 510 void _DrawPackageCronologicalInfo(BRect updateRect, PackageInfoRef pkg, 511 float y, bool selected) 512 { 513 BString versionCreateTimestampPresentation 514 = B_TRANSLATE("%VersionCreateDate%"); 515 versionCreateTimestampPresentation.ReplaceAll("%VersionCreateDate%", 516 LocaleUtils::TimestampToDateString( 517 pkg->VersionCreateTimestamp())); 518 _DrawPackageGenericTextSlug(updateRect, pkg, 519 versionCreateTimestampPresentation, y, 520 Y_PROPORTION_CHRONOLOGICAL_DATA, selected); 521 } 522 523 524 // TODO; show the sample size 525 void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y, 526 bool selected) 527 { 528 BPoint at(X_POSITION_RATING, 529 y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f); 530 RatingUtils::Draw(this, at, 531 pkg->CalculateRatingSummary().averageRating); 532 } 533 534 535 // TODO; handle multi-line rendering of the text 536 void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y, 537 bool selected) 538 { 539 BRect bounds = Bounds(); 540 541 SetDrawingMode(B_OP_COPY); 542 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 543 : B_LIST_ITEM_TEXT_COLOR); 544 SetFont(be_plain_font); 545 546 float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING; 547 BString summary(pkg->ShortDescription()); 548 TruncateString(&summary, B_TRUNCATE_END, maxTextWidth); 549 550 DrawString(summary, BPoint(X_POSITION_SUMMARY, 551 y + (HEIGHT_PACKAGE * 0.5))); 552 } 553 554 555 // #pragma mark - geometry and scrolling 556 557 558 /*! This method will make sure that the package at the given index is 559 visible. If the whole of the package can be seen already then it will 560 do nothing. If the package is located above the visible region then it 561 will scroll up to it. If the package is located below the visible 562 region then it will scroll down to it. 563 */ 564 565 void _EnsureIndexVisible(int32 index) 566 { 567 if (!_IsIndexEntirelyVisible(index)) { 568 BRect bounds = Bounds(); 569 int32 indexOfCentreVisible = _IndexOfY( 570 bounds.top + bounds.Height() / 2); 571 if (index < indexOfCentreVisible) 572 ScrollTo(0, _YOfIndex(index)); 573 else { 574 float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE) 575 - bounds.Height(); 576 ScrollTo(0, scrollPointY); 577 } 578 } 579 } 580 581 582 /*! This method will return true if the package at the supplied index is 583 entirely visible. 584 */ 585 586 bool _IsIndexEntirelyVisible(int32 index) 587 { 588 BRect bounds = Bounds(); 589 return bounds == (bounds | _RectOfIndex(index)); 590 } 591 592 593 BRect _RectOfIndex(int32 index) const 594 { 595 if (index < 0) 596 return BRect(0, 0, 0, 0); 597 return _RectOfY(_YOfIndex(index)); 598 } 599 600 601 /*! Provides the top coordinate (offset from the top of view) of the package 602 supplied. If the package does not exist in the view then the coordinate 603 returned will be B_SIZE_UNSET. 604 */ 605 606 float TopOfPackage(const PackageInfoRef& package) 607 { 608 if (package.IsSet()) { 609 int index = _IndexOfPackage(package); 610 if (-1 != index) 611 return _YOfIndex(index); 612 } 613 return B_SIZE_UNSET; 614 } 615 616 617 BRect _RectOfY(float y) const 618 { 619 return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE); 620 } 621 622 623 float _YOfIndex(int32 i) const 624 { 625 return i * HEIGHT_PACKAGE; 626 } 627 628 629 /*! Finds the offset into the list of packages for the y-coord in the view's 630 coordinate space. If the y is above or below the list of packages then 631 this will return -1 to signal this. 632 */ 633 634 int32 _IndexOfY(float y) const 635 { 636 if (fPackages.empty()) 637 return -1; 638 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 639 if (i < 0 || i >= static_cast<int32>(fPackages.size())) 640 return -1; 641 return i; 642 } 643 644 645 /*! Find the offset into the list of packages for the y-coord in the view's 646 coordinate space. If the y is above or below the list of packages then 647 this will return the first or last package index respectively. If there 648 are no packages then this will return -1; 649 */ 650 651 int32 _IndexRoundedOfY(float y) const 652 { 653 if (fPackages.empty()) 654 return -1; 655 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 656 if (i < 0) 657 return 0; 658 return std::min(i, (int32) (fPackages.size() - 1)); 659 } 660 661 662 virtual BSize PreferredSize() 663 { 664 return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size()); 665 } 666 667 668 private: 669 Model& fModel; 670 std::vector<PackageInfoRef> 671 fPackages; 672 int32 fSelectedIndex; 673 OnePackageMessagePackageListener* 674 fPackageListener; 675 int32 fLowestIndexAddedOrRemoved; 676 }; 677 678 679 // #pragma mark - FeaturedPackagesView 680 681 682 FeaturedPackagesView::FeaturedPackagesView(Model& model) 683 : 684 BView(B_TRANSLATE("Featured packages"), 0), 685 fModel(model) 686 { 687 fPackagesView = new StackedFeaturedPackagesView(fModel); 688 689 fScrollView = new BScrollView("featured packages scroll view", 690 fPackagesView, 0, false, true, B_FANCY_BORDER); 691 692 BLayoutBuilder::Group<>(this) 693 .Add(fScrollView, 1.0f); 694 } 695 696 697 FeaturedPackagesView::~FeaturedPackagesView() 698 { 699 } 700 701 702 void 703 FeaturedPackagesView::BeginAddRemove() 704 { 705 fPackagesView->BeginAddRemove(); 706 } 707 708 709 void 710 FeaturedPackagesView::EndAddRemove() 711 { 712 fPackagesView->EndAddRemove(); 713 _AdjustViews(); 714 } 715 716 717 /*! This method will add the package into the list to be displayed. The 718 insertion will occur in alphabetical order. 719 */ 720 721 void 722 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 723 { 724 fPackagesView->AddPackage(package); 725 } 726 727 728 void 729 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 730 { 731 fPackagesView->RemovePackage(package); 732 } 733 734 735 void 736 FeaturedPackagesView::Clear() 737 { 738 HDINFO("did clear the featured packages view"); 739 fPackagesView->Clear(); 740 _AdjustViews(); 741 } 742 743 744 void 745 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 746 bool scrollToEntry) 747 { 748 fPackagesView->SelectPackage(package); 749 750 if (scrollToEntry) { 751 float offset = fPackagesView->TopOfPackage(package); 752 if (offset != B_SIZE_UNSET) 753 fPackagesView->ScrollTo(0, offset); 754 } 755 } 756 757 758 void 759 FeaturedPackagesView::DoLayout() 760 { 761 BView::DoLayout(); 762 _AdjustViews(); 763 } 764 765 766 void 767 FeaturedPackagesView::_AdjustViews() 768 { 769 fScrollView->FrameResized(fScrollView->Frame().Width(), 770 fScrollView->Frame().Height()); 771 } 772 773 774 void 775 FeaturedPackagesView::CleanupIcons() 776 { 777 sInstalledIcon.Unset(); 778 } 779