/* * Copyright 2018-2025, Andrew Lindesay, . * Copyright 2017, Julian Harnath, . * Copyright 2015, Axel Dörfler, . * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent, . * All rights reserved. Distributed under the terms of the MIT License. */ #include "PackageListView.h" #include #include #include #include #include #include #include #include #include #include #include #include "LocaleUtils.h" #include "Logger.h" #include "PackageUtils.h" #include "RatingUtils.h" #include "SharedIcons.h" #include "WorkStatusView.h" #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "PackageListView" static const char* skPackageStateAvailable = B_TRANSLATE_MARK("Available"); static const char* skPackageStateUninstalled = B_TRANSLATE_MARK("Uninstalled"); static const char* skPackageStateActive = B_TRANSLATE_MARK("Active"); static const char* skPackageStateInactive = B_TRANSLATE_MARK("Inactive"); static const char* skPackageStatePending = B_TRANSLATE_MARK("Pending" B_UTF8_ELLIPSIS); static const BString sSizeNoValue = B_TRANSLATE_CONTEXT("-", "no package size"); inline BString package_state_to_string(PackageInfoRef package) { static BNumberFormat numberFormat; switch (PackageUtils::State(package)) { case NONE: return B_TRANSLATE(skPackageStateAvailable); case INSTALLED: return B_TRANSLATE(skPackageStateInactive); case ACTIVATED: return B_TRANSLATE(skPackageStateActive); case UNINSTALLED: return B_TRANSLATE(skPackageStateUninstalled); case DOWNLOADING: { BString data; float fraction = PackageUtils::DownloadProgress(package); if (numberFormat.FormatPercent(data, fraction) != B_OK) { HDERROR("unable to format the percentage"); data = "???"; } return data; } case PENDING: return B_TRANSLATE(skPackageStatePending); } return B_TRANSLATE("Unknown"); } class LazyStringField : public BField { public: LazyStringField(); virtual ~LazyStringField(); const char* String(); void SetClippedString(const char* string); bool HasClippedString() const; const char* ClippedString() const; void SetWidth(float); float Width() const; protected: virtual const BString CreateString() const = 0; void EvictCache(); private: BString fCachedValue; bool fIsCached; float fWidth; BString fClippedString; bool fHasClippedString; }; class PackageIconAndTitleField : public BStringField { typedef BStringField Inherited; public: PackageIconAndTitleField( const char* packageName, const char* string, bool isActivated, bool isNativeDesktop); virtual ~PackageIconAndTitleField(); const BString PackageName() const { return fPackageName; } bool IsActivated() const { return fIsActivated; } bool IsNativeDesktop() const { return fIsNativeDesktop; } private: const BString fPackageName; const bool fIsActivated; const bool fIsNativeDesktop; }; class RatingField : public BField { public: RatingField(float rating); virtual ~RatingField(); void SetRating(float rating); float Rating() const { return fRating; } private: float fRating; }; class SizeField : public LazyStringField { public: SizeField(double size); virtual ~SizeField(); void SetSize(double size); double Size() const { return fSize; } protected: virtual const BString CreateString() const; private: double fSize; }; class DateField : public LazyStringField { public: DateField(uint64 millisSinceEpoc); virtual ~DateField(); void SetMillisSinceEpoc(uint64 millisSinceEpoc); uint64 MillisSinceEpoc() const { return fMillisSinceEpoc; } protected: virtual const BString CreateString() const; private: uint64 fMillisSinceEpoc; }; // BColumn for PackageListView which knows how to render // a PackageIconAndTitleField class PackageColumn : public BTitledColumn { typedef BTitledColumn Inherited; public: PackageColumn(Model* model, const char* title, float width, float minWidth, float maxWidth, uint32 truncateMode, alignment align = B_ALIGN_LEFT); virtual ~PackageColumn(); virtual void DrawField(BField* field, BRect rect, BView* parent); virtual int CompareFields(BField* field1, BField* field2); virtual float GetPreferredWidth(BField* field, BView* parent) const; virtual bool AcceptsField(const BField* field) const; static void InitTextMargin(BView* parent); private: Model* fModel; uint32 fTruncateMode; RatingStarsMetrics* fRatingsMetrics; static float sTextMargin; }; // BRow for the PackageListView class PackageRow : public BRow { typedef BRow Inherited; public: PackageRow(const PackageInfoRef& package); virtual ~PackageRow(); const PackageInfoRef& Package() const { return fPackage; } void SetPackage(const PackageInfoRef& value); void UpdateIconAndTitle(); void UpdateSummary(); void UpdateState(); void UpdateRating(); void UpdateSize(); void UpdateRepository(); void UpdateVersion(); void UpdateVersionCreateTimestamp(); PackageRow*& NextInHash() { return fNextInHash; } private: PackageInfoRef fPackage; PackageRow* fNextInHash; // link for BOpenHashTable }; // #pragma mark - LazyStringField LazyStringField::LazyStringField() : fCachedValue(), fIsCached(false), fWidth(-1.0f), fClippedString(), fHasClippedString(false) { } LazyStringField::~LazyStringField() { } const char* LazyStringField::String() { if (!fIsCached) { EvictCache(); fCachedValue = CreateString(); fIsCached = true; } return fCachedValue.String(); } void LazyStringField::SetClippedString(const char* string) { fClippedString = string; fHasClippedString = true; } bool LazyStringField::HasClippedString() const { return fHasClippedString; } const char* LazyStringField::ClippedString() const { return fClippedString.String(); } void LazyStringField::SetWidth(float value) { fWidth = value; } float LazyStringField::Width() const { return fWidth; } void LazyStringField::EvictCache() { fCachedValue = ""; fIsCached = false; fWidth = -1.0f; fClippedString = ""; fHasClippedString = false; } // #pragma mark - PackageIconAndTitleField PackageIconAndTitleField::PackageIconAndTitleField(const char* packageName, const char* string, bool isActivated, bool isNativeDesktop) : Inherited(string), fPackageName(packageName), fIsActivated(isActivated), fIsNativeDesktop(isNativeDesktop) { } PackageIconAndTitleField::~PackageIconAndTitleField() { } // #pragma mark - RatingField RatingField::RatingField(float rating) : fRating(RATING_MISSING) { SetRating(rating); } RatingField::~RatingField() { } void RatingField::SetRating(float rating) { fRating = rating; } // #pragma mark - SizeField SizeField::SizeField(double size) : fSize(-1.0) { SetSize(size); } SizeField::~SizeField() { } void SizeField::SetSize(double size) { if (size < 0.0) size = 0.0; if (size == fSize) return; fSize = size; EvictCache(); } const BString SizeField::CreateString() const { if (fSize == 0.0) return sSizeNoValue; // TODO; don't keep making the buffer on the stack. char buffer[256]; return string_for_size(fSize, buffer, sizeof(buffer)); } // #pragma mark - DateField DateField::DateField(uint64 millisSinceEpoc) : fMillisSinceEpoc(0) { SetMillisSinceEpoc(millisSinceEpoc); } DateField::~DateField() { } void DateField::SetMillisSinceEpoc(uint64 millisSinceEpoc) { if (millisSinceEpoc == fMillisSinceEpoc) return; fMillisSinceEpoc = millisSinceEpoc; EvictCache(); } const BString DateField::CreateString() const { if (fMillisSinceEpoc == 0) return B_TRANSLATE_CONTEXT("-", "no package publish"); return LocaleUtils::TimestampToDateString(fMillisSinceEpoc); } // #pragma mark - PackageColumn // TODO: Code-duplication with DriveSetup PartitionList.cpp float PackageColumn::sTextMargin = 0.0; PackageColumn::PackageColumn(Model* model, const char* title, float width, float minWidth, float maxWidth, uint32 truncateMode, alignment align) : Inherited(title, width, minWidth, maxWidth, align), fModel(model), fTruncateMode(truncateMode) { SetWantsEvents(true); BSize ratingStarSize = SharedIcons::IconStarBlue12Scaled()->Bitmap()->Bounds().Size(); fRatingsMetrics = new RatingStarsMetrics(ratingStarSize); } PackageColumn::~PackageColumn() { delete fRatingsMetrics; } void PackageColumn::DrawField(BField* field, BRect rect, BView* parent) { PackageIconAndTitleField* packageIconAndTitleField = dynamic_cast(field); BStringField* stringField = dynamic_cast(field); LazyStringField* lazyStringField = dynamic_cast(field); RatingField* ratingField = dynamic_cast(field); if (packageIconAndTitleField != NULL) { // TODO (andponlin) factor this out as this method is getting too large. BSize iconSize = BControlLook::ComposeIconSize(16); BSize trailingIconSize = BControlLook::ComposeIconSize(8); float trailingIconPaddingFactor = 0.2f; BRect iconRect; BRect titleRect; float titleTextWidth = 0.0f; float textMargin = 8.0f; // copied from ColumnTypes.cpp std::vector trailingIconBitmaps; if (packageIconAndTitleField->IsActivated()) trailingIconBitmaps.push_back(SharedIcons::IconInstalled16Scaled()); if (packageIconAndTitleField->IsNativeDesktop()) trailingIconBitmaps.push_back(SharedIcons::IconNative16Scaled()); // If there is not enough space then drop the "activated" indicator in order to make more // room for the title. float trailingIconsWidth = static_cast(trailingIconBitmaps.size()) * (trailingIconSize.Width() * (1.0 + trailingIconPaddingFactor)); if (!trailingIconBitmaps.empty()) { static float sMinimalTextPart = -1.0; if (sMinimalTextPart < 0.0) sMinimalTextPart = parent->StringWidth("M") * 5.0; float minimalWidth = iconSize.Width() + trailingIconsWidth + sTextMargin + sMinimalTextPart; if (rect.Width() <= minimalWidth) trailingIconBitmaps.clear(); } // Calculate the location of the icon. iconRect = BRect(BPoint(rect.left + sTextMargin, rect.top + ((rect.Height() - iconSize.Height()) / 2) - 1), iconSize); // Calculate the location of the title text. titleRect = rect; titleRect.left = iconRect.right; titleRect.right -= trailingIconsWidth; // Figure out if the text needs to be truncated. float textWidth = titleRect.Width() - (2.0 * textMargin); BString truncatedString(packageIconAndTitleField->String()); parent->TruncateString(&truncatedString, fTruncateMode, textWidth); packageIconAndTitleField->SetClippedString(truncatedString.String()); titleTextWidth = parent->StringWidth(truncatedString); // Draw the icon. PackageIconRepositoryRef iconRepository = fModel->IconRepository(); status_t bitmapResult = B_OK; BitmapHolderRef bitmapHolderRef; if (iconRepository.IsSet()) { BString packageName = packageIconAndTitleField->PackageName(); bitmapResult = iconRepository->GetIcon(packageName, iconSize.Width() + 1, bitmapHolderRef); } else { bitmapResult = B_NOT_INITIALIZED; } if (bitmapResult == B_OK) { if (bitmapHolderRef.IsSet()) { const BBitmap* bitmap = bitmapHolderRef->Bitmap(); if (bitmap != NULL && bitmap->IsValid()) { parent->SetDrawingMode(B_OP_ALPHA); parent->DrawBitmap(bitmap, bitmap->Bounds(), iconRect, B_FILTER_BITMAP_BILINEAR); parent->SetDrawingMode(B_OP_OVER); } } } // Draw the title. DrawString(packageIconAndTitleField->ClippedString(), parent, titleRect); // Draw the trailing icons if (!trailingIconBitmaps.empty()) { BRect trailingIconRect( BPoint(titleRect.left + titleTextWidth + textMargin, iconRect.top), trailingIconSize); parent->SetDrawingMode(B_OP_ALPHA); for (std::vector::iterator it = trailingIconBitmaps.begin(); it != trailingIconBitmaps.end(); it++) { const BBitmap* bitmap = (*it)->Bitmap(); BRect bitmapBounds = bitmap->Bounds(); BRect trailingIconAlignedRect = BRect(BPoint(ceilf(trailingIconRect.LeftTop().x) + 0.5, ceilf(trailingIconRect.LeftTop().y) + 0.5), trailingIconRect.Size()); parent->DrawBitmap(bitmap, bitmapBounds, trailingIconAlignedRect, B_FILTER_BITMAP_BILINEAR); trailingIconRect.OffsetBy( trailingIconSize.Width() * (1.0 + trailingIconPaddingFactor), 0); } parent->SetDrawingMode(B_OP_OVER); } } else if (stringField != NULL) { float width = rect.Width() - (2 * sTextMargin); if (width != stringField->Width()) { BString truncatedString(stringField->String()); parent->TruncateString(&truncatedString, fTruncateMode, width + 2); stringField->SetClippedString(truncatedString.String()); stringField->SetWidth(width); } DrawString(stringField->ClippedString(), parent, rect); } else if (lazyStringField != NULL) { float width = rect.Width() - (2 * sTextMargin); if (width != lazyStringField->Width()) { BString truncatedString(lazyStringField->String()); parent->TruncateString(&truncatedString, fTruncateMode, width + 2); lazyStringField->SetClippedString(truncatedString.String()); lazyStringField->SetWidth(width); } DrawString(lazyStringField->ClippedString(), parent, rect); } else if (ratingField != NULL) { float width = rect.Width(); float padding = be_control_look->ComposeSpacing(B_USE_SMALL_SPACING); bool isRatingValid = ratingField->Rating() >= RATING_MIN; if (!isRatingValid || width < fRatingsMetrics->Size().Width() + padding * 2.0) { BString ratingAsText = "-"; if (isRatingValid) ratingAsText.SetToFormat("%.1f", ratingField->Rating()); float ratingAsTextWidth = parent->StringWidth(ratingAsText); if (ratingAsTextWidth + padding * 2.0 < width) { font_height fontHeight; parent->GetFontHeight(&fontHeight); float fullHeight = fontHeight.ascent + fontHeight.descent; float y = rect.top + (rect.Height() - fullHeight) / 2 + fontHeight.ascent; parent->DrawString(ratingAsText, BPoint(rect.left + padding, y)); } } else { const BBitmap* starIcon = SharedIcons::IconStarBlue12Scaled()->Bitmap(); float ratingsStarsHeight = fRatingsMetrics->Size().Height(); BPoint starsPt(floorf(rect.LeftTop().x + padding), floorf(rect.LeftTop().y + (rect.Size().Height() / 2.0) - ratingsStarsHeight / 2.0)); RatingUtils::Draw(parent, starsPt, ratingField->Rating(), starIcon); } } } int PackageColumn::CompareFields(BField* field1, BField* field2) { DateField* dateField1 = dynamic_cast(field1); DateField* dateField2 = dynamic_cast(field2); if (dateField1 != NULL && dateField2 != NULL) { if (dateField1->MillisSinceEpoc() > dateField2->MillisSinceEpoc()) return -1; else if (dateField1->MillisSinceEpoc() < dateField2->MillisSinceEpoc()) return 1; return 0; } SizeField* sizeField1 = dynamic_cast(field1); SizeField* sizeField2 = dynamic_cast(field2); if (sizeField1 != NULL && sizeField2 != NULL) { if (sizeField1->Size() > sizeField2->Size()) return -1; else if (sizeField1->Size() < sizeField2->Size()) return 1; return 0; } BStringField* stringField1 = dynamic_cast(field1); BStringField* stringField2 = dynamic_cast(field2); if (stringField1 != NULL && stringField2 != NULL) { // TODO: Locale aware string compare... not too important if // package names are not translated. return strcasecmp(stringField1->String(), stringField2->String()); } RatingField* ratingField1 = dynamic_cast(field1); RatingField* ratingField2 = dynamic_cast(field2); if (ratingField1 != NULL && ratingField2 != NULL) { if (ratingField1->Rating() > ratingField2->Rating()) return -1; else if (ratingField1->Rating() < ratingField2->Rating()) return 1; return 0; } return Inherited::CompareFields(field1, field2); } float PackageColumn::GetPreferredWidth(BField* _field, BView* parent) const { PackageIconAndTitleField* packageIconAndTitleField = dynamic_cast(_field); BStringField* stringField = dynamic_cast(_field); LazyStringField* lazyStringField = dynamic_cast(_field); float parentWidth = Inherited::GetPreferredWidth(_field, parent); float width = 0.0; if (packageIconAndTitleField) { BFont font; parent->GetFont(&font); width = font.StringWidth(packageIconAndTitleField->String()) + 3 * sTextMargin; width += 16; // for the icon; always 16px } else if (stringField) { BFont font; parent->GetFont(&font); width = font.StringWidth(stringField->String()) + 2 * sTextMargin; } else if (lazyStringField) { BFont font; parent->GetFont(&font); width = font.StringWidth(lazyStringField->String()) + 2 * sTextMargin; } return max_c(width, parentWidth); } bool PackageColumn::AcceptsField(const BField* field) const { return dynamic_cast(field) != NULL || dynamic_cast(field) != NULL || dynamic_cast(field) != NULL; } void PackageColumn::InitTextMargin(BView* parent) { BFont font; parent->GetFont(&font); sTextMargin = ceilf(font.Size() * 0.8); } // #pragma mark - PackageRow enum { kTitleColumn, kRatingColumn, kDescriptionColumn, kSizeColumn, kStatusColumn, kRepositoryColumn, kVersionColumn, kVersionCreateTimestampColumn, }; PackageRow::PackageRow(const PackageInfoRef& packageRef) : Inherited(ceilf(be_plain_font->Size() * 1.8f)), fPackage(packageRef), fNextInHash(NULL) { if (!packageRef.IsSet()) return; // Package icon and title // NOTE: The icon BBitmap is referenced by the fPackage member. UpdateIconAndTitle(); UpdateRating(); UpdateSummary(); UpdateSize(); UpdateState(); UpdateRepository(); UpdateVersion(); UpdateVersionCreateTimestamp(); } PackageRow::~PackageRow() { } void PackageRow::SetPackage(const PackageInfoRef& value) { fPackage = value; } void PackageRow::UpdateIconAndTitle() { if (!fPackage.IsSet()) return; PackageIconAndTitleField* existingField = static_cast(GetField(kTitleColumn)); BString packageName = fPackage->Name(); BString title; PackageUtils::TitleOrName(fPackage, title); bool isActivated = PackageUtils::State(fPackage) == ACTIVATED; bool isNativeDesktop = PackageUtils::IsNativeDesktop(fPackage); if (existingField != NULL && existingField->PackageName() == packageName && existingField->String() == title && existingField->IsActivated() == isActivated && existingField->IsNativeDesktop() == isNativeDesktop) { return; } BField* field = new PackageIconAndTitleField(packageName, title, isActivated, isNativeDesktop); SetField(field, kTitleColumn); } void PackageRow::UpdateState() { if (!fPackage.IsSet()) return; BStringField* existingStringField = static_cast(GetField(kStatusColumn)); BString updatedStatus = package_state_to_string(fPackage); if (existingStringField != NULL && existingStringField->String() == updatedStatus) return; SetField(new BStringField(updatedStatus), kStatusColumn); } void PackageRow::UpdateSummary() { if (!fPackage.IsSet()) return; BStringField* existingStringField = static_cast(GetField(kDescriptionColumn)); BString updatedSummary; PackageUtils::Summary(fPackage, updatedSummary); if (existingStringField != NULL && existingStringField->String() == updatedSummary) return; // TODO; `kDescriptionColumn` seems wrong here? SetField(new BStringField(updatedSummary), kDescriptionColumn); } void PackageRow::UpdateRating() { if (!fPackage.IsSet()) return; PackageUserRatingInfoRef userRatingInfo = fPackage->UserRatingInfo(); UserRatingSummaryRef userRatingSummary; float averageRating = RATING_MISSING; if (userRatingInfo.IsSet()) userRatingSummary = userRatingInfo->Summary(); if (userRatingSummary.IsSet()) averageRating = userRatingSummary->AverageRating(); RatingField* existingRatingField = static_cast(GetField(kRatingColumn)); if (existingRatingField != NULL && existingRatingField->Rating() == averageRating) return; SetField(new RatingField(averageRating), kRatingColumn); } void PackageRow::UpdateSize() { if (!fPackage.IsSet()) return; SizeField* existingSizeField = static_cast(GetField(kSizeColumn)); double updatedSize = static_cast(PackageUtils::Size(fPackage)); if (existingSizeField != NULL && existingSizeField->Size() == updatedSize) return; SetField(new SizeField(updatedSize), kSizeColumn); } void PackageRow::UpdateRepository() { if (!fPackage.IsSet()) return; BStringField* existingDepotNameField = static_cast(GetField(kRepositoryColumn)); BString depotName = PackageUtils::DepotName(fPackage); if (existingDepotNameField != NULL && existingDepotNameField->String() == depotName) return; SetField(new BStringField(depotName), kRepositoryColumn); } void PackageRow::UpdateVersion() { if (!fPackage.IsSet()) return; PackageVersionRef version = PackageUtils::Version(fPackage); BString versionString; if (version.IsSet()) versionString = version->ToString(); BStringField* existingStringField = static_cast(GetField(kVersionColumn)); if (existingStringField != NULL && existingStringField->String() == versionString) return; SetField(new BStringField(versionString), kVersionColumn); } void PackageRow::UpdateVersionCreateTimestamp() { if (!fPackage.IsSet()) return; DateField* existingDateField = static_cast(GetField(kVersionCreateTimestampColumn)); PackageVersionRef version = PackageUtils::Version(fPackage); uint64 millisSinceEpoc = 0; if (version.IsSet()) millisSinceEpoc = version->CreateTimestamp(); if (existingDateField != NULL && existingDateField->MillisSinceEpoc() == millisSinceEpoc) return; SetField(new DateField(millisSinceEpoc), kVersionCreateTimestampColumn); } // #pragma mark - ItemCountView class PackageListView::ItemCountView : public BView { public: ItemCountView() : BView("item count view", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE), fItemCount(0), fInvalidated(false) { BFont font(be_plain_font); font.SetSize(font.Size() * 0.75f); SetFont(&font); SetViewUIColor(B_PANEL_BACKGROUND_COLOR); SetLowUIColor(ViewUIColor()); SetHighUIColor(LowUIColor(), B_DARKEN_4_TINT); // constantly calculating the size is expensive so here a sensible // upper limit on the number of packages is arbitrarily chosen. fMinSize = BSize(StringWidth(_DeriveLabel(999999)) + 10, be_control_look->GetScrollBarWidth()); } virtual BSize MinSize() { return fMinSize; } virtual BSize PreferredSize() { return MinSize(); } virtual BSize MaxSize() { return MinSize(); } virtual void Draw(BRect updateRect) { if (fInvalidated) { fLabel = _DeriveLabel(fItemCount); fInvalidated = false; } FillRect(updateRect, B_SOLID_LOW); font_height fontHeight; GetFontHeight(&fontHeight); BRect bounds(Bounds()); float width = StringWidth(fLabel); BPoint offset; offset.x = bounds.left + (bounds.Width() - width) / 2.0f; offset.y = bounds.top + (bounds.Height() - (fontHeight.ascent + fontHeight.descent)) / 2.0f + fontHeight.ascent; DrawString(fLabel, offset); } void SetItemCount(int32 count) { if (count == fItemCount) return; fItemCount = count; if (!fInvalidated) { Invalidate(); fInvalidated = true; } } private: /*! This method is hit quite often when the list of packages in the table-view are updated. Derivation of the plural for some languages such as Russian can be slow so this method should be called sparingly. */ BString _DeriveLabel(int32 count) const { static BStringFormat format(B_TRANSLATE("{0, plural, one{# item} other{# items}}")); BString label; format.Format(label, count); return label; } private: int32 fItemCount; BString fLabel; BSize fMinSize; bool fInvalidated; }; // #pragma mark - PackageListView::RowByNameHashDefinition struct PackageListView::RowByNameHashDefinition { typedef const char* KeyType; typedef PackageRow ValueType; size_t HashKey(const char* key) const { return BString::HashValue(key); } size_t Hash(PackageRow* value) const { return HashKey(value->Package()->Name().String()); } bool Compare(const char* key, PackageRow* value) const { return value->Package()->Name() == key; } ValueType*& GetLink(PackageRow* value) const { return value->NextInHash(); } }; // #pragma mark - PackageListView PackageListView::PackageListView(Model* model) : BColumnListView(B_TRANSLATE("All packages"), 0, B_FANCY_BORDER, true), fModel(model), fRowByNameTable(new RowByNameTable()), fWorkStatusView(NULL), fIgnoreSelectionChanged(false) { float scale = be_plain_font->Size() / 12.f; float spacing = be_control_look->DefaultItemSpacing() * 2; AddColumn(new PackageColumn(fModel, B_TRANSLATE("Name"), 150 * scale, 50 * scale, 300 * scale, B_TRUNCATE_MIDDLE), kTitleColumn); AddColumn(new PackageColumn(fModel, B_TRANSLATE("Rating"), 80 * scale, 50 * scale, 100 * scale, B_TRUNCATE_MIDDLE), kRatingColumn); AddColumn(new PackageColumn(fModel, B_TRANSLATE("Description"), 300 * scale, 80 * scale, 1000 * scale, B_TRUNCATE_MIDDLE), kDescriptionColumn); PackageColumn* sizeColumn = new PackageColumn(fModel, B_TRANSLATE("Size"), spacing + StringWidth("9999.99 KiB"), 50 * scale, 140 * scale, B_TRUNCATE_END); sizeColumn->SetAlignment(B_ALIGN_RIGHT); AddColumn(sizeColumn, kSizeColumn); AddColumn(new PackageColumn(fModel, B_TRANSLATE("Status"), spacing + StringWidth(B_TRANSLATE("Available")), 60 * scale, 140 * scale, B_TRUNCATE_END), kStatusColumn); AddColumn(new PackageColumn(fModel, B_TRANSLATE("Repository"), 120 * scale, 50 * scale, 200 * scale, B_TRUNCATE_MIDDLE), kRepositoryColumn); SetColumnVisible(kRepositoryColumn, false); // invisible by default float widthWithPlacboVersion = spacing + StringWidth("8.2.3176-2"); // average sort of version length as model AddColumn(new PackageColumn(fModel, B_TRANSLATE("Version"), widthWithPlacboVersion, widthWithPlacboVersion, widthWithPlacboVersion + (50 * scale), B_TRUNCATE_MIDDLE), kVersionColumn); float widthWithPlaceboDate = spacing + StringWidth(LocaleUtils::TimestampToDateString(static_cast(1000))); AddColumn(new PackageColumn(fModel, B_TRANSLATE("Date"), widthWithPlaceboDate, widthWithPlaceboDate, widthWithPlaceboDate + (50 * scale), B_TRUNCATE_END), kVersionCreateTimestampColumn); fItemCountView = new ItemCountView(); AddStatusView(fItemCountView); } PackageListView::~PackageListView() { Clear(); } void PackageListView::AttachedToWindow() { BColumnListView::AttachedToWindow(); PackageColumn::InitTextMargin(ScrollView()); } void PackageListView::AllAttached() { BColumnListView::AllAttached(); SetSortingEnabled(true); SetSortColumn(ColumnAt(0), false, true); } void PackageListView::HandleIconsChanged() { // Go through the rows and in each case where there is an icon for the // package then update that row. for (int32 i = CountRows() - 1; i >= 0; i--) { PackageRow* row = static_cast(RowAt(i)); InvalidateRow(row); } } void PackageListView::HandlePackagesChanged(const PackageInfoEvents& events) { for (int32 i = events.CountEvents() - 1; i >= 0; i--) _HandlePackageChanged(events.EventAtIndex(i)); } void PackageListView::_HandlePackageChanged(const PackageInfoEvent& event) { uint32 changes = event.Changes(); PackageInfoRef package = event.Package(); if (!package.IsSet() || 0 == changes) return; PackageRow* row = _FindRow(package->Name()); if (row != NULL) { PackageInfoRef previousPackage = row->Package(); row->SetPackage(package); if ((changes & PKG_CHANGED_LOCALIZED_TEXT) != 0 || (changes & PKG_CHANGED_LOCAL_INFO) != 0) { row->UpdateIconAndTitle(); row->UpdateSummary(); } if ((changes & PKG_CHANGED_RATINGS) != 0) row->UpdateRating(); if ((changes & PKG_CHANGED_LOCAL_INFO) != 0) { row->UpdateState(); row->UpdateSize(); } if ((changes & PKG_CHANGED_CORE_INFO) != 0) row->UpdateVersionCreateTimestamp(); } } void PackageListView::SelectionChanged() { BColumnListView::SelectionChanged(); if (fIgnoreSelectionChanged) return; BMessage message(MSG_PACKAGE_SELECTED); PackageRow* selected = dynamic_cast(CurrentSelection()); if (selected != NULL) message.AddString("name", selected->Package()->Name()); Window()->PostMessage(&message); } void PackageListView::Clear() { fItemCountView->SetItemCount(0); BColumnListView::Clear(); fRowByNameTable->Clear(); } /*! Only keep the packages in the supplied list and remove all others. */ void PackageListView::RetainPackages(const std::vector& packages) { if (packages.empty()) { Clear(); fRowByNameTable->Clear(); return; } std::vector::const_iterator it; std::set packagesNames; std::vector removedPackages; for (it = packages.begin(); it != packages.end(); it++) { PackageInfoRef package = *it; packagesNames.insert(package->Name()); } for (int32 i = CountRows() - 1; i >= 0; i--) { PackageRow* row = static_cast(RowAt(i)); PackageInfoRef package = row->Package(); if (packagesNames.find(package->Name()) == packagesNames.end()) removedPackages.push_back(package); } AddRemovePackages(packages, removedPackages); } void PackageListView::AddRemovePackages(const std::vector& addedPackages, const std::vector& removedPackages) { std::vector::const_iterator it; if (!addedPackages.empty()) { BList addedRows; for (it = addedPackages.begin(); it != addedPackages.end(); it++) { PackageInfoRef package = *it; PackageRow* packageRow = _FindRow(package); if (packageRow == NULL) { packageRow = new PackageRow(package); addedRows.AddItem(packageRow); fRowByNameTable->Insert(packageRow); } } if (!addedRows.IsEmpty()) { AddRows(&addedRows, -1, NULL); BRow* row = static_cast(addedRows.ItemAt(0)); ExpandOrCollapse(row, true); } } if (!removedPackages.empty()) { BList removedRows; for (it = removedPackages.begin(); it != removedPackages.end(); it++) { PackageInfoRef package = *it; PackageRow* packageRow = _FindRow(package); if (packageRow != NULL) removedRows.AddItem(packageRow); } if (!removedRows.IsEmpty()) RemoveRows(&removedRows); for (int32 i = removedRows.CountItems() - 1; i >= 0; i--) { PackageRow* packageRow = static_cast(removedRows.ItemAt(i)); fRowByNameTable->Remove(packageRow); delete packageRow; } } fItemCountView->SetItemCount(CountRows()); } void PackageListView::_AddPackage(const PackageInfoRef& package) { PackageRow* packageRow = _FindRow(package); // forget about it if this package is already in the listview if (packageRow != NULL) return; // create the row for this package packageRow = new PackageRow(package); // add the row, parent may be NULL (add at top level) AddRow(packageRow); // add to hash table for quick lookup of row by package name fRowByNameTable->Insert(packageRow); // make sure the row is initially expanded ExpandOrCollapse(packageRow, true); } void PackageListView::_RemovePackage(const PackageInfoRef& package) { PackageRow* packageRow = _FindRow(package); if (packageRow == NULL) return; fRowByNameTable->Remove(packageRow); RemoveRow(packageRow); delete packageRow; } void PackageListView::SelectPackage(const PackageInfoRef& package) { fIgnoreSelectionChanged = true; PackageRow* row = _FindRow(package); BRow* selected = CurrentSelection(); if (row != selected) DeselectAll(); if (row != NULL) { AddToSelection(row); SetFocusRow(row, false); ScrollTo(row); } fIgnoreSelectionChanged = false; } void PackageListView::AttachWorkStatusView(WorkStatusView* view) { fWorkStatusView = view; } PackageRow* PackageListView::_FindRow(const PackageInfoRef& package) { if (!package.IsSet()) return NULL; return fRowByNameTable->Lookup(package->Name().String()); } PackageRow* PackageListView::_FindRow(const BString& packageName) { return fRowByNameTable->Lookup(packageName.String()); }