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