xref: /haiku/src/apps/haikudepot/ui/FeaturedPackagesView.cpp (revision 909af08f4328301fbdef1ffb41f566c3b5bec0c7)
1 /*
2  * Copyright 2013-214, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>.
4  * Copyright 2020-2024, 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 <ControlLook.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 
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 "SharedIcons.h"
33 
34 
35 #undef B_TRANSLATION_CONTEXT
36 #define B_TRANSLATION_CONTEXT "FeaturedPackagesView"
37 
38 #define SIZE_ICON 64.0f
39 
40 // If the space for the summary has less than this many "M" characters then the summary will not be
41 // displayed.
42 #define MINIMUM_M_COUNT_SUMMARY 10.0f
43 
44 // The title area will be this many times the width of an "M".
45 #define M_COUNT_TITLE 10
46 
47 
48 // #pragma mark - PackageView
49 
50 
51 class StackedFeaturesPackageBandMetrics
52 {
53 public:
54 	StackedFeaturesPackageBandMetrics(float width, BFont* titleFont, BFont* metadataFont)
55 	{
56 		float padding = be_control_look->DefaultItemSpacing();
57 		BSize iconSize = BControlLook::ComposeIconSize(SIZE_ICON);
58 
59 		font_height titleFontHeight;
60 		font_height metadataFontHeight;
61 		titleFont->GetHeight(&titleFontHeight);
62 		metadataFont->GetHeight(&metadataFontHeight);
63 
64 		float totalTitleAndMetadataHeight = titleFontHeight.ascent + titleFontHeight.descent
65 			+ titleFontHeight.leading
66 			+ metadataFontHeight.leading
67 			+ 2.0 * (metadataFontHeight.ascent + metadataFontHeight.descent);
68 
69 		fHeight = fmaxf(totalTitleAndMetadataHeight, iconSize.Height()) + 2.0 * padding;
70 
71 		{
72 			float iconInset = (fHeight - iconSize.Width()) / 2.0;
73 			fIconRect = BRect(padding, iconInset, iconSize.Width() + padding,
74 				iconSize.Height() + iconInset);
75 		}
76 
77 		{
78 			float titleWidthM = titleFont->StringWidth("M");
79 
80 			float leftTitlePublisherAndChronologicalInfo = fIconRect.right + padding;
81 			float rightTitlePublisherAndChronologicalInfo = fminf(width, fIconRect.Size().Width()
82 				+ (2.0 * padding) + (titleWidthM * M_COUNT_TITLE));
83 
84 // left, top, right bottom
85 			fTitleRect = BRect(leftTitlePublisherAndChronologicalInfo,
86 				(fHeight - totalTitleAndMetadataHeight) / 2.0,
87 				rightTitlePublisherAndChronologicalInfo,
88 				((fHeight - totalTitleAndMetadataHeight) / 2.0)
89 					+ titleFontHeight.ascent + titleFontHeight.descent);
90 
91 			fPublisherRect = BRect(leftTitlePublisherAndChronologicalInfo,
92 				fTitleRect.bottom + titleFontHeight.leading,
93 				rightTitlePublisherAndChronologicalInfo,
94 				fTitleRect.bottom + titleFontHeight.leading
95 					+ metadataFontHeight.ascent + metadataFontHeight.descent);
96 
97 			fChronologicalInfoRect = BRect(leftTitlePublisherAndChronologicalInfo,
98 				fPublisherRect.bottom + metadataFontHeight.leading,
99 				rightTitlePublisherAndChronologicalInfo,
100 				fPublisherRect.bottom + metadataFontHeight.leading
101 					+ metadataFontHeight.ascent + metadataFontHeight.descent);
102         }
103 
104         // sort out the ratings display
105 
106 		{
107 			BSize ratingStarSize = SharedIcons::IconStarBlue16Scaled()->Bitmap()->Bounds().Size();
108 			RatingStarsMetrics ratingStarsMetrics(ratingStarSize);
109 
110 			fRatingStarsRect = BRect(BPoint(fTitleRect.right + padding,
111 				(fHeight - ratingStarsMetrics.Size().Height()) / 2), ratingStarsMetrics.Size());
112 
113 			if (fRatingStarsRect.right > width)
114 				fRatingStarsRect = BRect();
115 			else {
116 				// Now sort out the position for the summary. This is reckoned as a container
117 				// rect because it would be nice to layout the text with newlines and not just a
118 				// single line.
119 
120 				fSummaryContainerRect = BRect(fRatingStarsRect.right + (padding * 2.0), padding,
121 					width - padding, fHeight - (padding * 2.0));
122 
123 				float metadataWidthM = metadataFont->StringWidth("M");
124 
125 				if (fSummaryContainerRect.Size().Width() < MINIMUM_M_COUNT_SUMMARY * metadataWidthM)
126 					fSummaryContainerRect = BRect();
127 			}
128 		}
129 	}
130 
131 	float Height()
132 	{
133 		return fHeight;
134 	}
135 
136 	BRect IconRect()
137 	{
138 		return fIconRect;
139 	}
140 
141 	BRect TitleRect()
142 	{
143 		return fTitleRect;
144 	}
145 
146 	BRect PublisherRect()
147 	{
148 		return fPublisherRect;
149 	}
150 
151 	BRect ChronologicalInfoRect()
152 	{
153 		return fChronologicalInfoRect;
154 	}
155 
156 	BRect RatingStarsRect()
157 	{
158 		return fRatingStarsRect;
159 	}
160 
161 	BRect SummaryContainerRect()
162 	{
163 		return fSummaryContainerRect;
164 	}
165 
166 private:
167 			float 				fHeight;
168 
169 			BRect				fIconRect;
170 			BRect				fTitleRect;
171 			BRect				fPublisherRect;
172 			BRect				fChronologicalInfoRect;
173 
174 			BRect				fRatingStarsRect;
175 
176 			BRect				fSummaryContainerRect;
177 };
178 
179 
180 class StackedFeaturedPackagesView : public BView {
181 public:
182 	StackedFeaturedPackagesView(Model& model)
183 		:
184 		BView("stacked featured packages view", B_WILL_DRAW | B_FRAME_EVENTS),
185 		fModel(model),
186 		fSelectedIndex(-1),
187 		fPackageListener(
188 			new(std::nothrow) OnePackageMessagePackageListener(this)),
189 		fLowestIndexAddedOrRemoved(-1)
190 	{
191 		SetEventMask(B_POINTER_EVENTS);
192 
193 		fTitleFont = StackedFeaturedPackagesView::CreateTitleFont();
194 		fMetadataFont = StackedFeaturedPackagesView::CreateMetadataFont();
195 		fSummaryFont = StackedFeaturedPackagesView::CreateSummaryFont();
196 		fBandMetrics = CreateBandMetrics();
197 
198 		Clear();
199 	}
200 
201 
202 	virtual ~StackedFeaturedPackagesView()
203 	{
204 		fPackageListener->SetPackage(PackageInfoRef(NULL));
205 		fPackageListener->ReleaseReference();
206 		delete fBandMetrics;
207 		delete fTitleFont;
208 		delete fMetadataFont;
209 		delete fSummaryFont;
210 	}
211 
212 // #pragma mark - message handling and events
213 
214 	virtual void MessageReceived(BMessage* message)
215 	{
216 		switch (message->what) {
217 			case MSG_UPDATE_PACKAGE:
218 			{
219 				BString name;
220 				if (message->FindString("name", &name) != B_OK)
221 					HDINFO("expected 'name' key on package update message");
222 				else
223 					_HandleUpdatePackage(name);
224 				break;
225 			}
226 
227 			case B_COLORS_UPDATED:
228 			{
229 				Invalidate();
230 				break;
231 			}
232 
233 			default:
234 				BView::MessageReceived(message);
235 				break;
236 		}
237 	}
238 
239 
240 	virtual void MouseDown(BPoint where)
241 	{
242 		if (Window()->IsActive() && !IsHidden()) {
243 			BRect bounds = Bounds();
244 			BRect parentBounds = Parent()->Bounds();
245 			ConvertFromParent(&parentBounds);
246 			bounds = bounds & parentBounds;
247 			if (bounds.Contains(where)) {
248 				_MessageSelectIndex(_IndexOfY(where.y));
249 				MakeFocus();
250 			}
251 		}
252 	}
253 
254 
255 	virtual void KeyDown(const char* bytes, int32 numBytes)
256 	{
257 		char key = bytes[0];
258 
259 		switch (key) {
260 			case B_RIGHT_ARROW:
261 			case B_DOWN_ARROW:
262 			{
263 				int32 lastIndex = static_cast<int32>(fPackages.size()) - 1;
264 				if (!IsEmpty() && fSelectedIndex != -1
265 						&& fSelectedIndex < lastIndex) {
266 					_MessageSelectIndex(fSelectedIndex + 1);
267 				}
268 				break;
269 			}
270 			case B_LEFT_ARROW:
271 			case B_UP_ARROW:
272 				if (fSelectedIndex > 0)
273 					_MessageSelectIndex( fSelectedIndex - 1);
274 				break;
275 			case B_PAGE_UP:
276 			{
277 				BRect bounds = Bounds();
278 				ScrollTo(0, fmaxf(0, bounds.top - bounds.Height()));
279 				break;
280 			}
281 			case B_PAGE_DOWN:
282 			{
283 				BRect bounds = Bounds();
284 				float height = fPackages.size() * fBandMetrics->Height();
285 				float maxScrollY = height - bounds.Height();
286 				float pageDownScrollY = bounds.top + bounds.Height();
287 				ScrollTo(0, fminf(maxScrollY, pageDownScrollY));
288 				break;
289 			}
290 			default:
291 				BView::KeyDown(bytes, numBytes);
292 				break;
293 		}
294 	}
295 
296 
297 	/*!	This method will send a message to the Window so that it can signal
298 		back to this and other views that a package has been selected.  This
299 		method won't actually change the state of this view directly.
300 	*/
301 
302 	void _MessageSelectIndex(int32 index) const
303 	{
304 		if (index != -1) {
305 			BMessage message(MSG_PACKAGE_SELECTED);
306 			BString packageName = fPackages[index]->Name();
307 			message.AddString("name", packageName);
308 			Window()->PostMessage(&message);
309 		}
310 	}
311 
312 
313 	virtual void FrameResized(float width, float height)
314 	{
315 		BView::FrameResized(width, height);
316 
317 		delete fBandMetrics;
318 		fBandMetrics = CreateBandMetrics();
319 
320 		Invalidate();
321 	}
322 
323 
324 // #pragma mark - update / add / remove / clear data
325 
326 
327 	void UpdatePackage(uint32 changeMask, const PackageInfoRef& package)
328 	{
329 		// TODO; could optimize the invalidation?
330 		int32 index = _IndexOfPackage(package);
331 		if (index >= 0) {
332 			fPackages[index] = package;
333 			Invalidate(_RectOfIndex(index));
334 		}
335 	}
336 
337 
338 	void Clear()
339 	{
340 		for (std::vector<PackageInfoRef>::iterator it = fPackages.begin();
341 				it != fPackages.end(); it++) {
342 			(*it)->RemoveListener(fPackageListener);
343 		}
344 		fPackages.clear();
345 		fSelectedIndex = -1;
346 
347 		Invalidate();
348 	}
349 
350 
351 	static BFont* CreateTitleFont()
352 	{
353 		BFont* font = new BFont(be_plain_font);
354 		font_family family;
355 		font_style style;
356 		font->SetSize(ceilf(font->Size() * 1.8f));
357 		font->GetFamilyAndStyle(&family, &style);
358 		font->SetFamilyAndStyle(family, "Bold");
359 		return font;
360 	}
361 
362 
363     static BFont* CreateMetadataFont()
364     {
365 		BFont* font = new BFont(be_plain_font);
366 		font_family family;
367 		font_style style;
368 		font->GetFamilyAndStyle(&family, &style);
369 		font->SetFamilyAndStyle(family, "Italic");
370 		return font;
371 	}
372 
373 
374 	static BFont* CreateSummaryFont()
375 	{
376 		return new BFont(be_plain_font);
377 	}
378 
379 
380 	StackedFeaturesPackageBandMetrics* CreateBandMetrics()
381 	{
382 		return new StackedFeaturesPackageBandMetrics(Bounds().Width(), fTitleFont, fMetadataFont);
383 	}
384 
385 
386 	bool IsEmpty() const
387 	{
388 		return fPackages.size() == 0;
389 	}
390 
391 
392 	void _HandleUpdatePackage(const BString& name)
393 	{
394 		int32 index = _IndexOfName(name);
395 		if (index != -1)
396 			Invalidate(_RectOfIndex(index));
397 	}
398 
399 
400 	static int _CmpProminences(int64 a, int64 b)
401 	{
402 		if (a <= 0)
403 			a = PROMINANCE_ORDERING_MAX;
404 		if (b <= 0)
405 			b = PROMINANCE_ORDERING_MAX;
406 		if (a == b)
407 			return 0;
408 		if (a > b)
409 			return 1;
410 		return -1;
411 	}
412 
413 
414 	/*! This method will return true if the packageA is ordered before
415 		packageB.
416 	*/
417 
418 	static bool _IsPackageBefore(const PackageInfoRef& packageA,
419 		const PackageInfoRef& packageB)
420 	{
421 		if (!packageA.IsSet() || !packageB.IsSet())
422 			HDFATAL("unexpected NULL reference in a referencable");
423 		int c = _CmpProminences(packageA->Prominence(), packageB->Prominence());
424 		if (c == 0)
425 			c = packageA->Title().ICompare(packageB->Title());
426 		if (c == 0)
427 			c = packageA->Name().Compare(packageB->Name());
428 		return c < 0;
429 	}
430 
431 
432 	void BeginAddRemove()
433 	{
434 		fLowestIndexAddedOrRemoved = INT32_MAX;
435 	}
436 
437 
438 	void EndAddRemove()
439 	{
440 		if (fLowestIndexAddedOrRemoved < INT32_MAX) {
441 			if (fPackages.empty())
442 				Invalidate();
443 			else {
444 				BRect invalidRect = Bounds();
445 				invalidRect.top = _YOfIndex(fLowestIndexAddedOrRemoved);
446 				Invalidate(invalidRect);
447 			}
448 		}
449 	}
450 
451 
452 	void AddPackage(const PackageInfoRef& package)
453 	{
454 		// fPackages is sorted and for this reason it is possible to find the
455 		// insertion point by identifying the first item in fPackages that does
456 		// not return true from the method '_IsPackageBefore'.
457 
458 		std::vector<PackageInfoRef>::iterator itInsertionPt
459 			= std::lower_bound(fPackages.begin(), fPackages.end(), package,
460 				&_IsPackageBefore);
461 
462 		if (itInsertionPt == fPackages.end()
463 				|| package->Name() != (*itInsertionPt)->Name()) {
464 			int32 insertionIndex =
465 				std::distance<std::vector<PackageInfoRef>::const_iterator>(
466 					fPackages.begin(), itInsertionPt);
467 			if (fSelectedIndex >= insertionIndex)
468 				fSelectedIndex++;
469 			fPackages.insert(itInsertionPt, package);
470 			package->AddListener(fPackageListener);
471 			if (insertionIndex < fLowestIndexAddedOrRemoved)
472 				fLowestIndexAddedOrRemoved = insertionIndex;
473 		}
474 	}
475 
476 
477 	void RemovePackage(const PackageInfoRef& package)
478 	{
479 		int32 index = _IndexOfPackage(package);
480 		if (index >= 0) {
481 			if (fSelectedIndex == index)
482 				fSelectedIndex = -1;
483 			if (fSelectedIndex > index)
484 				fSelectedIndex--;
485 			fPackages[index]->RemoveListener(fPackageListener);
486 			fPackages.erase(fPackages.begin() + index);
487 			if (index < fLowestIndexAddedOrRemoved)
488 				fLowestIndexAddedOrRemoved = index;
489 		}
490 	}
491 
492 
493 // #pragma mark - selection and index handling
494 
495 
496 	void SelectPackage(const PackageInfoRef& package)
497 	{
498 		_SelectIndex(_IndexOfPackage(package));
499 	}
500 
501 
502 	void _SelectIndex(int32 index)
503 	{
504 		if (index != fSelectedIndex) {
505 			int32 previousSelectedIndex = fSelectedIndex;
506 			fSelectedIndex = index;
507 			if (fSelectedIndex >= 0)
508 				Invalidate(_RectOfIndex(fSelectedIndex));
509 			if (previousSelectedIndex >= 0)
510 				Invalidate(_RectOfIndex(previousSelectedIndex));
511 			_EnsureIndexVisible(index);
512 		}
513 	}
514 
515 
516 	int32 _IndexOfPackage(PackageInfoRef package) const
517 	{
518 		std::vector<PackageInfoRef>::const_iterator it
519 			= std::lower_bound(fPackages.begin(), fPackages.end(), package,
520 				&_IsPackageBefore);
521 
522 		return (it == fPackages.end() || (*it)->Name() != package->Name())
523 			? -1 : it - fPackages.begin();
524 	}
525 
526 
527 	int32 _IndexOfName(const BString& name) const
528 	{
529 		// TODO; slow linear search.
530 		// the fPackages is not sorted on name and for this reason it is not
531 		// possible to do a binary search.
532 		for (uint32 i = 0; i < fPackages.size(); i++) {
533 			if (fPackages[i]->Name() == name)
534 				return i;
535 		}
536 		return -1;
537 	}
538 
539 
540 // #pragma mark - drawing and rendering
541 
542 
543 	virtual void Draw(BRect updateRect)
544 	{
545 		SetHighUIColor(B_LIST_BACKGROUND_COLOR);
546 		FillRect(updateRect);
547 
548 		int32 iStart = _IndexRoundedOfY(updateRect.top);
549 
550 		if (iStart != -1) {
551 			int32 iEnd = _IndexRoundedOfY(updateRect.bottom);
552 			for (int32 i = iStart; i <= iEnd; i++)
553 				_DrawPackageAtIndex(updateRect, i);
554 		}
555 	}
556 
557 
558 	void _DrawPackageAtIndex(BRect updateRect, int32 index)
559 	{
560 		_DrawPackage(updateRect, fPackages[index], _YOfIndex(index), index == fSelectedIndex);
561 	}
562 
563 
564 	void _DrawPackage(BRect updateRect, PackageInfoRef pkg, float y, bool selected)
565 	{
566 		if (selected) {
567 			SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR);
568 			FillRect(_RectOfY(y), B_SOLID_LOW);
569 		} else {
570 			SetLowUIColor(B_LIST_BACKGROUND_COLOR);
571 		}
572 
573 		BRect iconRect = fBandMetrics->IconRect();
574 		BRect titleRect = fBandMetrics->TitleRect();
575 		BRect publisherRect = fBandMetrics->PublisherRect();
576 		BRect chronologicalInfoRect = fBandMetrics->ChronologicalInfoRect();
577 		BRect ratingStarsRect = fBandMetrics->RatingStarsRect();
578 		BRect summaryContainerRect = fBandMetrics->SummaryContainerRect();
579 
580 		iconRect.OffsetBy(0.0, y);
581 		titleRect.OffsetBy(0.0, y);
582 		publisherRect.OffsetBy(0.0, y);
583 		chronologicalInfoRect.OffsetBy(0.0, y);
584 		ratingStarsRect.OffsetBy(0.0, y);
585 		summaryContainerRect.OffsetBy(0.0, y);
586 
587 		// TODO; optimization; the updateRect may only cover some of this?
588 		_DrawPackageIcon(iconRect, pkg, selected);
589 		_DrawPackageTitle(titleRect, pkg, selected);
590 		_DrawPackagePublisher(publisherRect, pkg, selected);
591 		_DrawPackageChronologicalInfo(chronologicalInfoRect, pkg, selected);
592 		_DrawPackageRating(ratingStarsRect, pkg);
593 		_DrawPackageSummary(summaryContainerRect, pkg, selected);
594 	}
595 
596 
597 	void _DrawPackageIcon(BRect iconRect, PackageInfoRef pkg, bool selected)
598 	{
599 		if (!iconRect.IsValid())
600 			return;
601 
602 		BitmapHolderRef icon;
603 		status_t iconResult = fModel.GetPackageIconRepository().GetIcon(pkg->Name(),
604 			iconRect.Width(), icon);
605 
606 		if (iconResult == B_OK) {
607 			if (icon.IsSet()) {
608 				const BBitmap* bitmap = icon->Bitmap();
609 
610 				if (bitmap != NULL && bitmap->IsValid()) {
611 					SetDrawingMode(B_OP_ALPHA);
612 					DrawBitmap(bitmap, bitmap->Bounds(), iconRect, B_FILTER_BITMAP_BILINEAR);
613 				}
614 			}
615 		}
616 	}
617 
618 
619 	void _DrawPackageTitle(BRect textRect, PackageInfoRef pkg, bool selected)
620 	{
621 		if (!textRect.IsValid())
622 			return;
623 
624 		const BBitmap* installedIconBitmap = SharedIcons::IconInstalled16Scaled()->Bitmap();
625 
626 		SetDrawingMode(B_OP_COPY);
627 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR);
628 		SetFont(fTitleFont);
629 
630 		font_height fontHeight;
631 		fTitleFont->GetHeight(&fontHeight);
632 		BPoint pt = textRect.LeftTop() + BPoint(0.0, + fontHeight.ascent);
633 
634 		BString renderedText = pkg->Title();
635 		float installedIconAllowance = installedIconBitmap->Bounds().Width() * 1.5;
636 		TruncateString(&renderedText, B_TRUNCATE_END, textRect.Width() - installedIconAllowance);
637 
638 		DrawString(renderedText, pt);
639 
640 		if (pkg->State() == ACTIVATED) {
641 			float stringWidth = StringWidth(pkg->Title());
642 			BRect iconRect = BRect(
643 				BPoint(textRect.left + stringWidth + (installedIconBitmap->Bounds().Width() / 2.0),
644 				textRect.top + (textRect.Height() / 2.0)
645 					- (installedIconBitmap->Bounds().Height() / 2.0)),
646 				installedIconBitmap->Bounds().Size());
647 			SetDrawingMode(B_OP_ALPHA);
648 			DrawBitmap(installedIconBitmap, installedIconBitmap->Bounds(), iconRect,
649 				B_FILTER_BITMAP_BILINEAR);
650 		}
651 	}
652 
653 
654 	void _DrawPackageGenericTextSlug(BRect textRect, const BString& text, bool selected)
655 	{
656 		if (!textRect.IsValid())
657 			return;
658 
659 		SetDrawingMode(B_OP_COPY);
660 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR);
661 		SetFont(fMetadataFont);
662 
663 		font_height fontHeight;
664 		fMetadataFont->GetHeight(&fontHeight);
665 		BPoint pt = textRect.LeftTop() + BPoint(0.0, + fontHeight.ascent);
666 
667 		BString renderedText(text);
668 		TruncateString(&renderedText, B_TRUNCATE_END, textRect.Width());
669 
670 		DrawString(renderedText, pt);
671 	}
672 
673 
674 	void _DrawPackagePublisher(BRect textRect, PackageInfoRef pkg, bool selected)
675 	{
676 		_DrawPackageGenericTextSlug(textRect, pkg->Publisher().Name(), selected);
677 	}
678 
679 
680 	void _DrawPackageChronologicalInfo(BRect textRect, PackageInfoRef pkg, bool selected)
681 	{
682 		BString versionCreateTimestampPresentation
683 			= LocaleUtils::TimestampToDateString(pkg->VersionCreateTimestamp());
684 		_DrawPackageGenericTextSlug(textRect, versionCreateTimestampPresentation, selected);
685 	}
686 
687 
688 	// TODO; show the sample size
689 	void _DrawPackageRating(BRect ratingRect, PackageInfoRef pkg)
690 	{
691 		if (!ratingRect.IsValid())
692 			return;
693 		RatingUtils::Draw(this, ratingRect.LeftTop(), pkg->CalculateRatingSummary().averageRating);
694 	}
695 
696 
697 	// TODO; handle multi-line rendering of the text
698 	void _DrawPackageSummary(BRect textRect, PackageInfoRef pkg, bool selected)
699 	{
700 		if (!textRect.IsValid())
701 			return;
702 
703 		SetDrawingMode(B_OP_COPY);
704 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR : B_LIST_ITEM_TEXT_COLOR);
705 		SetFont(fSummaryFont);
706 
707 		font_height fontHeight;
708 		fSummaryFont->GetHeight(&fontHeight);
709 
710 		// The text rect is a container into which later text can be made to flow multi-line. For
711 		// now just draw one line of the summary.
712 
713 		BPoint pt = textRect.LeftTop() + BPoint(0.0,
714 			(textRect.Size().Height() / 2.0) - ((fontHeight.ascent + fontHeight.descent) / 2.0)
715 				+ fontHeight.ascent);
716 
717 		BString summary(pkg->ShortDescription());
718 		TruncateString(&summary, B_TRUNCATE_END, textRect.Width());
719 
720 		DrawString(summary, pt);
721 	}
722 
723 
724 // #pragma mark - geometry and scrolling
725 
726 
727 	/*!	This method will make sure that the package at the given index is
728 		visible.  If the whole of the package can be seen already then it will
729 		do nothing.  If the package is located above the visible region then it
730 		will scroll up to it.  If the package is located below the visible
731 		region then it will scroll down to it.
732 	*/
733 
734 	void _EnsureIndexVisible(int32 index)
735 	{
736 		if (!_IsIndexEntirelyVisible(index)) {
737 			BRect bounds = Bounds();
738 			int32 indexOfCentreVisible = _IndexOfY(bounds.top + bounds.Height() / 2);
739 			if (index < indexOfCentreVisible)
740 				ScrollTo(0, _YOfIndex(index));
741 			else {
742 				float scrollPointY = (_YOfIndex(index) + fBandMetrics->Height()) - bounds.Height();
743 				ScrollTo(0, scrollPointY);
744 			}
745 		}
746 	}
747 
748 
749 	/*!	This method will return true if the package at the supplied index is
750 		entirely visible.
751 	*/
752 
753 	bool _IsIndexEntirelyVisible(int32 index)
754 	{
755 		BRect bounds = Bounds();
756 		return bounds == (bounds | _RectOfIndex(index));
757 	}
758 
759 
760 	BRect _RectOfIndex(int32 index) const
761 	{
762 		if (index < 0)
763 			return BRect(0, 0, 0, 0);
764 		return _RectOfY(_YOfIndex(index));
765 	}
766 
767 
768 	/*!	Provides the top coordinate (offset from the top of view) of the package
769 		supplied.  If the package does not exist in the view then the coordinate
770 		returned will be B_SIZE_UNSET.
771 	*/
772 
773 	float TopOfPackage(const PackageInfoRef& package)
774 	{
775 		if (package.IsSet()) {
776 			int index = _IndexOfPackage(package);
777 			if (-1 != index)
778 				return _YOfIndex(index);
779 		}
780 		return B_SIZE_UNSET;
781 	}
782 
783 
784 	BRect _RectOfY(float y) const
785 	{
786 		return BRect(0, y, Bounds().Width(), y + fBandMetrics->Height());
787 	}
788 
789 
790 	float _YOfIndex(int32 i) const
791 	{
792 		return i * fBandMetrics->Height();
793 	}
794 
795 
796 	/*! Finds the offset into the list of packages for the y-coord in the view's
797 		coordinate space.  If the y is above or below the list of packages then
798 		this will return -1 to signal this.
799 	*/
800 
801 	int32 _IndexOfY(float y) const
802 	{
803 		if (fPackages.empty())
804 			return -1;
805 		int32 i = static_cast<int32>(y / fBandMetrics->Height());
806 		if (i < 0 || i >= static_cast<int32>(fPackages.size()))
807 			return -1;
808 		return i;
809 	}
810 
811 
812 	/*! Find the offset into the list of packages for the y-coord in the view's
813 		coordinate space.  If the y is above or below the list of packages then
814 		this will return the first or last package index respectively.  If there
815 		are no packages then this will return -1;
816 	*/
817 
818 	int32 _IndexRoundedOfY(float y) const
819 	{
820 		if (fPackages.empty())
821 			return -1;
822 		int32 i = static_cast<int32>(y / fBandMetrics->Height());
823 		if (i < 0)
824 			return 0;
825 		return std::min(i, (int32) (fPackages.size() - 1));
826 	}
827 
828 
829 	virtual BSize PreferredSize()
830 	{
831 		return BSize(B_SIZE_UNLIMITED,
832 			fBandMetrics->Height() * static_cast<float>(fPackages.size()));
833 	}
834 
835 
836 private:
837 			Model&				fModel;
838 			std::vector<PackageInfoRef>
839 								fPackages;
840 			int32				fSelectedIndex;
841 			OnePackageMessagePackageListener*
842 								fPackageListener;
843 			int32				fLowestIndexAddedOrRemoved;
844 			StackedFeaturesPackageBandMetrics*
845 								fBandMetrics;
846 
847 			BFont*				fTitleFont;
848 			BFont*				fMetadataFont;
849 			BFont*				fSummaryFont;
850 };
851 
852 
853 // #pragma mark - FeaturedPackagesView
854 
855 
856 FeaturedPackagesView::FeaturedPackagesView(Model& model)
857 	:
858 	BView(B_TRANSLATE("Featured packages"), 0),
859 	fModel(model)
860 {
861 	fPackagesView = new StackedFeaturedPackagesView(fModel);
862 
863 	fScrollView = new BScrollView("featured packages scroll view",
864 		fPackagesView, 0, false, true, B_FANCY_BORDER);
865 
866 	BLayoutBuilder::Group<>(this)
867 		.Add(fScrollView, 1.0f);
868 }
869 
870 
871 FeaturedPackagesView::~FeaturedPackagesView()
872 {
873 }
874 
875 
876 void
877 FeaturedPackagesView::BeginAddRemove()
878 {
879 	fPackagesView->BeginAddRemove();
880 }
881 
882 
883 void
884 FeaturedPackagesView::EndAddRemove()
885 {
886 	fPackagesView->EndAddRemove();
887 	_AdjustViews();
888 }
889 
890 
891 /*! This method will add the package into the list to be displayed.  The
892     insertion will occur in alphabetical order.
893 */
894 
895 void
896 FeaturedPackagesView::AddPackage(const PackageInfoRef& package)
897 {
898 	fPackagesView->AddPackage(package);
899 }
900 
901 
902 void
903 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package)
904 {
905 	fPackagesView->RemovePackage(package);
906 }
907 
908 
909 void
910 FeaturedPackagesView::Clear()
911 {
912 	HDINFO("did clear the featured packages view");
913 	fPackagesView->Clear();
914 	_AdjustViews();
915 }
916 
917 
918 void
919 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package,
920 	bool scrollToEntry)
921 {
922 	fPackagesView->SelectPackage(package);
923 
924 	if (scrollToEntry) {
925 		float offset = fPackagesView->TopOfPackage(package);
926 		if (offset != B_SIZE_UNSET)
927 			fPackagesView->ScrollTo(0, offset);
928 	}
929 }
930 
931 
932 void
933 FeaturedPackagesView::DoLayout()
934 {
935 	BView::DoLayout();
936 	_AdjustViews();
937 }
938 
939 
940 void
941 FeaturedPackagesView::_AdjustViews()
942 {
943 	fScrollView->FrameResized(fScrollView->Frame().Width(),
944 		fScrollView->Frame().Height());
945 }
946