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
35static const char* skPackageStateAvailable = B_TRANSLATE_MARK("Available");
36static const char* skPackageStateUninstalled = B_TRANSLATE_MARK("Uninstalled");
37static const char* skPackageStateActive = B_TRANSLATE_MARK("Active");
38static const char* skPackageStateInactive = B_TRANSLATE_MARK("Inactive");
39static const char* skPackageStatePending = B_TRANSLATE_MARK(
40	"Pending" B_UTF8_ELLIPSIS);
41
42
43inline BString
44package_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
75class PackageIconAndTitleField : public BStringField {
76	typedef BStringField Inherited;
77public:
78								PackageIconAndTitleField(
79									const char* packageName,
80									const char* string);
81	virtual						~PackageIconAndTitleField();
82
83			const BString		PackageName() const
84									{ return fPackageName; }
85private:
86			const BString		fPackageName;
87};
88
89
90class RatingField : public BField {
91public:
92								RatingField(float rating);
93	virtual						~RatingField();
94
95			void				SetRating(float rating);
96			float				Rating() const
97									{ return fRating; }
98private:
99			float				fRating;
100};
101
102
103class SizeField : public BStringField {
104public:
105								SizeField(double size);
106	virtual						~SizeField();
107
108			void				SetSize(double size);
109			double				Size() const
110									{ return fSize; }
111private:
112			double				fSize;
113};
114
115
116class DateField : public BStringField {
117public:
118								DateField(uint64 millisSinceEpoc);
119	virtual						~DateField();
120
121			void				SetMillisSinceEpoc(uint64 millisSinceEpoc);
122			uint64				MillisSinceEpoc() const
123									{ return fMillisSinceEpoc; }
124
125private:
126			void				_SetMillisSinceEpoc(uint64 millisSinceEpoc);
127
128private:
129			uint64				fMillisSinceEpoc;
130};
131
132
133// BColumn for PackageListView which knows how to render
134// a PackageIconAndTitleField
135class PackageColumn : public BTitledColumn {
136	typedef BTitledColumn Inherited;
137public:
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
154private:
155			Model*				fModel;
156			uint32				fTruncateMode;
157	static	float				sTextMargin;
158};
159
160
161// BRow for the PackageListView
162class PackageRow : public BRow {
163	typedef BRow Inherited;
164public:
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
185private:
186			PackageInfoRef		fPackage;
187			PackageInfoListenerRef
188								fPackageListener;
189
190			PackageRow*			fNextInHash;
191				// link for BOpenHashTable
192};
193
194
195enum {
196	MSG_UPDATE_PACKAGE		= 'updp'
197};
198
199
200class PackageListener : public PackageInfoListener {
201public:
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
227private:
228	PackageListView*	fView;
229};
230
231
232// #pragma mark - SharedBitmapStringField
233
234
235PackageIconAndTitleField::PackageIconAndTitleField(const char* packageName,
236	const char* string)
237	:
238	Inherited(string),
239	fPackageName(packageName)
240{
241}
242
243
244PackageIconAndTitleField::~PackageIconAndTitleField()
245{
246}
247
248
249// #pragma mark - RatingField
250
251
252RatingField::RatingField(float rating)
253	:
254	fRating(0.0f)
255{
256	SetRating(rating);
257}
258
259
260RatingField::~RatingField()
261{
262}
263
264
265void
266RatingField::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
283SizeField::SizeField(double size)
284	:
285	BStringField(""),
286	fSize(-1.0)
287{
288	SetSize(size);
289}
290
291
292SizeField::~SizeField()
293{
294}
295
296
297void
298SizeField::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
322DateField::DateField(uint64 millisSinceEpoc)
323	:
324	BStringField(""),
325	fMillisSinceEpoc(0)
326{
327	_SetMillisSinceEpoc(millisSinceEpoc);
328}
329
330
331DateField::~DateField()
332{
333}
334
335
336void
337DateField::SetMillisSinceEpoc(uint64 millisSinceEpoc)
338{
339	if (millisSinceEpoc == fMillisSinceEpoc)
340		return;
341	_SetMillisSinceEpoc(millisSinceEpoc);
342}
343
344
345void
346DateField::_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
366float PackageColumn::sTextMargin = 0.0;
367
368
369PackageColumn::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
380void
381PackageColumn::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				if (bitmap != NULL && bitmap->IsValid()) {
432					parent->SetDrawingMode(B_OP_ALPHA);
433					BRect viewRect(x, y, x + 15, y + 15);
434					parent->DrawBitmap(bitmap, bitmap->Bounds(), viewRect);
435					parent->SetDrawingMode(B_OP_OVER);
436				}
437			}
438		}
439
440		// draw the string
441		DrawString(packageIconAndTitleField->ClippedString(), parent, r);
442
443	} else if (stringField != NULL) {
444
445		float width = rect.Width() - (2 * sTextMargin);
446
447		if (width != stringField->Width()) {
448			BString truncatedString(stringField->String());
449
450			parent->TruncateString(&truncatedString, fTruncateMode, width + 2);
451			stringField->SetClippedString(truncatedString.String());
452			stringField->SetWidth(width);
453		}
454
455		DrawString(stringField->ClippedString(), parent, rect);
456
457	} else if (ratingField != NULL) {
458
459		const float kDefaultTextMargin = 8;
460
461		float width = rect.Width() - (2 * kDefaultTextMargin);
462
463		BString string = "���������������";
464		float stringWidth = parent->StringWidth(string);
465		bool drawOverlay = true;
466
467		if (width < stringWidth) {
468			string.SetToFormat("%.1f", ratingField->Rating());
469			drawOverlay = false;
470			stringWidth = parent->StringWidth(string);
471		}
472
473		switch (Alignment()) {
474			default:
475			case B_ALIGN_LEFT:
476				rect.left += kDefaultTextMargin;
477				break;
478			case B_ALIGN_CENTER:
479				rect.left = rect.left + (width - stringWidth) / 2.0f;
480				break;
481
482			case B_ALIGN_RIGHT:
483				rect.left = rect.right - (stringWidth + kDefaultTextMargin);
484				break;
485		}
486
487		rect.left = floorf(rect.left);
488		rect.right = rect.left + stringWidth;
489
490		if (drawOverlay)
491			parent->SetHighColor(0, 170, 255);
492
493		font_height	fontHeight;
494		parent->GetFontHeight(&fontHeight);
495		float y = rect.top + (rect.Height()
496			- (fontHeight.ascent + fontHeight.descent)) / 2
497			+ fontHeight.ascent;
498
499		parent->DrawString(string, BPoint(rect.left, y));
500
501		if (drawOverlay) {
502			rect.left = ceilf(rect.left
503				+ (ratingField->Rating() / 5.0f) * rect.Width());
504
505			rgb_color color = parent->LowColor();
506			color.alpha = 190;
507			parent->SetHighColor(color);
508
509			parent->SetDrawingMode(B_OP_ALPHA);
510			parent->FillRect(rect, B_SOLID_HIGH);
511
512		}
513	}
514}
515
516
517int
518PackageColumn::CompareFields(BField* field1, BField* field2)
519{
520	DateField* dateField1 = dynamic_cast<DateField*>(field1);
521	DateField* dateField2 = dynamic_cast<DateField*>(field2);
522	if (dateField1 != NULL && dateField2 != NULL) {
523		if (dateField1->MillisSinceEpoc() > dateField2->MillisSinceEpoc())
524			return -1;
525		else if (dateField1->MillisSinceEpoc() < dateField2->MillisSinceEpoc())
526			return 1;
527		return 0;
528	}
529
530	SizeField* sizeField1 = dynamic_cast<SizeField*>(field1);
531	SizeField* sizeField2 = dynamic_cast<SizeField*>(field2);
532	if (sizeField1 != NULL && sizeField2 != NULL) {
533		if (sizeField1->Size() > sizeField2->Size())
534			return -1;
535		else if (sizeField1->Size() < sizeField2->Size())
536			return 1;
537		return 0;
538	}
539
540	BStringField* stringField1 = dynamic_cast<BStringField*>(field1);
541	BStringField* stringField2 = dynamic_cast<BStringField*>(field2);
542	if (stringField1 != NULL && stringField2 != NULL) {
543		// TODO: Locale aware string compare... not too important if
544		// package names are not translated.
545		return strcasecmp(stringField1->String(), stringField2->String());
546	}
547
548	RatingField* ratingField1 = dynamic_cast<RatingField*>(field1);
549	RatingField* ratingField2 = dynamic_cast<RatingField*>(field2);
550	if (ratingField1 != NULL && ratingField2 != NULL) {
551		if (ratingField1->Rating() > ratingField2->Rating())
552			return -1;
553		else if (ratingField1->Rating() < ratingField2->Rating())
554			return 1;
555		return 0;
556	}
557
558	return Inherited::CompareFields(field1, field2);
559}
560
561
562float
563PackageColumn::GetPreferredWidth(BField *_field, BView* parent) const
564{
565	PackageIconAndTitleField* packageIconAndTitleField
566		= dynamic_cast<PackageIconAndTitleField*>(_field);
567	BStringField* stringField = dynamic_cast<BStringField*>(_field);
568
569	float parentWidth = Inherited::GetPreferredWidth(_field, parent);
570	float width = 0.0;
571
572	if (packageIconAndTitleField) {
573		BFont font;
574		parent->GetFont(&font);
575		width = font.StringWidth(packageIconAndTitleField->String())
576			+ 3 * sTextMargin;
577		width += 16;
578			// for the icon; always 16px
579	} else if (stringField) {
580		BFont font;
581		parent->GetFont(&font);
582		width = font.StringWidth(stringField->String()) + 2 * sTextMargin;
583	}
584	return max_c(width, parentWidth);
585}
586
587
588bool
589PackageColumn::AcceptsField(const BField* field) const
590{
591	return dynamic_cast<const BStringField*>(field) != NULL
592		|| dynamic_cast<const RatingField*>(field) != NULL;
593}
594
595
596void
597PackageColumn::InitTextMargin(BView* parent)
598{
599	BFont font;
600	parent->GetFont(&font);
601	sTextMargin = ceilf(font.Size() * 0.8);
602}
603
604
605// #pragma mark - PackageRow
606
607
608enum {
609	kTitleColumn,
610	kRatingColumn,
611	kDescriptionColumn,
612	kSizeColumn,
613	kStatusColumn,
614	kRepositoryColumn,
615	kVersionColumn,
616	kVersionCreateTimestampColumn,
617};
618
619
620PackageRow::PackageRow(const PackageInfoRef& packageRef,
621		PackageListener* packageListener)
622	:
623	Inherited(ceilf(be_plain_font->Size() * 1.8f)),
624	fPackage(packageRef),
625	fPackageListener(packageListener),
626	fNextInHash(NULL)
627{
628	if (!packageRef.IsSet())
629		return;
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	packageRef->AddListener(fPackageListener);
644}
645
646
647PackageRow::~PackageRow()
648{
649	if (fPackage.IsSet())
650		fPackage->RemoveListener(fPackageListener);
651}
652
653
654void
655PackageRow::UpdateIconAndTitle()
656{
657	if (!fPackage.IsSet())
658		return;
659	SetField(new PackageIconAndTitleField(
660		fPackage->Name(), fPackage->Title()), kTitleColumn);
661}
662
663
664void
665PackageRow::UpdateState()
666{
667	if (!fPackage.IsSet())
668		return;
669	SetField(new BStringField(package_state_to_string(fPackage)),
670		kStatusColumn);
671}
672
673
674void
675PackageRow::UpdateSummary()
676{
677	if (!fPackage.IsSet())
678		return;
679	SetField(new BStringField(fPackage->ShortDescription()),
680		kDescriptionColumn);
681}
682
683
684void
685PackageRow::UpdateRating()
686{
687	if (!fPackage.IsSet())
688		return;
689	RatingSummary summary = fPackage->CalculateRatingSummary();
690	SetField(new RatingField(summary.averageRating), kRatingColumn);
691}
692
693
694void
695PackageRow::UpdateSize()
696{
697	if (!fPackage.IsSet())
698		return;
699	SetField(new SizeField(fPackage->Size()), kSizeColumn);
700}
701
702
703void
704PackageRow::UpdateRepository()
705{
706	if (!fPackage.IsSet())
707		return;
708	SetField(new BStringField(fPackage->DepotName()), kRepositoryColumn);
709}
710
711
712void
713PackageRow::UpdateVersion()
714{
715	if (!fPackage.IsSet())
716		return;
717	SetField(new BStringField(fPackage->Version().ToString()), kVersionColumn);
718}
719
720
721void
722PackageRow::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
734class PackageListView::ItemCountView : public BView {
735public:
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(font.Size() * 0.75f);
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			be_control_look->GetScrollBarWidth());
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
798private:
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
824struct 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
854PackageListView::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
911PackageListView::~PackageListView()
912{
913	Clear();
914	delete fPackageListener;
915}
916
917
918void
919PackageListView::AttachedToWindow()
920{
921	BColumnListView::AttachedToWindow();
922
923	PackageColumn::InitTextMargin(ScrollView());
924}
925
926
927void
928PackageListView::AllAttached()
929{
930	BColumnListView::AllAttached();
931
932	SetSortingEnabled(true);
933	SetSortColumn(ColumnAt(0), false, true);
934}
935
936
937void
938PackageListView::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
982void
983PackageListView::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
1000void
1001PackageListView::Clear()
1002{
1003	fItemCountView->SetItemCount(0);
1004	BColumnListView::Clear();
1005	fRowByNameTable->Clear();
1006}
1007
1008
1009void
1010PackageListView::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
1036void
1037PackageListView::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
1052void
1053PackageListView::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
1071void
1072PackageListView::AttachWorkStatusView(WorkStatusView* view)
1073{
1074	fWorkStatusView = view;
1075}
1076
1077
1078PackageRow*
1079PackageListView::_FindRow(const PackageInfoRef& package)
1080{
1081	if (!package.IsSet())
1082		return NULL;
1083	return fRowByNameTable->Lookup(package->Name().String());
1084}
1085
1086
1087PackageRow*
1088PackageListView::_FindRow(const BString& packageName)
1089{
1090	return fRowByNameTable->Lookup(packageName.String());
1091}
1092