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