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