xref: /haiku/src/apps/haikudepot/ui/FeaturedPackagesView.cpp (revision 151de9ff9b93267bfaf53123004c013fef3e79eb)
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(Model& model)
59 		:
60 		BView("stacked featured packages view", B_WILL_DRAW | B_FRAME_EVENTS),
61 		fModel(model),
62 		fSelectedIndex(-1),
63 		fPackageListener(
64 			new(std::nothrow) OnePackageMessagePackageListener(this))
65 	{
66 		SetEventMask(B_POINTER_EVENTS);
67 		Clear();
68 	}
69 
70 
71 	virtual ~StackedFeaturedPackagesView()
72 	{
73 		fPackageListener->SetPackage(PackageInfoRef(NULL));
74 		fPackageListener->ReleaseReference();
75 	}
76 
77 // #pragma mark - message handling and events
78 
79 	virtual void MessageReceived(BMessage* message)
80 	{
81 		switch (message->what) {
82 			case MSG_UPDATE_PACKAGE:
83 			{
84 				BString name;
85 				if (message->FindString("name", &name) != B_OK)
86 					HDINFO("expected 'name' key on package update message");
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 	static int _CmpProminences(int64 a, int64 b)
228 	{
229 		if (a <= 0)
230 			a = PROMINANCE_ORDERING_MAX;
231 		if (b <= 0)
232 			b = PROMINANCE_ORDERING_MAX;
233 		if (a == b)
234 			return 0;
235 		if (a > b)
236 			return 1;
237 		return -1;
238 	}
239 
240 
241 	/*! This method will return true if the packageA is ordered before
242 		packageB.
243 	*/
244 
245 	static bool _IsPackageBefore(const PackageInfoRef& packageA,
246 		const PackageInfoRef& packageB)
247 	{
248 		if (packageA.Get() == NULL || packageB.Get() == NULL)
249 			HDFATAL("unexpected NULL reference in a referencable");
250 		int c = _CmpProminences(packageA->Prominence(), packageB->Prominence());
251 		if (c == 0)
252 			c = packageA->Title().ICompare(packageB->Title());
253 		if (c == 0)
254 			c = packageA->Name().Compare(packageB->Name());
255 		return c < 0;
256 	}
257 
258 
259 	void AddPackage(const PackageInfoRef& package)
260 	{
261 		// fPackages is sorted and for this reason it is possible to find the
262 		// insertion point by identifying the first item in fPackages that does
263 		// not return true from the method '_IsPackageBefore'.
264 
265 		std::vector<PackageInfoRef>::iterator itInsertionPt
266 			= std::lower_bound(fPackages.begin(), fPackages.end(), package,
267 				&_IsPackageBefore);
268 
269 		if (itInsertionPt == fPackages.end()
270 				|| package->Name() != (*itInsertionPt)->Name()) {
271 			int32 insertionIndex =
272 				std::distance<std::vector<PackageInfoRef>::const_iterator>(
273 					fPackages.begin(), itInsertionPt);
274 			if (fSelectedIndex >= insertionIndex)
275 				fSelectedIndex++;
276 			fPackages.insert(itInsertionPt, package);
277 			Invalidate(_RectOfIndex(insertionIndex)
278 				| _RectOfIndex(fPackages.size() - 1));
279 			package->AddListener(fPackageListener);
280 		}
281 	}
282 
283 
284 	void RemovePackage(const PackageInfoRef& package)
285 	{
286 		int32 index = _IndexOfPackage(package);
287 		if (index >= 0) {
288 			if (fSelectedIndex == index)
289 				fSelectedIndex = -1;
290 			if (fSelectedIndex > index)
291 				fSelectedIndex--;
292 			fPackages[index]->RemoveListener(fPackageListener);
293 			fPackages.erase(fPackages.begin() + index);
294 			if (fPackages.empty())
295 				Invalidate();
296 			else {
297 				Invalidate(_RectOfIndex(index)
298 					| _RectOfIndex(fPackages.size() - 1));
299 			}
300 		}
301 	}
302 
303 
304 // #pragma mark - selection and index handling
305 
306 
307 	void SelectPackage(const PackageInfoRef& package)
308 	{
309 		_SelectIndex(_IndexOfPackage(package));
310 	}
311 
312 
313 	void _SelectIndex(int32 index)
314 	{
315 		if (index != fSelectedIndex) {
316 			int32 previousSelectedIndex = fSelectedIndex;
317 			fSelectedIndex = index;
318 			if (fSelectedIndex >= 0)
319 				Invalidate(_RectOfIndex(fSelectedIndex));
320 			if (previousSelectedIndex >= 0)
321 				Invalidate(_RectOfIndex(previousSelectedIndex));
322 			_EnsureIndexVisible(index);
323 		}
324 	}
325 
326 
327 	int32 _IndexOfPackage(PackageInfoRef package) const
328 	{
329 		std::vector<PackageInfoRef>::const_iterator it
330 			= std::lower_bound(fPackages.begin(), fPackages.end(), package,
331 				&_IsPackageBefore);
332 
333 		return (it == fPackages.end() || (*it)->Name() != package->Name())
334 			? -1 : it - fPackages.begin();
335 	}
336 
337 
338 	int32 _IndexOfName(const BString& name) const
339 	{
340 		// TODO; slow linear search.
341 		// the fPackages is not sorted on name and for this reason it is not
342 		// possible to do a binary search.
343 		for (uint32 i = 0; i < fPackages.size(); i++) {
344 			if (fPackages[i]->Name() == name)
345 				return i;
346 		}
347 		return -1;
348 	}
349 
350 
351 // #pragma mark - drawing and rendering
352 
353 
354 	virtual void Draw(BRect updateRect)
355 	{
356 		SetHighUIColor(B_LIST_BACKGROUND_COLOR);
357 		FillRect(updateRect);
358 
359 		int32 iStart = _IndexRoundedOfY(updateRect.top);
360 
361 		if (iStart != -1) {
362 			int32 iEnd = _IndexRoundedOfY(updateRect.bottom);
363 			for (int32 i = iStart; i <= iEnd; i++)
364 				_DrawPackageAtIndex(updateRect, i);
365 		}
366 	}
367 
368 
369 	void _DrawPackageAtIndex(BRect updateRect, int32 index)
370 	{
371 		_DrawPackage(updateRect, fPackages[index], index, _YOfIndex(index),
372 			index == fSelectedIndex);
373 	}
374 
375 
376 	void _DrawPackage(BRect updateRect, PackageInfoRef pkg, int index, float y,
377 		bool selected)
378 	{
379 		if (selected) {
380 			SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR);
381 			FillRect(_RectOfY(y), B_SOLID_LOW);
382 		} else {
383 			SetLowUIColor(B_LIST_BACKGROUND_COLOR);
384 		}
385 		// TODO; optimization; the updateRect may only cover some of this?
386 		_DrawPackageIcon(updateRect, pkg, y, selected);
387 		_DrawPackageTitle(updateRect, pkg, y, selected);
388 		_DrawPackagePublisher(updateRect, pkg, y, selected);
389 		_DrawPackageRating(updateRect, pkg, y, selected);
390 		_DrawPackageSummary(updateRect, pkg, y, selected);
391 	}
392 
393 
394 	void _DrawPackageIcon(BRect updateRect, PackageInfoRef pkg, float y,
395 		bool selected)
396 	{
397 		BitmapRef icon;
398 		status_t iconResult = fModel.GetPackageIconRepository().GetIcon(
399 			pkg->Name(), BITMAP_SIZE_64, icon);
400 
401 		if (iconResult == B_OK) {
402 			if (icon.Get() != NULL) {
403 				float inset = (HEIGHT_PACKAGE - SIZE_ICON) / 2.0;
404 				BRect targetRect = BRect(inset, y + inset, SIZE_ICON + inset,
405 					y + SIZE_ICON + inset);
406 				const BBitmap* bitmap = icon->Bitmap(BITMAP_SIZE_64);
407 				SetDrawingMode(B_OP_ALPHA);
408 				DrawBitmap(bitmap, bitmap->Bounds(), targetRect,
409 					B_FILTER_BITMAP_BILINEAR);
410 			}
411 		}
412 	}
413 
414 
415 	void _DrawPackageTitle(BRect updateRect, PackageInfoRef pkg, float y,
416 		bool selected)
417 	{
418 		static BFont* sFont = NULL;
419 
420 		if (sFont == NULL) {
421 			sFont = new BFont(be_plain_font);
422 			GetFont(sFont);
423   			font_family family;
424 			font_style style;
425 			sFont->SetSize(ceilf(sFont->Size() * 1.8f));
426 			sFont->GetFamilyAndStyle(&family, &style);
427 			sFont->SetFamilyAndStyle(family, "Bold");
428 		}
429 
430 		SetDrawingMode(B_OP_COPY);
431 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR
432 			: B_LIST_ITEM_TEXT_COLOR);
433 		SetFont(sFont);
434 		BPoint pt(HEIGHT_PACKAGE, y + (HEIGHT_PACKAGE * Y_PROPORTION_TITLE));
435 		DrawString(pkg->Title(), pt);
436 
437 		if (pkg->State() == ACTIVATED) {
438 			const BBitmap* bitmap = sInstalledIcon->Bitmap(
439 				BITMAP_SIZE_16);
440 			float stringWidth = StringWidth(pkg->Title());
441 			float offsetX = pt.x + stringWidth + PADDING;
442 			BRect targetRect(offsetX, pt.y - 16, offsetX + 16, pt.y);
443 			SetDrawingMode(B_OP_ALPHA);
444 			DrawBitmap(bitmap, bitmap->Bounds(), targetRect,
445 				B_FILTER_BITMAP_BILINEAR);
446 		}
447 	}
448 
449 
450 	void _DrawPackagePublisher(BRect updateRect, PackageInfoRef pkg, float y,
451 		bool selected)
452 	{
453 		static BFont* sFont = NULL;
454 
455 		if (sFont == NULL) {
456 			sFont = new BFont(be_plain_font);
457 			font_family family;
458 			font_style style;
459 			sFont->SetSize(std::max(9.0f, floorf(sFont->Size() * 0.92f)));
460 			sFont->GetFamilyAndStyle(&family, &style);
461 			sFont->SetFamilyAndStyle(family, "Italic");
462 		}
463 
464 		SetDrawingMode(B_OP_COPY);
465 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR
466 			: B_LIST_ITEM_TEXT_COLOR);
467 		SetFont(sFont);
468 
469 		float maxTextWidth = (X_POSITION_RATING - HEIGHT_PACKAGE) - PADDING;
470 		BString publisherName(pkg->Publisher().Name());
471 		TruncateString(&publisherName, B_TRUNCATE_END, maxTextWidth);
472 
473 		DrawString(publisherName, BPoint(HEIGHT_PACKAGE,
474 			y + (HEIGHT_PACKAGE * Y_PROPORTION_PUBLISHER)));
475 	}
476 
477 
478 	// TODO; show the sample size
479 	void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y,
480 		bool selected)
481 	{
482 		BPoint at(X_POSITION_RATING,
483 			y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f);
484 		RatingUtils::Draw(this, at,
485 			pkg->CalculateRatingSummary().averageRating);
486 	}
487 
488 
489 	// TODO; handle multi-line rendering of the text
490 	void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y,
491 		bool selected)
492 	{
493 		BRect bounds = Bounds();
494 
495 		SetDrawingMode(B_OP_COPY);
496 		SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR
497 			: B_LIST_ITEM_TEXT_COLOR);
498 		SetFont(be_plain_font);
499 
500 		float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING;
501 		BString summary(pkg->ShortDescription());
502 		TruncateString(&summary, B_TRUNCATE_END, maxTextWidth);
503 
504 		DrawString(summary, BPoint(X_POSITION_SUMMARY,
505 			y + (HEIGHT_PACKAGE * 0.5)));
506 	}
507 
508 
509 // #pragma mark - geometry and scrolling
510 
511 
512 	/*!	This method will make sure that the package at the given index is
513 		visible.  If the whole of the package can be seen already then it will
514 		do nothing.  If the package is located above the visible region then it
515 		will scroll up to it.  If the package is located below the visible
516 		region then it will scroll down to it.
517 	*/
518 
519 	void _EnsureIndexVisible(int32 index)
520 	{
521 		if (!_IsIndexEntirelyVisible(index)) {
522 			BRect bounds = Bounds();
523 			int32 indexOfCentreVisible = _IndexOfY(
524 				bounds.top + bounds.Height() / 2);
525 			if (index < indexOfCentreVisible)
526 				ScrollTo(0, _YOfIndex(index));
527 			else {
528 				float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE)
529 					- bounds.Height();
530 				ScrollTo(0, scrollPointY);
531 			}
532 		}
533 	}
534 
535 
536 	/*!	This method will return true if the package at the supplied index is
537 		entirely visible.
538 	*/
539 
540 	bool _IsIndexEntirelyVisible(int32 index)
541 	{
542 		BRect bounds = Bounds();
543 		return bounds == (bounds | _RectOfIndex(index));
544 	}
545 
546 
547 	BRect _RectOfIndex(int32 index) const
548 	{
549 		if (index < 0)
550 			return BRect(0, 0, 0, 0);
551 		return _RectOfY(_YOfIndex(index));
552 	}
553 
554 
555 	/*!	Provides the top coordinate (offset from the top of view) of the package
556 		supplied.  If the package does not exist in the view then the coordinate
557 		returned will be B_SIZE_UNSET.
558 	*/
559 
560 	float TopOfPackage(const PackageInfoRef& package)
561 	{
562 		if (package.Get() != NULL) {
563 			int index = _IndexOfPackage(package);
564 			if (-1 != index)
565 				return _YOfIndex(index);
566 		}
567 		return B_SIZE_UNSET;
568 	}
569 
570 
571 	BRect _RectOfY(float y) const
572 	{
573 		return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE);
574 	}
575 
576 
577 	float _YOfIndex(int32 i) const
578 	{
579 		return i * HEIGHT_PACKAGE;
580 	}
581 
582 
583 	/*! Finds the offset into the list of packages for the y-coord in the view's
584 		coordinate space.  If the y is above or below the list of packages then
585 		this will return -1 to signal this.
586 	*/
587 
588 	int32 _IndexOfY(float y) const
589 	{
590 		if (fPackages.empty())
591 			return -1;
592 		int32 i = y / HEIGHT_PACKAGE;
593 		if (i < 0 || i >= fPackages.size())
594 			return -1;
595 		return i;
596 	}
597 
598 
599 	/*! Find the offset into the list of packages for the y-coord in the view's
600 		coordinate space.  If the y is above or below the list of packages then
601 		this will return the first or last package index respectively.  If there
602 		are no packages then this will return -1;
603 	*/
604 
605 	int32 _IndexRoundedOfY(float y) const
606 	{
607 		if (fPackages.empty())
608 			return -1;
609 		int32 i = y / HEIGHT_PACKAGE;
610 		if (i < 0)
611 			return 0;
612 		return std::min(i, (int32) (fPackages.size() - 1));
613 	}
614 
615 
616 	virtual BSize PreferredSize()
617 	{
618 		return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size());
619 	}
620 
621 
622 private:
623 			Model&				fModel;
624 			std::vector<PackageInfoRef>
625 								fPackages;
626 			int32				fSelectedIndex;
627 			OnePackageMessagePackageListener*
628 								fPackageListener;
629 };
630 
631 
632 // #pragma mark - FeaturedPackagesView
633 
634 
635 FeaturedPackagesView::FeaturedPackagesView(Model& model)
636 	:
637 	BView(B_TRANSLATE("Featured packages"), 0),
638 	fModel(model)
639 {
640 	fPackagesView = new StackedFeaturedPackagesView(fModel);
641 
642 	fScrollView = new BScrollView("featured packages scroll view",
643 		fPackagesView, 0, false, true, B_FANCY_BORDER);
644 
645 	BLayoutBuilder::Group<>(this)
646 		.Add(fScrollView, 1.0f);
647 }
648 
649 
650 FeaturedPackagesView::~FeaturedPackagesView()
651 {
652 }
653 
654 
655 /*! This method will add the package into the list to be displayed.  The
656     insertion will occur in alphabetical order.
657 */
658 
659 void
660 FeaturedPackagesView::AddPackage(const PackageInfoRef& package)
661 {
662 	fPackagesView->AddPackage(package);
663 	_AdjustViews();
664 }
665 
666 
667 void
668 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package)
669 {
670 	fPackagesView->RemovePackage(package);
671 	_AdjustViews();
672 }
673 
674 
675 void
676 FeaturedPackagesView::Clear()
677 {
678 	HDINFO("did clear the featured packages view");
679 	fPackagesView->Clear();
680 	_AdjustViews();
681 }
682 
683 
684 void
685 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package,
686 	bool scrollToEntry)
687 {
688 	fPackagesView->SelectPackage(package);
689 
690 	if (scrollToEntry) {
691 		float offset = fPackagesView->TopOfPackage(package);
692 		if (offset != B_SIZE_UNSET)
693 			fPackagesView->ScrollTo(0, offset);
694 	}
695 }
696 
697 
698 void
699 FeaturedPackagesView::DoLayout()
700 {
701 	BView::DoLayout();
702 	_AdjustViews();
703 }
704 
705 
706 void
707 FeaturedPackagesView::_AdjustViews()
708 {
709 	fScrollView->FrameResized(fScrollView->Frame().Width(),
710 		fScrollView->Frame().Height());
711 }
712 
713 
714 void
715 FeaturedPackagesView::CleanupIcons()
716 {
717 	sInstalledIcon.Unset();
718 }
719