xref: /haiku/src/apps/haikudepot/ui/PackageListView.cpp (revision caed67a8cba83913b9c21ac2b06ebc6bd1cb3111)
1 /*
2  * Copyright 2018-2024, Andrew Lindesay, <apl@lindesay.co.nz>.
3  * Copyright 2017, Julian Harnath, <julian.harnath@rwth-aachen.de>.
4  * Copyright 2015, Axel Dörfler, <axeld@pinc-software.de>.
5  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
6  * Copyright 2013, Rene Gollent, <rene@gollent.com>.
7  * All rights reserved. Distributed under the terms of the MIT License.
8  */
9 
10 #include "PackageListView.h"
11 
12 #include <algorithm>
13 #include <stdio.h>
14 
15 #include <Autolock.h>
16 #include <Catalog.h>
17 #include <ControlLook.h>
18 #include <NumberFormat.h>
19 #include <ScrollBar.h>
20 #include <StringFormat.h>
21 #include <StringForSize.h>
22 #include <package/hpkg/Strings.h>
23 #include <Window.h>
24 
25 #include "LocaleUtils.h"
26 #include "Logger.h"
27 #include "RatingUtils.h"
28 #include "SharedIcons.h"
29 #include "WorkStatusView.h"
30 
31 
32 #undef B_TRANSLATION_CONTEXT
33 #define B_TRANSLATION_CONTEXT "PackageListView"
34 
35 
36 static const char* skPackageStateAvailable = B_TRANSLATE_MARK("Available");
37 static const char* skPackageStateUninstalled = B_TRANSLATE_MARK("Uninstalled");
38 static const char* skPackageStateActive = B_TRANSLATE_MARK("Active");
39 static const char* skPackageStateInactive = B_TRANSLATE_MARK("Inactive");
40 static const char* skPackageStatePending = B_TRANSLATE_MARK(
41 	"Pending" B_UTF8_ELLIPSIS);
42 
43 
44 inline BString
45 package_state_to_string(PackageInfoRef ref)
46 {
47 	static BNumberFormat numberFormat;
48 
49 	switch (ref->State()) {
50 		case NONE:
51 			return B_TRANSLATE(skPackageStateAvailable);
52 		case INSTALLED:
53 			return B_TRANSLATE(skPackageStateInactive);
54 		case ACTIVATED:
55 			return B_TRANSLATE(skPackageStateActive);
56 		case UNINSTALLED:
57 			return B_TRANSLATE(skPackageStateUninstalled);
58 		case DOWNLOADING:
59 		{
60 			BString data;
61 			float fraction = ref->DownloadProgress();
62 			if (numberFormat.FormatPercent(data, fraction) != B_OK) {
63 				HDERROR("unable to format the percentage");
64 				data = "???";
65 			}
66 			return data;
67 		}
68 		case PENDING:
69 			return B_TRANSLATE(skPackageStatePending);
70 	}
71 
72 	return B_TRANSLATE("Unknown");
73 }
74 
75 
76 class PackageIconAndTitleField : public BStringField {
77 	typedef BStringField Inherited;
78 public:
79 								PackageIconAndTitleField(
80 									const char* packageName,
81 									const char* string);
82 	virtual						~PackageIconAndTitleField();
83 
84 			const BString		PackageName() const
85 									{ return fPackageName; }
86 private:
87 			const BString		fPackageName;
88 };
89 
90 
91 class RatingField : public BField {
92 public:
93 								RatingField(float rating);
94 	virtual						~RatingField();
95 
96 			void				SetRating(float rating);
97 			float				Rating() const
98 									{ return fRating; }
99 private:
100 			float				fRating;
101 };
102 
103 
104 class SizeField : public BStringField {
105 public:
106 								SizeField(double size);
107 	virtual						~SizeField();
108 
109 			void				SetSize(double size);
110 			double				Size() const
111 									{ return fSize; }
112 private:
113 			double				fSize;
114 };
115 
116 
117 class DateField : public BStringField {
118 public:
119 								DateField(uint64 millisSinceEpoc);
120 	virtual						~DateField();
121 
122 			void				SetMillisSinceEpoc(uint64 millisSinceEpoc);
123 			uint64				MillisSinceEpoc() const
124 									{ return fMillisSinceEpoc; }
125 
126 private:
127 			void				_SetMillisSinceEpoc(uint64 millisSinceEpoc);
128 
129 private:
130 			uint64				fMillisSinceEpoc;
131 };
132 
133 
134 // BColumn for PackageListView which knows how to render
135 // a PackageIconAndTitleField
136 class PackageColumn : public BTitledColumn {
137 	typedef BTitledColumn Inherited;
138 public:
139 								PackageColumn(Model* model,
140 									const char* title,
141 									float width, float minWidth,
142 									float maxWidth, uint32 truncateMode,
143 									alignment align = B_ALIGN_LEFT);
144 	virtual						~PackageColumn();
145 
146 	virtual	void				DrawField(BField* field, BRect rect,
147 									BView* parent);
148 	virtual	int					CompareFields(BField* field1, BField* field2);
149 	virtual float				GetPreferredWidth(BField* field,
150 									BView* parent) const;
151 
152 	virtual	bool				AcceptsField(const BField* field) const;
153 
154 	static	void				InitTextMargin(BView* parent);
155 
156 private:
157 			Model*				fModel;
158 			uint32				fTruncateMode;
159 			RatingStarsMetrics*	fRatingsMetrics;
160 	static	float				sTextMargin;
161 };
162 
163 
164 // BRow for the PackageListView
165 class PackageRow : public BRow {
166 	typedef BRow Inherited;
167 public:
168 								PackageRow(
169 									const PackageInfoRef& package,
170 									PackageListener* listener);
171 	virtual						~PackageRow();
172 
173 			const PackageInfoRef& Package() const
174 									{ return fPackage; }
175 
176 			void				UpdateIconAndTitle();
177 			void				UpdateSummary();
178 			void				UpdateState();
179 			void				UpdateRating();
180 			void				UpdateSize();
181 			void				UpdateRepository();
182 			void				UpdateVersion();
183 			void				UpdateVersionCreateTimestamp();
184 
185 			PackageRow*&		NextInHash()
186 									{ return fNextInHash; }
187 
188 private:
189 			PackageInfoRef		fPackage;
190 			PackageInfoListenerRef
191 								fPackageListener;
192 
193 			PackageRow*			fNextInHash;
194 				// link for BOpenHashTable
195 };
196 
197 
198 enum {
199 	MSG_UPDATE_PACKAGE		= 'updp'
200 };
201 
202 
203 class PackageListener : public PackageInfoListener {
204 public:
205 	PackageListener(PackageListView* view)
206 		:
207 		fView(view)
208 	{
209 	}
210 
211 	virtual ~PackageListener()
212 	{
213 	}
214 
215 	virtual void PackageChanged(const PackageInfoEvent& event)
216 	{
217 		BMessenger messenger(fView);
218 		if (!messenger.IsValid())
219 			return;
220 
221 		const PackageInfo& package = *event.Package().Get();
222 
223 		BMessage message(MSG_UPDATE_PACKAGE);
224 		message.AddString("name", package.Name());
225 		message.AddUInt32("changes", event.Changes());
226 
227 		messenger.SendMessage(&message);
228 	}
229 
230 private:
231 	PackageListView*	fView;
232 };
233 
234 
235 // #pragma mark - PackageIconAndTitleField
236 
237 
238 PackageIconAndTitleField::PackageIconAndTitleField(const char* packageName,
239 	const char* string)
240 	:
241 	Inherited(string),
242 	fPackageName(packageName)
243 {
244 }
245 
246 
247 PackageIconAndTitleField::~PackageIconAndTitleField()
248 {
249 }
250 
251 
252 // #pragma mark - RatingField
253 
254 
255 RatingField::RatingField(float rating)
256 	:
257 	fRating(RATING_MISSING)
258 {
259 	SetRating(rating);
260 }
261 
262 
263 RatingField::~RatingField()
264 {
265 }
266 
267 
268 void
269 RatingField::SetRating(float rating)
270 {
271 	fRating = rating;
272 }
273 
274 
275 // #pragma mark - SizeField
276 
277 
278 SizeField::SizeField(double size)
279 	:
280 	BStringField(""),
281 	fSize(-1.0)
282 {
283 	SetSize(size);
284 }
285 
286 
287 SizeField::~SizeField()
288 {
289 }
290 
291 
292 void
293 SizeField::SetSize(double size)
294 {
295 	if (size < 0.0)
296 		size = 0.0;
297 
298 	if (size == fSize)
299 		return;
300 
301 	BString sizeString;
302 	if (size == 0) {
303 		sizeString = B_TRANSLATE_CONTEXT("-", "no package size");
304 	} else {
305 		char buffer[256];
306 		sizeString = string_for_size(size, buffer, sizeof(buffer));
307 	}
308 
309 	fSize = size;
310 	SetString(sizeString.String());
311 }
312 
313 
314 // #pragma mark - DateField
315 
316 
317 DateField::DateField(uint64 millisSinceEpoc)
318 	:
319 	BStringField(""),
320 	fMillisSinceEpoc(0)
321 {
322 	_SetMillisSinceEpoc(millisSinceEpoc);
323 }
324 
325 
326 DateField::~DateField()
327 {
328 }
329 
330 
331 void
332 DateField::SetMillisSinceEpoc(uint64 millisSinceEpoc)
333 {
334 	if (millisSinceEpoc == fMillisSinceEpoc)
335 		return;
336 	_SetMillisSinceEpoc(millisSinceEpoc);
337 }
338 
339 
340 void
341 DateField::_SetMillisSinceEpoc(uint64 millisSinceEpoc)
342 {
343 	BString dateString;
344 
345 	if (millisSinceEpoc == 0)
346 		dateString = B_TRANSLATE_CONTEXT("-", "no package publish");
347 	else
348 		dateString = LocaleUtils::TimestampToDateString(millisSinceEpoc);
349 
350 	fMillisSinceEpoc = millisSinceEpoc;
351 	SetString(dateString.String());
352 }
353 
354 
355 // #pragma mark - PackageColumn
356 
357 
358 // TODO: Code-duplication with DriveSetup PartitionList.cpp
359 
360 
361 float PackageColumn::sTextMargin = 0.0;
362 
363 
364 PackageColumn::PackageColumn(Model* model, const char* title, float width,
365 		float minWidth, float maxWidth, uint32 truncateMode, alignment align)
366 	:
367 	Inherited(title, width, minWidth, maxWidth, align),
368 	fModel(model),
369 	fTruncateMode(truncateMode)
370 {
371 	SetWantsEvents(true);
372 
373 	BSize ratingStarSize = SharedIcons::IconStarBlue12Scaled()->Bitmap()->Bounds().Size();
374 	fRatingsMetrics = new RatingStarsMetrics(ratingStarSize);
375 }
376 
377 
378 PackageColumn::~PackageColumn()
379 {
380 	delete fRatingsMetrics;
381 }
382 
383 
384 void
385 PackageColumn::DrawField(BField* field, BRect rect, BView* parent)
386 {
387 	PackageIconAndTitleField* packageIconAndTitleField
388 		= dynamic_cast<PackageIconAndTitleField*>(field);
389 	BStringField* stringField = dynamic_cast<BStringField*>(field);
390 	RatingField* ratingField = dynamic_cast<RatingField*>(field);
391 
392 	if (packageIconAndTitleField != NULL) {
393 		BSize iconSize = BControlLook::ComposeIconSize(16);
394 		BRect r(BPoint(0, 0), iconSize);
395 
396 		// figure out the placement
397 		float x = 0.0;
398 		float y = rect.top + ((rect.Height() - r.Height()) / 2) - 1;
399 		float width = 0.0;
400 
401 		switch (Alignment()) {
402 			default:
403 			case B_ALIGN_LEFT:
404 			case B_ALIGN_CENTER:
405 				x = rect.left + sTextMargin;
406 				width = rect.right - (x + r.Width()) - (2 * sTextMargin);
407 				r.Set(x + r.Width(), rect.top, rect.right - width, rect.bottom);
408 				break;
409 
410 			case B_ALIGN_RIGHT:
411 				x = rect.right - sTextMargin - r.Width();
412 				width = (x - rect.left - (2 * sTextMargin));
413 				r.Set(rect.left, rect.top, rect.left + width, rect.bottom);
414 				break;
415 		}
416 
417 		if (width != packageIconAndTitleField->Width()) {
418 			BString truncatedString(packageIconAndTitleField->String());
419 			parent->TruncateString(&truncatedString, fTruncateMode, width + 2);
420 			packageIconAndTitleField->SetClippedString(truncatedString.String());
421 			packageIconAndTitleField->SetWidth(width);
422 		}
423 
424 		// draw the bitmap
425 		BitmapHolderRef bitmapHolderRef;
426 		status_t bitmapResult;
427 
428 		bitmapResult = fModel->GetPackageIconRepository().GetIcon(
429 			packageIconAndTitleField->PackageName(), iconSize.Width() + 1, bitmapHolderRef);
430 
431 		if (bitmapResult == B_OK) {
432 			if (bitmapHolderRef.IsSet()) {
433 				const BBitmap* bitmap = bitmapHolderRef->Bitmap();
434 				if (bitmap != NULL && bitmap->IsValid()) {
435 					parent->SetDrawingMode(B_OP_ALPHA);
436 					BRect viewRect(BPoint(x, y), iconSize);
437 					parent->DrawBitmap(bitmap, bitmap->Bounds(), viewRect);
438 					parent->SetDrawingMode(B_OP_OVER);
439 				}
440 			}
441 		}
442 
443 		// draw the string
444 		DrawString(packageIconAndTitleField->ClippedString(), parent, r);
445 
446 	} else if (stringField != NULL) {
447 
448 		float width = rect.Width() - (2 * sTextMargin);
449 
450 		if (width != stringField->Width()) {
451 			BString truncatedString(stringField->String());
452 
453 			parent->TruncateString(&truncatedString, fTruncateMode, width + 2);
454 			stringField->SetClippedString(truncatedString.String());
455 			stringField->SetWidth(width);
456 		}
457 
458 		DrawString(stringField->ClippedString(), parent, rect);
459 
460 	} else if (ratingField != NULL) {
461 		float width = rect.Width();
462 		float padding = be_control_look->ComposeSpacing(B_USE_SMALL_SPACING);
463 		bool isRatingValid = ratingField->Rating() >= RATING_MIN;
464 
465 		if (!isRatingValid || width < fRatingsMetrics->Size().Width() + padding * 2.0) {
466 			BString ratingAsText = "-";
467 
468 			if (isRatingValid)
469 				ratingAsText.SetToFormat("%.1f", ratingField->Rating());
470 
471 			float ratingAsTextWidth = parent->StringWidth(ratingAsText);
472 
473 			if (ratingAsTextWidth + padding * 2.0 < width) {
474 				font_height fontHeight;
475 				parent->GetFontHeight(&fontHeight);
476 				float fullHeight = fontHeight.ascent + fontHeight.descent;
477 				float y = rect.top + (rect.Height() - fullHeight) / 2 + fontHeight.ascent;
478 				parent->DrawString(ratingAsText, BPoint(rect.left + padding, y));
479 			}
480 		} else {
481 			const BBitmap* starIcon = SharedIcons::IconStarBlue12Scaled()->Bitmap();
482 			float ratingsStarsHeight = fRatingsMetrics->Size().Height();
483 			BPoint starsPt(floorf(rect.LeftTop().x + padding),
484 				floorf(rect.LeftTop().y + (rect.Size().Height() / 2.0) - ratingsStarsHeight / 2.0));
485 			RatingUtils::Draw(parent, starsPt, ratingField->Rating(), starIcon);
486 		}
487 	}
488 }
489 
490 
491 int
492 PackageColumn::CompareFields(BField* field1, BField* field2)
493 {
494 	DateField* dateField1 = dynamic_cast<DateField*>(field1);
495 	DateField* dateField2 = dynamic_cast<DateField*>(field2);
496 	if (dateField1 != NULL && dateField2 != NULL) {
497 		if (dateField1->MillisSinceEpoc() > dateField2->MillisSinceEpoc())
498 			return -1;
499 		else if (dateField1->MillisSinceEpoc() < dateField2->MillisSinceEpoc())
500 			return 1;
501 		return 0;
502 	}
503 
504 	SizeField* sizeField1 = dynamic_cast<SizeField*>(field1);
505 	SizeField* sizeField2 = dynamic_cast<SizeField*>(field2);
506 	if (sizeField1 != NULL && sizeField2 != NULL) {
507 		if (sizeField1->Size() > sizeField2->Size())
508 			return -1;
509 		else if (sizeField1->Size() < sizeField2->Size())
510 			return 1;
511 		return 0;
512 	}
513 
514 	BStringField* stringField1 = dynamic_cast<BStringField*>(field1);
515 	BStringField* stringField2 = dynamic_cast<BStringField*>(field2);
516 	if (stringField1 != NULL && stringField2 != NULL) {
517 		// TODO: Locale aware string compare... not too important if
518 		// package names are not translated.
519 		return strcasecmp(stringField1->String(), stringField2->String());
520 	}
521 
522 	RatingField* ratingField1 = dynamic_cast<RatingField*>(field1);
523 	RatingField* ratingField2 = dynamic_cast<RatingField*>(field2);
524 	if (ratingField1 != NULL && ratingField2 != NULL) {
525 		if (ratingField1->Rating() > ratingField2->Rating())
526 			return -1;
527 		else if (ratingField1->Rating() < ratingField2->Rating())
528 			return 1;
529 		return 0;
530 	}
531 
532 	return Inherited::CompareFields(field1, field2);
533 }
534 
535 
536 float
537 PackageColumn::GetPreferredWidth(BField *_field, BView* parent) const
538 {
539 	PackageIconAndTitleField* packageIconAndTitleField
540 		= dynamic_cast<PackageIconAndTitleField*>(_field);
541 	BStringField* stringField = dynamic_cast<BStringField*>(_field);
542 
543 	float parentWidth = Inherited::GetPreferredWidth(_field, parent);
544 	float width = 0.0;
545 
546 	if (packageIconAndTitleField) {
547 		BFont font;
548 		parent->GetFont(&font);
549 		width = font.StringWidth(packageIconAndTitleField->String())
550 			+ 3 * sTextMargin;
551 		width += 16;
552 			// for the icon; always 16px
553 	} else if (stringField) {
554 		BFont font;
555 		parent->GetFont(&font);
556 		width = font.StringWidth(stringField->String()) + 2 * sTextMargin;
557 	}
558 	return max_c(width, parentWidth);
559 }
560 
561 
562 bool
563 PackageColumn::AcceptsField(const BField* field) const
564 {
565 	return dynamic_cast<const BStringField*>(field) != NULL
566 		|| dynamic_cast<const RatingField*>(field) != NULL;
567 }
568 
569 
570 void
571 PackageColumn::InitTextMargin(BView* parent)
572 {
573 	BFont font;
574 	parent->GetFont(&font);
575 	sTextMargin = ceilf(font.Size() * 0.8);
576 }
577 
578 
579 // #pragma mark - PackageRow
580 
581 
582 enum {
583 	kTitleColumn,
584 	kRatingColumn,
585 	kDescriptionColumn,
586 	kSizeColumn,
587 	kStatusColumn,
588 	kRepositoryColumn,
589 	kVersionColumn,
590 	kVersionCreateTimestampColumn,
591 };
592 
593 
594 PackageRow::PackageRow(const PackageInfoRef& packageRef,
595 		PackageListener* packageListener)
596 	:
597 	Inherited(ceilf(be_plain_font->Size() * 1.8f)),
598 	fPackage(packageRef),
599 	fPackageListener(packageListener),
600 	fNextInHash(NULL)
601 {
602 	if (!packageRef.IsSet())
603 		return;
604 
605 	// Package icon and title
606 	// NOTE: The icon BBitmap is referenced by the fPackage member.
607 	UpdateIconAndTitle();
608 
609 	UpdateRating();
610 	UpdateSummary();
611 	UpdateSize();
612 	UpdateState();
613 	UpdateRepository();
614 	UpdateVersion();
615 	UpdateVersionCreateTimestamp();
616 
617 	packageRef->AddListener(fPackageListener);
618 }
619 
620 
621 PackageRow::~PackageRow()
622 {
623 	if (fPackage.IsSet())
624 		fPackage->RemoveListener(fPackageListener);
625 }
626 
627 
628 void
629 PackageRow::UpdateIconAndTitle()
630 {
631 	if (!fPackage.IsSet())
632 		return;
633 	SetField(new PackageIconAndTitleField(
634 		fPackage->Name(), fPackage->Title()), kTitleColumn);
635 }
636 
637 
638 void
639 PackageRow::UpdateState()
640 {
641 	if (!fPackage.IsSet())
642 		return;
643 	SetField(new BStringField(package_state_to_string(fPackage)),
644 		kStatusColumn);
645 }
646 
647 
648 void
649 PackageRow::UpdateSummary()
650 {
651 	if (!fPackage.IsSet())
652 		return;
653 	SetField(new BStringField(fPackage->ShortDescription()),
654 		kDescriptionColumn);
655 }
656 
657 
658 void
659 PackageRow::UpdateRating()
660 {
661 	if (!fPackage.IsSet())
662 		return;
663 	RatingSummary summary = fPackage->CalculateRatingSummary();
664 	SetField(new RatingField(summary.averageRating), kRatingColumn);
665 }
666 
667 
668 void
669 PackageRow::UpdateSize()
670 {
671 	if (!fPackage.IsSet())
672 		return;
673 	SetField(new SizeField(fPackage->Size()), kSizeColumn);
674 }
675 
676 
677 void
678 PackageRow::UpdateRepository()
679 {
680 	if (!fPackage.IsSet())
681 		return;
682 	SetField(new BStringField(fPackage->DepotName()), kRepositoryColumn);
683 }
684 
685 
686 void
687 PackageRow::UpdateVersion()
688 {
689 	if (!fPackage.IsSet())
690 		return;
691 	SetField(new BStringField(fPackage->Version().ToString()), kVersionColumn);
692 }
693 
694 
695 void
696 PackageRow::UpdateVersionCreateTimestamp()
697 {
698 	if (!fPackage.IsSet())
699 		return;
700 	SetField(new DateField(fPackage->VersionCreateTimestamp()),
701 		kVersionCreateTimestampColumn);
702 }
703 
704 
705 // #pragma mark - ItemCountView
706 
707 
708 class PackageListView::ItemCountView : public BView {
709 public:
710 	ItemCountView()
711 		:
712 		BView("item count view", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE),
713 		fItemCount(0)
714 	{
715 		BFont font(be_plain_font);
716 		font.SetSize(font.Size() * 0.75f);
717 		SetFont(&font);
718 
719 		SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
720 		SetLowUIColor(ViewUIColor());
721 		SetHighUIColor(LowUIColor(), B_DARKEN_4_TINT);
722 
723 		// constantly calculating the size is expensive so here a sensible
724 		// upper limit on the number of packages is arbitrarily chosen.
725 		fMinSize = BSize(StringWidth(_DeriveLabel(999999)) + 10,
726 			be_control_look->GetScrollBarWidth());
727 	}
728 
729 	virtual BSize MinSize()
730 	{
731 		return fMinSize;
732 	}
733 
734 	virtual BSize PreferredSize()
735 	{
736 		return MinSize();
737 	}
738 
739 	virtual BSize MaxSize()
740 	{
741 		return MinSize();
742 	}
743 
744 	virtual void Draw(BRect updateRect)
745 	{
746 		FillRect(updateRect, B_SOLID_LOW);
747 
748 		font_height fontHeight;
749 		GetFontHeight(&fontHeight);
750 
751 		BRect bounds(Bounds());
752 		float width = StringWidth(fLabel);
753 
754 		BPoint offset;
755 		offset.x = bounds.left + (bounds.Width() - width) / 2.0f;
756 		offset.y = bounds.top + (bounds.Height()
757 			- (fontHeight.ascent + fontHeight.descent)) / 2.0f
758 			+ fontHeight.ascent;
759 
760 		DrawString(fLabel, offset);
761 	}
762 
763 	void SetItemCount(int32 count)
764 	{
765 		if (count == fItemCount)
766 			return;
767 		fItemCount = count;
768 		fLabel = _DeriveLabel(fItemCount);
769 		Invalidate();
770 	}
771 
772 private:
773 
774 /*! This method is hit quite often when the list of packages in the
775     table-view are updated.  Derivation of the plural for some
776     languages such as Russian can be slow so this method should be
777     called sparingly.
778 */
779 
780 	BString _DeriveLabel(int32 count) const
781 	{
782 		static BStringFormat format(B_TRANSLATE("{0, plural, "
783 			"one{# item} other{# items}}"));
784 		BString label;
785 		format.Format(label, count);
786 		return label;
787 	}
788 
789 	int32		fItemCount;
790 	BString		fLabel;
791 	BSize		fMinSize;
792 };
793 
794 
795 // #pragma mark - PackageListView::RowByNameHashDefinition
796 
797 
798 struct PackageListView::RowByNameHashDefinition {
799 	typedef const char*	KeyType;
800 	typedef	PackageRow	ValueType;
801 
802 	size_t HashKey(const char* key) const
803 	{
804 		return BPackageKit::BHPKG::BPrivate::hash_string(key);
805 	}
806 
807 	size_t Hash(PackageRow* value) const
808 	{
809 		return BPackageKit::BHPKG::BPrivate::hash_string(
810 			value->Package()->Name().String());
811 	}
812 
813 	bool Compare(const char* key, PackageRow* value) const
814 	{
815 		return value->Package()->Name() == key;
816 	}
817 
818 	ValueType*& GetLink(PackageRow* value) const
819 	{
820 		return value->NextInHash();
821 	}
822 };
823 
824 
825 // #pragma mark - PackageListView
826 
827 
828 PackageListView::PackageListView(Model* model)
829 	:
830 	BColumnListView(B_TRANSLATE("All packages"), 0, B_FANCY_BORDER, true),
831 	fModel(model),
832 	fPackageListener(new(std::nothrow) PackageListener(this)),
833 	fRowByNameTable(new RowByNameTable()),
834 	fWorkStatusView(NULL),
835 	fIgnoreSelectionChanged(false)
836 {
837 	float scale = be_plain_font->Size() / 12.f;
838 	float spacing = be_control_look->DefaultItemSpacing() * 2;
839 
840 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Name"),
841 		150 * scale, 50 * scale, 300 * scale,
842 		B_TRUNCATE_MIDDLE), kTitleColumn);
843 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Rating"),
844 		80 * scale, 50 * scale, 100 * scale,
845 		B_TRUNCATE_MIDDLE), kRatingColumn);
846 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Description"),
847 		300 * scale, 80 * scale, 1000 * scale,
848 		B_TRUNCATE_MIDDLE), kDescriptionColumn);
849 	PackageColumn* sizeColumn = new PackageColumn(fModel, B_TRANSLATE("Size"),
850 		spacing + StringWidth("9999.99 KiB"), 50 * scale,
851 		140 * scale, B_TRUNCATE_END);
852 	sizeColumn->SetAlignment(B_ALIGN_RIGHT);
853 	AddColumn(sizeColumn, kSizeColumn);
854 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Status"),
855 		spacing + StringWidth(B_TRANSLATE("Available")), 60 * scale,
856 		140 * scale, B_TRUNCATE_END), kStatusColumn);
857 
858 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Repository"),
859 		120 * scale, 50 * scale, 200 * scale,
860 		B_TRUNCATE_MIDDLE), kRepositoryColumn);
861 	SetColumnVisible(kRepositoryColumn, false);
862 		// invisible by default
863 
864 	float widthWithPlacboVersion = spacing
865 		+ StringWidth("8.2.3176-2");
866 		// average sort of version length as model
867 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Version"),
868 		widthWithPlacboVersion, widthWithPlacboVersion,
869 		widthWithPlacboVersion + (50 * scale),
870 		B_TRUNCATE_MIDDLE), kVersionColumn);
871 
872 	float widthWithPlaceboDate = spacing
873 		+ StringWidth(LocaleUtils::TimestampToDateString(
874 			static_cast<uint64>(1000)));
875 	AddColumn(new PackageColumn(fModel, B_TRANSLATE("Date"),
876 		widthWithPlaceboDate, widthWithPlaceboDate,
877 		widthWithPlaceboDate + (50 * scale),
878 		B_TRUNCATE_END), kVersionCreateTimestampColumn);
879 
880 	fItemCountView = new ItemCountView();
881 	AddStatusView(fItemCountView);
882 }
883 
884 
885 PackageListView::~PackageListView()
886 {
887 	Clear();
888 	delete fPackageListener;
889 }
890 
891 
892 void
893 PackageListView::AttachedToWindow()
894 {
895 	BColumnListView::AttachedToWindow();
896 
897 	PackageColumn::InitTextMargin(ScrollView());
898 }
899 
900 
901 void
902 PackageListView::AllAttached()
903 {
904 	BColumnListView::AllAttached();
905 
906 	SetSortingEnabled(true);
907 	SetSortColumn(ColumnAt(0), false, true);
908 }
909 
910 
911 void
912 PackageListView::MessageReceived(BMessage* message)
913 {
914 	switch (message->what) {
915 		case MSG_UPDATE_PACKAGE:
916 		{
917 			BString name;
918 			uint32 changes;
919 			if (message->FindString("name", &name) != B_OK
920 				|| message->FindUInt32("changes", &changes) != B_OK) {
921 				break;
922 			}
923 
924 			BAutolock _(fModel->Lock());
925 			PackageRow* row = _FindRow(name);
926 			if (row != NULL) {
927 				if ((changes & PKG_CHANGED_TITLE) != 0)
928 					row->UpdateIconAndTitle();
929 				if ((changes & PKG_CHANGED_SUMMARY) != 0)
930 					row->UpdateSummary();
931 				if ((changes & PKG_CHANGED_RATINGS) != 0)
932 					row->UpdateRating();
933 				if ((changes & PKG_CHANGED_STATE) != 0)
934 					row->UpdateState();
935 				if ((changes & PKG_CHANGED_SIZE) != 0)
936 					row->UpdateSize();
937 				if ((changes & PKG_CHANGED_ICON) != 0)
938 					row->UpdateIconAndTitle();
939 				if ((changes & PKG_CHANGED_DEPOT) != 0)
940 					row->UpdateRepository();
941 				if ((changes & PKG_CHANGED_VERSION) != 0)
942 					row->UpdateVersion();
943 				if ((changes & PKG_CHANGED_VERSION_CREATE_TIMESTAMP) != 0)
944 					row->UpdateVersionCreateTimestamp();
945 			}
946 			break;
947 		}
948 
949 		default:
950 			BColumnListView::MessageReceived(message);
951 			break;
952 	}
953 }
954 
955 
956 void
957 PackageListView::SelectionChanged()
958 {
959 	BColumnListView::SelectionChanged();
960 
961 	if (fIgnoreSelectionChanged)
962 		return;
963 
964 	BMessage message(MSG_PACKAGE_SELECTED);
965 
966 	PackageRow* selected = dynamic_cast<PackageRow*>(CurrentSelection());
967 	if (selected != NULL)
968 		message.AddString("name", selected->Package()->Name());
969 
970 	Window()->PostMessage(&message);
971 }
972 
973 
974 void
975 PackageListView::Clear()
976 {
977 	fItemCountView->SetItemCount(0);
978 	BColumnListView::Clear();
979 	fRowByNameTable->Clear();
980 }
981 
982 
983 void
984 PackageListView::AddPackage(const PackageInfoRef& package)
985 {
986 	PackageRow* packageRow = _FindRow(package);
987 
988 	// forget about it if this package is already in the listview
989 	if (packageRow != NULL)
990 		return;
991 
992 	BAutolock _(fModel->Lock());
993 
994 	// create the row for this package
995 	packageRow = new PackageRow(package, fPackageListener);
996 
997 	// add the row, parent may be NULL (add at top level)
998 	AddRow(packageRow);
999 
1000 	// add to hash table for quick lookup of row by package name
1001 	fRowByNameTable->Insert(packageRow);
1002 
1003 	// make sure the row is initially expanded
1004 	ExpandOrCollapse(packageRow, true);
1005 
1006 	fItemCountView->SetItemCount(CountRows());
1007 }
1008 
1009 
1010 void
1011 PackageListView::RemovePackage(const PackageInfoRef& package)
1012 {
1013 	PackageRow* packageRow = _FindRow(package);
1014 	if (packageRow == NULL)
1015 		return;
1016 
1017 	fRowByNameTable->Remove(packageRow);
1018 
1019 	RemoveRow(packageRow);
1020 	delete packageRow;
1021 
1022 	fItemCountView->SetItemCount(CountRows());
1023 }
1024 
1025 
1026 void
1027 PackageListView::SelectPackage(const PackageInfoRef& package)
1028 {
1029 	fIgnoreSelectionChanged = true;
1030 
1031 	PackageRow* row = _FindRow(package);
1032 	BRow* selected = CurrentSelection();
1033 	if (row != selected)
1034 		DeselectAll();
1035 	if (row != NULL) {
1036 		AddToSelection(row);
1037 		SetFocusRow(row, false);
1038 		ScrollTo(row);
1039 	}
1040 
1041 	fIgnoreSelectionChanged = false;
1042 }
1043 
1044 
1045 void
1046 PackageListView::AttachWorkStatusView(WorkStatusView* view)
1047 {
1048 	fWorkStatusView = view;
1049 }
1050 
1051 
1052 PackageRow*
1053 PackageListView::_FindRow(const PackageInfoRef& package)
1054 {
1055 	if (!package.IsSet())
1056 		return NULL;
1057 	return fRowByNameTable->Lookup(package->Name().String());
1058 }
1059 
1060 
1061 PackageRow*
1062 PackageListView::_FindRow(const BString& packageName)
1063 {
1064 	return fRowByNameTable->Lookup(packageName.String());
1065 }
1066