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 = LocaleUtils::TimestampToDateString(pkg->VersionCreateTimestamp()); 515 _DrawPackageGenericTextSlug(updateRect, pkg, 516 versionCreateTimestampPresentation, y, 517 Y_PROPORTION_CHRONOLOGICAL_DATA, selected); 518 } 519 520 521 // TODO; show the sample size 522 void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y, 523 bool selected) 524 { 525 BPoint at(X_POSITION_RATING, 526 y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f); 527 RatingUtils::Draw(this, at, 528 pkg->CalculateRatingSummary().averageRating); 529 } 530 531 532 // TODO; handle multi-line rendering of the text 533 void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y, 534 bool selected) 535 { 536 BRect bounds = Bounds(); 537 538 SetDrawingMode(B_OP_COPY); 539 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 540 : B_LIST_ITEM_TEXT_COLOR); 541 SetFont(be_plain_font); 542 543 float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING; 544 BString summary(pkg->ShortDescription()); 545 TruncateString(&summary, B_TRUNCATE_END, maxTextWidth); 546 547 DrawString(summary, BPoint(X_POSITION_SUMMARY, 548 y + (HEIGHT_PACKAGE * 0.5))); 549 } 550 551 552 // #pragma mark - geometry and scrolling 553 554 555 /*! This method will make sure that the package at the given index is 556 visible. If the whole of the package can be seen already then it will 557 do nothing. If the package is located above the visible region then it 558 will scroll up to it. If the package is located below the visible 559 region then it will scroll down to it. 560 */ 561 562 void _EnsureIndexVisible(int32 index) 563 { 564 if (!_IsIndexEntirelyVisible(index)) { 565 BRect bounds = Bounds(); 566 int32 indexOfCentreVisible = _IndexOfY( 567 bounds.top + bounds.Height() / 2); 568 if (index < indexOfCentreVisible) 569 ScrollTo(0, _YOfIndex(index)); 570 else { 571 float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE) 572 - bounds.Height(); 573 ScrollTo(0, scrollPointY); 574 } 575 } 576 } 577 578 579 /*! This method will return true if the package at the supplied index is 580 entirely visible. 581 */ 582 583 bool _IsIndexEntirelyVisible(int32 index) 584 { 585 BRect bounds = Bounds(); 586 return bounds == (bounds | _RectOfIndex(index)); 587 } 588 589 590 BRect _RectOfIndex(int32 index) const 591 { 592 if (index < 0) 593 return BRect(0, 0, 0, 0); 594 return _RectOfY(_YOfIndex(index)); 595 } 596 597 598 /*! Provides the top coordinate (offset from the top of view) of the package 599 supplied. If the package does not exist in the view then the coordinate 600 returned will be B_SIZE_UNSET. 601 */ 602 603 float TopOfPackage(const PackageInfoRef& package) 604 { 605 if (package.IsSet()) { 606 int index = _IndexOfPackage(package); 607 if (-1 != index) 608 return _YOfIndex(index); 609 } 610 return B_SIZE_UNSET; 611 } 612 613 614 BRect _RectOfY(float y) const 615 { 616 return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE); 617 } 618 619 620 float _YOfIndex(int32 i) const 621 { 622 return i * HEIGHT_PACKAGE; 623 } 624 625 626 /*! Finds the offset into the list of packages for the y-coord in the view's 627 coordinate space. If the y is above or below the list of packages then 628 this will return -1 to signal this. 629 */ 630 631 int32 _IndexOfY(float y) const 632 { 633 if (fPackages.empty()) 634 return -1; 635 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 636 if (i < 0 || i >= static_cast<int32>(fPackages.size())) 637 return -1; 638 return i; 639 } 640 641 642 /*! Find the offset into the list of packages for the y-coord in the view's 643 coordinate space. If the y is above or below the list of packages then 644 this will return the first or last package index respectively. If there 645 are no packages then this will return -1; 646 */ 647 648 int32 _IndexRoundedOfY(float y) const 649 { 650 if (fPackages.empty()) 651 return -1; 652 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 653 if (i < 0) 654 return 0; 655 return std::min(i, (int32) (fPackages.size() - 1)); 656 } 657 658 659 virtual BSize PreferredSize() 660 { 661 return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size()); 662 } 663 664 665 private: 666 Model& fModel; 667 std::vector<PackageInfoRef> 668 fPackages; 669 int32 fSelectedIndex; 670 OnePackageMessagePackageListener* 671 fPackageListener; 672 int32 fLowestIndexAddedOrRemoved; 673 }; 674 675 676 // #pragma mark - FeaturedPackagesView 677 678 679 FeaturedPackagesView::FeaturedPackagesView(Model& model) 680 : 681 BView(B_TRANSLATE("Featured packages"), 0), 682 fModel(model) 683 { 684 fPackagesView = new StackedFeaturedPackagesView(fModel); 685 686 fScrollView = new BScrollView("featured packages scroll view", 687 fPackagesView, 0, false, true, B_FANCY_BORDER); 688 689 BLayoutBuilder::Group<>(this) 690 .Add(fScrollView, 1.0f); 691 } 692 693 694 FeaturedPackagesView::~FeaturedPackagesView() 695 { 696 } 697 698 699 void 700 FeaturedPackagesView::BeginAddRemove() 701 { 702 fPackagesView->BeginAddRemove(); 703 } 704 705 706 void 707 FeaturedPackagesView::EndAddRemove() 708 { 709 fPackagesView->EndAddRemove(); 710 _AdjustViews(); 711 } 712 713 714 /*! This method will add the package into the list to be displayed. The 715 insertion will occur in alphabetical order. 716 */ 717 718 void 719 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 720 { 721 fPackagesView->AddPackage(package); 722 } 723 724 725 void 726 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 727 { 728 fPackagesView->RemovePackage(package); 729 } 730 731 732 void 733 FeaturedPackagesView::Clear() 734 { 735 HDINFO("did clear the featured packages view"); 736 fPackagesView->Clear(); 737 _AdjustViews(); 738 } 739 740 741 void 742 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 743 bool scrollToEntry) 744 { 745 fPackagesView->SelectPackage(package); 746 747 if (scrollToEntry) { 748 float offset = fPackagesView->TopOfPackage(package); 749 if (offset != B_SIZE_UNSET) 750 fPackagesView->ScrollTo(0, offset); 751 } 752 } 753 754 755 void 756 FeaturedPackagesView::DoLayout() 757 { 758 BView::DoLayout(); 759 _AdjustViews(); 760 } 761 762 763 void 764 FeaturedPackagesView::_AdjustViews() 765 { 766 fScrollView->FrameResized(fScrollView->Frame().Width(), 767 fScrollView->Frame().Height()); 768 } 769 770 771 void 772 FeaturedPackagesView::CleanupIcons() 773 { 774 sInstalledIcon.Unset(); 775 } 776