xref: /haiku/src/apps/haikudepot/ui/PackageListView.cpp (revision 0e7fcd84af6c2bae5d5a741f3eb8f59813e0a6e0)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2013, Rene Gollent, <rene@gollent.com>.
4  * All rights reserved. Distributed under the terms of the MIT License.
5  */
6 
7 #include "PackageListView.h"
8 
9 #include <algorithm>
10 #include <stdio.h>
11 
12 #include <Autolock.h>
13 #include <Catalog.h>
14 #include <MessageFormat.h>
15 #include <ScrollBar.h>
16 #include <Window.h>
17 
18 
19 #undef B_TRANSLATION_CONTEXT
20 #define B_TRANSLATION_CONTEXT "PackageListView"
21 
22 
23 static const char* skPackageStateAvailable = B_TRANSLATE_MARK("Available");
24 static const char* skPackageStateUninstalled = B_TRANSLATE_MARK("Uninstalled");
25 static const char* skPackageStateActive = B_TRANSLATE_MARK("Active");
26 static const char* skPackageStateInactive = B_TRANSLATE_MARK("Inactive");
27 static const char* skPackageStatePending = B_TRANSLATE_MARK(
28 	"Pending" B_UTF8_ELLIPSIS);
29 
30 
31 inline BString
32 package_state_to_string(PackageInfoRef ref)
33 {
34 	switch (ref->State()) {
35 		case NONE:
36 			return B_TRANSLATE(skPackageStateAvailable);
37 		case INSTALLED:
38 			return B_TRANSLATE(skPackageStateInactive);
39 		case ACTIVATED:
40 			return B_TRANSLATE(skPackageStateActive);
41 		case UNINSTALLED:
42 			return B_TRANSLATE(skPackageStateUninstalled);
43 		case DOWNLOADING:
44 		{
45 			BString data;
46 			data.SetToFormat("%3.2f%%", ref->DownloadProgress() * 100.0);
47 			return data;
48 		}
49 		case PENDING:
50 			return B_TRANSLATE(skPackageStatePending);
51 	}
52 
53 	return B_TRANSLATE("Unknown");
54 }
55 
56 
57 // A field type displaying both a bitmap and a string so that the
58 // tree display looks nicer (both text and bitmap are indented)
59 // TODO: Code-duplication with DriveSetup PartitionList.h
60 class BBitmapStringField : public BStringField {
61 	typedef BStringField Inherited;
62 public:
63 								BBitmapStringField(const BBitmap* bitmap,
64 									const char* string);
65 	virtual						~BBitmapStringField();
66 
67 			void				SetBitmap(const BBitmap* bitmap);
68 			const BBitmap*		Bitmap() const
69 									{ return fBitmap; }
70 
71 private:
72 			const BBitmap*		fBitmap;
73 };
74 
75 
76 class RatingField : public BField {
77 public:
78 								RatingField(float rating);
79 	virtual						~RatingField();
80 
81 			void				SetRating(float rating);
82 			float				Rating() const
83 									{ return fRating; }
84 private:
85 			float				fRating;
86 };
87 
88 
89 // BColumn for PackageListView which knows how to render
90 // a BBitmapStringField
91 // TODO: Code-duplication with DriveSetup PartitionList.h
92 class PackageColumn : public BTitledColumn {
93 	typedef BTitledColumn Inherited;
94 public:
95 								PackageColumn(const char* title,
96 									float width, float minWidth,
97 									float maxWidth, uint32 truncateMode,
98 									alignment align = B_ALIGN_LEFT);
99 
100 	virtual	void				DrawField(BField* field, BRect rect,
101 									BView* parent);
102 	virtual	int					CompareFields(BField* field1, BField* field2);
103 	virtual float				GetPreferredWidth(BField* field,
104 									BView* parent) const;
105 
106 	virtual	bool				AcceptsField(const BField* field) const;
107 
108 	static	void				InitTextMargin(BView* parent);
109 
110 private:
111 			uint32				fTruncateMode;
112 	static	float				sTextMargin;
113 };
114 
115 
116 // BRow for the PartitionListView
117 class PackageRow : public BRow {
118 	typedef BRow Inherited;
119 public:
120 								PackageRow(const PackageInfoRef& package,
121 									PackageListener* listener);
122 	virtual						~PackageRow();
123 
124 			const PackageInfoRef& Package() const
125 									{ return fPackage; }
126 
127 			void				UpdateTitle();
128 			void				UpdateSummary();
129 			void				UpdateState();
130 			void				UpdateRating();
131 
132 private:
133 			PackageInfoRef		fPackage;
134 			PackageInfoListenerRef fPackageListener;
135 };
136 
137 
138 enum {
139 	MSG_UPDATE_PACKAGE		= 'updp'
140 };
141 
142 
143 class PackageListener : public PackageInfoListener {
144 public:
145 	PackageListener(PackageListView* view)
146 		:
147 		fView(view)
148 	{
149 	}
150 
151 	virtual ~PackageListener()
152 	{
153 	}
154 
155 	virtual void PackageChanged(const PackageInfoEvent& event)
156 	{
157 		BMessenger messenger(fView);
158 		if (!messenger.IsValid())
159 			return;
160 
161 		const PackageInfo& package = *event.Package().Get();
162 
163 		BMessage message(MSG_UPDATE_PACKAGE);
164 		message.AddString("title", package.Title());
165 		message.AddUInt32("changes", event.Changes());
166 
167 		messenger.SendMessage(&message);
168 	}
169 
170 private:
171 	PackageListView*	fView;
172 };
173 
174 
175 // #pragma mark - BBitmapStringField
176 
177 
178 // TODO: Code-duplication with DriveSetup PartitionList.cpp
179 BBitmapStringField::BBitmapStringField(const BBitmap* bitmap,
180 		const char* string)
181 	:
182 	Inherited(string),
183 	fBitmap(bitmap)
184 {
185 }
186 
187 
188 BBitmapStringField::~BBitmapStringField()
189 {
190 }
191 
192 
193 void
194 BBitmapStringField::SetBitmap(const BBitmap* bitmap)
195 {
196 	fBitmap = bitmap;
197 	// TODO: cause a redraw?
198 }
199 
200 
201 // #pragma mark - RatingField
202 
203 
204 RatingField::RatingField(float rating)
205 	:
206 	fRating(0.0f)
207 {
208 	SetRating(rating);
209 }
210 
211 
212 RatingField::~RatingField()
213 {
214 }
215 
216 
217 void
218 RatingField::SetRating(float rating)
219 {
220 	if (rating < 0.0f)
221 		rating = 0.0f;
222 	if (rating > 5.0f)
223 		rating = 5.0f;
224 
225 	if (rating == fRating)
226 		return;
227 
228 	fRating = rating;
229 }
230 
231 
232 // #pragma mark - PackageColumn
233 
234 
235 // TODO: Code-duplication with DriveSetup PartitionList.cpp
236 
237 
238 float PackageColumn::sTextMargin = 0.0;
239 
240 
241 PackageColumn::PackageColumn(const char* title, float width, float minWidth,
242 		float maxWidth, uint32 truncateMode, alignment align)
243 	:
244 	Inherited(title, width, minWidth, maxWidth, align),
245 	fTruncateMode(truncateMode)
246 {
247 	SetWantsEvents(true);
248 }
249 
250 
251 void
252 PackageColumn::DrawField(BField* field, BRect rect, BView* parent)
253 {
254 	BBitmapStringField* bitmapField
255 		= dynamic_cast<BBitmapStringField*>(field);
256 	BStringField* stringField = dynamic_cast<BStringField*>(field);
257 	RatingField* ratingField = dynamic_cast<RatingField*>(field);
258 
259 	if (bitmapField != NULL) {
260 		const BBitmap* bitmap = bitmapField->Bitmap();
261 
262 		// figure out the placement
263 		float x = 0.0;
264 		BRect r = bitmap ? bitmap->Bounds() : BRect(0, 0, 15, 15);
265 		float y = rect.top + ((rect.Height() - r.Height()) / 2);
266 		float width = 0.0;
267 
268 		switch (Alignment()) {
269 			default:
270 			case B_ALIGN_LEFT:
271 			case B_ALIGN_CENTER:
272 				x = rect.left + sTextMargin;
273 				width = rect.right - (x + r.Width()) - (2 * sTextMargin);
274 				r.Set(x + r.Width(), rect.top, rect.right - width, rect.bottom);
275 				break;
276 
277 			case B_ALIGN_RIGHT:
278 				x = rect.right - sTextMargin - r.Width();
279 				width = (x - rect.left - (2 * sTextMargin));
280 				r.Set(rect.left, rect.top, rect.left + width, rect.bottom);
281 				break;
282 		}
283 
284 		if (width != bitmapField->Width()) {
285 			BString truncatedString(bitmapField->String());
286 			parent->TruncateString(&truncatedString, fTruncateMode, width + 2);
287 			bitmapField->SetClippedString(truncatedString.String());
288 			bitmapField->SetWidth(width);
289 		}
290 
291 		// draw the bitmap
292 		if (bitmap != NULL) {
293 			parent->SetDrawingMode(B_OP_ALPHA);
294 			parent->DrawBitmap(bitmap, BPoint(x, y));
295 			parent->SetDrawingMode(B_OP_OVER);
296 		}
297 
298 		// draw the string
299 		DrawString(bitmapField->ClippedString(), parent, r);
300 
301 	} else if (stringField != NULL) {
302 
303 		float width = rect.Width() - (2 * sTextMargin);
304 
305 		if (width != stringField->Width()) {
306 			BString truncatedString(stringField->String());
307 
308 			parent->TruncateString(&truncatedString, fTruncateMode, width + 2);
309 			stringField->SetClippedString(truncatedString.String());
310 			stringField->SetWidth(width);
311 		}
312 
313 		DrawString(stringField->ClippedString(), parent, rect);
314 
315 	} else if (ratingField != NULL) {
316 
317 		const float kDefaultTextMargin = 8;
318 
319 		float width = rect.Width() - (2 * kDefaultTextMargin);
320 
321 		BString string = "★★★★★";
322 		float stringWidth = parent->StringWidth(string);
323 		bool drawOverlay = true;
324 
325 		if (width < stringWidth) {
326 			string.SetToFormat("%.1f", ratingField->Rating());
327 			drawOverlay = false;
328 			stringWidth = parent->StringWidth(string);
329 		}
330 
331 		switch (Alignment()) {
332 			default:
333 			case B_ALIGN_LEFT:
334 				rect.left += kDefaultTextMargin;
335 				break;
336 			case B_ALIGN_CENTER:
337 				rect.left = rect.left + (width - stringWidth) / 2.0f;
338 				break;
339 
340 			case B_ALIGN_RIGHT:
341 				rect.left = rect.right - (stringWidth + kDefaultTextMargin);
342 				break;
343 		}
344 
345 		rect.left = floorf(rect.left);
346 		rect.right = rect.left + stringWidth;
347 
348 		if (drawOverlay)
349 			parent->SetHighColor(0, 170, 255);
350 
351 		font_height	fontHeight;
352 		parent->GetFontHeight(&fontHeight);
353 		float y = rect.top + (rect.Height()
354 			- (fontHeight.ascent + fontHeight.descent)) / 2
355 			+ fontHeight.ascent;
356 
357 		parent->DrawString(string, BPoint(rect.left, y));
358 
359 		if (drawOverlay) {
360 			rect.left = ceilf(rect.left
361 				+ (ratingField->Rating() / 5.0f) * rect.Width());
362 
363 			rgb_color color = parent->LowColor();
364 			color.alpha = 190;
365 			parent->SetHighColor(color);
366 
367 			parent->SetDrawingMode(B_OP_ALPHA);
368 			parent->FillRect(rect, B_SOLID_HIGH);
369 
370 		}
371 	}
372 }
373 
374 
375 int
376 PackageColumn::CompareFields(BField* field1, BField* field2)
377 {
378 	BStringField* stringField1 = dynamic_cast<BStringField*>(field1);
379 	BStringField* stringField2 = dynamic_cast<BStringField*>(field2);
380 	if (stringField1 != NULL && stringField2 != NULL) {
381 		// TODO: Locale aware string compare... not too important if
382 		// package names are not translated.
383 		return strcmp(stringField1->String(), stringField2->String());
384 	}
385 
386 	RatingField* ratingField1 = dynamic_cast<RatingField*>(field1);
387 	RatingField* ratingField2 = dynamic_cast<RatingField*>(field2);
388 	if (ratingField1 != NULL && ratingField2 != NULL) {
389 		if (ratingField1->Rating() > ratingField2->Rating())
390 			return -1;
391 		else if (ratingField1->Rating() < ratingField2->Rating())
392 			return 1;
393 		return 0;
394 	}
395 
396 	return Inherited::CompareFields(field1, field2);
397 }
398 
399 
400 float
401 PackageColumn::GetPreferredWidth(BField *_field, BView* parent) const
402 {
403 	BBitmapStringField* bitmapField
404 		= dynamic_cast<BBitmapStringField*>(_field);
405 	BStringField* stringField = dynamic_cast<BStringField*>(_field);
406 
407 	float parentWidth = Inherited::GetPreferredWidth(_field, parent);
408 	float width = 0.0;
409 
410 	if (bitmapField) {
411 		const BBitmap* bitmap = bitmapField->Bitmap();
412 		BFont font;
413 		parent->GetFont(&font);
414 		width = font.StringWidth(bitmapField->String()) + 3 * sTextMargin;
415 		if (bitmap)
416 			width += bitmap->Bounds().Width();
417 		else
418 			width += 16;
419 	} else if (stringField) {
420 		BFont font;
421 		parent->GetFont(&font);
422 		width = font.StringWidth(stringField->String()) + 2 * sTextMargin;
423 	}
424 	return max_c(width, parentWidth);
425 }
426 
427 
428 bool
429 PackageColumn::AcceptsField(const BField* field) const
430 {
431 	return dynamic_cast<const BStringField*>(field) != NULL
432 		|| dynamic_cast<const RatingField*>(field) != NULL;
433 }
434 
435 
436 void
437 PackageColumn::InitTextMargin(BView* parent)
438 {
439 	BFont font;
440 	parent->GetFont(&font);
441 	sTextMargin = ceilf(font.Size() * 0.8);
442 }
443 
444 
445 // #pragma mark - PackageRow
446 
447 
448 enum {
449 	kTitleColumn,
450 	kRatingColumn,
451 	kDescriptionColumn,
452 	kSizeColumn,
453 	kStatusColumn,
454 };
455 
456 
457 PackageRow::PackageRow(const PackageInfoRef& packageRef,
458 		PackageListener* packageListener)
459 	:
460 	Inherited(ceilf(be_plain_font->Size() * 1.8f)),
461 	fPackage(packageRef),
462 	fPackageListener(packageListener)
463 {
464 	if (packageRef.Get() == NULL)
465 		return;
466 
467 	PackageInfo& package = *packageRef.Get();
468 
469 	// Package icon and title
470 	// NOTE: The icon BBitmap is referenced by the fPackage member.
471 	UpdateTitle();
472 
473 	// Rating
474 	UpdateRating();
475 
476 	// Summary
477 	UpdateSummary();
478 
479 	// Size
480 	// TODO: Store package size
481 	SetField(new BStringField("0 KiB"), kSizeColumn);
482 
483 	// Status
484 	SetField(new BStringField(package_state_to_string(fPackage)),
485 		kStatusColumn);
486 
487 	package.AddListener(fPackageListener);
488 }
489 
490 
491 PackageRow::~PackageRow()
492 {
493 	if (fPackage.Get() != NULL)
494 		fPackage->RemoveListener(fPackageListener);
495 }
496 
497 
498 void
499 PackageRow::UpdateTitle()
500 {
501 	if (fPackage.Get() == NULL)
502 		return;
503 
504 	const BBitmap* icon = NULL;
505 	if (fPackage->Icon().Get() != NULL)
506 		icon = fPackage->Icon()->Bitmap(SharedBitmap::SIZE_16);
507 	SetField(new BBitmapStringField(icon, fPackage->Title()), kTitleColumn);
508 }
509 
510 
511 void
512 PackageRow::UpdateState()
513 {
514 	if (fPackage.Get() == NULL)
515 		return;
516 
517 	SetField(new BStringField(package_state_to_string(fPackage)),
518 		kStatusColumn);
519 }
520 
521 
522 void
523 PackageRow::UpdateSummary()
524 {
525 	if (fPackage.Get() == NULL)
526 		return;
527 
528 	SetField(new BStringField(fPackage->ShortDescription()),
529 		kDescriptionColumn);
530 }
531 
532 
533 void
534 PackageRow::UpdateRating()
535 {
536 	if (fPackage.Get() == NULL)
537 		return;
538 	RatingSummary summary = fPackage->CalculateRatingSummary();
539 	SetField(new RatingField(summary.averageRating), kRatingColumn);
540 }
541 
542 
543 // #pragma mark - ItemCountView
544 
545 
546 class PackageListView::ItemCountView : public BView {
547 public:
548 	ItemCountView()
549 		:
550 		BView("item count view", B_WILL_DRAW),
551 		fItemCount(0)
552 	{
553 		BFont font(be_plain_font);
554 		font.SetSize(9.0f);
555 		SetFont(&font);
556 
557 		SetViewColor(B_TRANSPARENT_COLOR);
558 		SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
559 
560 		SetHighColor(tint_color(LowColor(), B_DARKEN_4_TINT));
561 	}
562 
563 	virtual BSize MinSize()
564 	{
565 		BString label(_GetLabel());
566 		return BSize(StringWidth(label) + 10, B_H_SCROLL_BAR_HEIGHT);
567 	}
568 
569 	virtual BSize PreferredSize()
570 	{
571 		return MinSize();
572 	}
573 
574 	virtual BSize MaxSize()
575 	{
576 		return MinSize();
577 	}
578 
579 	virtual void Draw(BRect updateRect)
580 	{
581 		FillRect(updateRect, B_SOLID_LOW);
582 
583 		BString label(_GetLabel());
584 
585 		font_height fontHeight;
586 		GetFontHeight(&fontHeight);
587 
588 		BRect bounds(Bounds());
589 		float width = StringWidth(label);
590 
591 		BPoint offset;
592 		offset.x = bounds.left + (bounds.Width() - width) / 2.0f;
593 		offset.y = bounds.top + (bounds.Height()
594 			- (fontHeight.ascent + fontHeight.descent)) / 2.0f
595 			+ fontHeight.ascent;
596 
597 		DrawString(label, offset);
598 	}
599 
600 	void SetItemCount(int32 count)
601 	{
602 		if (count == fItemCount)
603 			return;
604 		fItemCount = count;
605 		InvalidateLayout();
606 		Invalidate();
607 	}
608 
609 private:
610 	BString _GetLabel() const
611 	{
612 		BString label;
613 		BMessageFormat().Format(label, B_TRANSLATE("{0, plural, one{# item} "
614 			"other{# items}}"), fItemCount);
615 		return label;
616 	}
617 
618 	int32		fItemCount;
619 };
620 
621 
622 // #pragma mark - PackageListView
623 
624 
625 PackageListView::PackageListView(BLocker* modelLock)
626 	:
627 	BColumnListView("package list view", 0, B_FANCY_BORDER, true),
628 	fModelLock(modelLock),
629 	fPackageListener(new(std::nothrow) PackageListener(this))
630 {
631 	AddColumn(new PackageColumn(B_TRANSLATE("Name"), 150, 50, 300,
632 		B_TRUNCATE_MIDDLE), kTitleColumn);
633 	AddColumn(new PackageColumn(B_TRANSLATE("Rating"), 80, 50, 100,
634 		B_TRUNCATE_MIDDLE), kRatingColumn);
635 	AddColumn(new PackageColumn(B_TRANSLATE("Description"), 300, 80, 1000,
636 		B_TRUNCATE_MIDDLE), kDescriptionColumn);
637 	AddColumn(new PackageColumn(B_TRANSLATE("Size"), 60, 50, 100,
638 		B_TRUNCATE_END), kSizeColumn);
639 	AddColumn(new PackageColumn(B_TRANSLATE("Status"), 60, 60, 100,
640 		B_TRUNCATE_END), kStatusColumn);
641 
642 	fItemCountView = new ItemCountView();
643 	AddStatusView(fItemCountView);
644 }
645 
646 
647 PackageListView::~PackageListView()
648 {
649 	Clear();
650 	delete fPackageListener;
651 }
652 
653 
654 void
655 PackageListView::AttachedToWindow()
656 {
657 	BColumnListView::AttachedToWindow();
658 
659 	PackageColumn::InitTextMargin(ScrollView());
660 }
661 
662 
663 void
664 PackageListView::AllAttached()
665 {
666 	BColumnListView::AllAttached();
667 
668 	SetSortingEnabled(true);
669 	SetSortColumn(ColumnAt(0), false, true);
670 }
671 
672 
673 void
674 PackageListView::MessageReceived(BMessage* message)
675 {
676 	switch (message->what) {
677 		case MSG_UPDATE_PACKAGE:
678 		{
679 			BString title;
680 			uint32 changes;
681 			if (message->FindString("title", &title) != B_OK
682 				|| message->FindUInt32("changes", &changes) != B_OK) {
683 				break;
684 			}
685 
686 			BAutolock _(fModelLock);
687 			PackageRow* row = _FindRow(title);
688 			if (row != NULL) {
689 				if ((changes & PKG_CHANGED_SUMMARY) != 0)
690 					row->UpdateSummary();
691 				if ((changes & PKG_CHANGED_RATINGS) != 0)
692 					row->UpdateRating();
693 				if ((changes & PKG_CHANGED_STATE) != 0)
694 					row->UpdateState();
695 				if ((changes & PKG_CHANGED_ICON) != 0)
696 					row->UpdateTitle();
697 			}
698 			break;
699 		}
700 
701 		default:
702 			BColumnListView::MessageReceived(message);
703 			break;
704 	}
705 }
706 
707 
708 void
709 PackageListView::SelectionChanged()
710 {
711 	BColumnListView::SelectionChanged();
712 
713 	BMessage message(MSG_PACKAGE_SELECTED);
714 
715 	PackageRow* selected = dynamic_cast<PackageRow*>(CurrentSelection());
716 	if (selected != NULL)
717 		message.AddString("title", selected->Package()->Title());
718 
719 	Window()->PostMessage(&message);
720 }
721 
722 
723 void
724 PackageListView::AddPackage(const PackageInfoRef& package)
725 {
726 	PackageRow* packageRow = _FindRow(package);
727 
728 	// forget about it if this package is already in the listview
729 	if (packageRow != NULL)
730 		return;
731 
732 	BAutolock _(fModelLock);
733 
734 	// create the row for this package
735 	packageRow = new PackageRow(package, fPackageListener);
736 
737 	// add the row, parent may be NULL (add at top level)
738 	AddRow(packageRow);
739 
740 	// make sure the row is initially expanded
741 	ExpandOrCollapse(packageRow, true);
742 
743 	fItemCountView->SetItemCount(CountRows());
744 }
745 
746 
747 PackageRow*
748 PackageListView::_FindRow(const PackageInfoRef& package, PackageRow* parent)
749 {
750 	for (int32 i = CountRows(parent) - 1; i >= 0; i--) {
751 		PackageRow* row = dynamic_cast<PackageRow*>(RowAt(i, parent));
752 		if (row != NULL && row->Package() == package)
753 			return row;
754 		if (CountRows(row) > 0) {
755 			// recurse into child rows
756 			row = _FindRow(package, row);
757 			if (row != NULL)
758 				return row;
759 		}
760 	}
761 
762 	return NULL;
763 }
764 
765 
766 PackageRow*
767 PackageListView::_FindRow(const BString& packageTitle, PackageRow* parent)
768 {
769 	for (int32 i = CountRows(parent) - 1; i >= 0; i--) {
770 		PackageRow* row = dynamic_cast<PackageRow*>(RowAt(i, parent));
771 		if (row != NULL && row->Package().Get() != NULL
772 			&& row->Package()->Title() == packageTitle) {
773 			return row;
774 		}
775 		if (CountRows(row) > 0) {
776 			// recurse into child rows
777 			row = _FindRow(packageTitle, row);
778 			if (row != NULL)
779 				return row;
780 		}
781 	}
782 
783 	return NULL;
784 }
785 
786