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