1 /* 2 * Copyright 2013-214, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>. 4 * Copyright 2020, Andrew Lindesay <apl@lindesay.co.nz>. 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 8 #include "FeaturedPackagesView.h" 9 10 #include <stdio.h> 11 12 #include <Catalog.h> 13 #include <Font.h> 14 #include <LayoutBuilder.h> 15 #include <Message.h> 16 #include <ScrollView.h> 17 #include <StringView.h> 18 #include <SpaceLayoutItem.h> 19 20 #include "BitmapView.h" 21 #include "HaikuDepotConstants.h" 22 #include "MainWindow.h" 23 #include "MarkupTextView.h" 24 #include "MessagePackageListener.h" 25 #include "RatingView.h" 26 #include "ScrollableGroupView.h" 27 28 29 #undef B_TRANSLATION_CONTEXT 30 #define B_TRANSLATION_CONTEXT "FeaturedPackagesView" 31 32 33 static BitmapRef sInstalledIcon(new(std::nothrow) 34 SharedBitmap(RSRC_INSTALLED), true); 35 36 37 // #pragma mark - PackageView 38 39 40 class PackageView : public BGroupView { 41 public: 42 PackageView() 43 : 44 BGroupView("package view", B_HORIZONTAL), 45 fPackageListener( 46 new(std::nothrow) OnePackageMessagePackageListener(this)), 47 fSelected(false) 48 { 49 SetViewUIColor(B_LIST_BACKGROUND_COLOR); 50 SetHighUIColor(B_LIST_ITEM_TEXT_COLOR); 51 SetEventMask(B_POINTER_EVENTS); 52 53 // Featured icon package should be scaled to 64x64 54 fIconView = new BitmapView("package icon view"); 55 fIconView->SetExplicitMinSize(BSize(64, 64)); 56 57 fInstalledIconView = new BitmapView("installed icon view"); 58 fTitleView = new BStringView("package title view", ""); 59 fPublisherView = new BStringView("package publisher view", ""); 60 61 // Title font 62 BFont font; 63 GetFont(&font); 64 font_family family; 65 font_style style; 66 font.SetSize(ceilf(font.Size() * 1.8f)); 67 font.GetFamilyAndStyle(&family, &style); 68 font.SetFamilyAndStyle(family, "Bold"); 69 fTitleView->SetFont(&font); 70 71 // Publisher font 72 GetFont(&font); 73 font.SetSize(std::max(9.0f, floorf(font.Size() * 0.92f))); 74 font.SetFamilyAndStyle(family, "Italic"); 75 fPublisherView->SetFont(&font); 76 77 // Summary text view 78 fSummaryView = new BTextView("package summary"); 79 fSummaryView->MakeSelectable(false); 80 fSummaryView->MakeEditable(false); 81 font = BFont(be_plain_font); 82 rgb_color color = HighColor(); 83 fSummaryView->SetFontAndColor(&font, B_FONT_ALL, &color); 84 85 // Rating view 86 fRatingView = new RatingView("package rating view"); 87 88 fAvgRating = new BStringView("package average rating", ""); 89 fAvgRating->SetFont(&font); 90 91 fVoteInfo = new BStringView("package vote info", ""); 92 // small font 93 GetFont(&font); 94 font.SetSize(std::max(9.0f, floorf(font.Size() * 0.85f))); 95 fVoteInfo->SetFont(&font); 96 fVoteInfo->SetHighUIColor(HighUIColor()); 97 98 BLayoutBuilder::Group<>(this) 99 .Add(fIconView) 100 .AddGroup(B_VERTICAL, 1.0f, 2.2f) 101 .AddGroup(B_HORIZONTAL) 102 .Add(fTitleView) 103 .Add(fInstalledIconView) 104 .AddGlue() 105 .End() 106 .Add(fPublisherView) 107 .SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)) 108 .End() 109 .AddGlue(0.1f) 110 .AddGroup(B_HORIZONTAL, 0.8f) 111 .Add(fRatingView) 112 .Add(fAvgRating) 113 .Add(fVoteInfo) 114 .End() 115 .AddGlue(0.2f) 116 .Add(fSummaryView, 2.0f) 117 118 .SetInsets(B_USE_WINDOW_INSETS) 119 ; 120 121 Clear(); 122 } 123 124 virtual ~PackageView() 125 { 126 fPackageListener->SetPackage(PackageInfoRef(NULL)); 127 fPackageListener->ReleaseReference(); 128 } 129 130 virtual void AllAttached() 131 { 132 for (int32 index = 0; index < CountChildren(); ++index) { 133 ChildAt(index)->SetViewUIColor(ViewUIColor()); 134 ChildAt(index)->SetLowUIColor(ViewUIColor()); 135 ChildAt(index)->SetHighUIColor(HighUIColor()); 136 } 137 } 138 139 virtual void MessageReceived(BMessage* message) 140 { 141 switch (message->what) { 142 case B_MOUSE_WHEEL_CHANGED: 143 Window()->PostMessage(message, Parent()); 144 break; 145 146 case MSG_UPDATE_PACKAGE: 147 { 148 uint32 changes = 0; 149 if (message->FindUInt32("changes", &changes) != B_OK) 150 break; 151 UpdatePackage(changes, fPackageListener->Package()); 152 break; 153 } 154 155 case B_COLORS_UPDATED: 156 { 157 if (message->HasColor(ui_color_name(B_LIST_ITEM_TEXT_COLOR))) 158 _UpdateColors(); 159 break; 160 } 161 162 default: 163 BView::MessageReceived(message); 164 break; 165 } 166 } 167 168 virtual void MouseDown(BPoint where) 169 { 170 BRect bounds = Bounds(); 171 BRect parentBounds = Parent()->Bounds(); 172 ConvertFromParent(&parentBounds); 173 bounds = bounds & parentBounds; 174 175 if (bounds.Contains(where) && Window()->IsActive() && !IsHidden()) { 176 BMessage message(MSG_PACKAGE_SELECTED); 177 message.AddString("name", PackageName()); 178 Window()->PostMessage(&message); 179 } 180 } 181 182 void SetPackage(const PackageInfoRef& package) 183 { 184 fPackageListener->SetPackage(package); 185 186 _SetIcon(package->Icon()); 187 _SetInstalled(package->State() == ACTIVATED); 188 189 fTitleView->SetText(package->Title()); 190 191 BString publisher = package->Publisher().Name(); 192 fPublisherView->SetText(publisher); 193 194 BString summary = package->ShortDescription(); 195 fSummaryView->SetText(summary); 196 197 _SetRating(package->CalculateRatingSummary()); 198 199 InvalidateLayout(); 200 Invalidate(); 201 } 202 203 void UpdatePackage(uint32 changeMask, const PackageInfoRef& package) 204 { 205 if ((changeMask & PKG_CHANGED_TITLE) != 0) 206 fTitleView->SetText(package->Title()); 207 if ((changeMask & PKG_CHANGED_SUMMARY) != 0) 208 fSummaryView->SetText(package->ShortDescription()); 209 if ((changeMask & PKG_CHANGED_RATINGS) != 0) 210 _SetRating(package->CalculateRatingSummary()); 211 if ((changeMask & PKG_CHANGED_STATE) != 0) 212 _SetInstalled(package->State() == ACTIVATED); 213 if ((changeMask & PKG_CHANGED_ICON) != 0) 214 _SetIcon(package->Icon()); 215 } 216 217 void Clear() 218 { 219 fPackageListener->SetPackage(PackageInfoRef(NULL)); 220 221 fIconView->UnsetBitmap(); 222 fInstalledIconView->UnsetBitmap(); 223 fTitleView->SetText(""); 224 fPublisherView->SetText(""); 225 fSummaryView->SetText(""); 226 fRatingView->SetRating(-1.0f); 227 fAvgRating->SetText(""); 228 fVoteInfo->SetText(""); 229 } 230 231 const char* PackageTitle() const 232 { 233 return fTitleView->Text(); 234 } 235 236 const char* PackageName() const 237 { 238 if (fPackageListener->Package().Get() != NULL) 239 return fPackageListener->Package()->Name(); 240 else 241 return ""; 242 } 243 244 void SetSelected(bool selected) 245 { 246 if (fSelected == selected) 247 return; 248 fSelected = selected; 249 250 _UpdateColors(); 251 } 252 253 void _UpdateColors() 254 { 255 color_which bgColor = B_LIST_BACKGROUND_COLOR; 256 color_which textColor = B_LIST_ITEM_TEXT_COLOR; 257 258 if (fSelected) { 259 bgColor = B_LIST_SELECTED_BACKGROUND_COLOR; 260 textColor = B_LIST_SELECTED_ITEM_TEXT_COLOR; 261 } 262 263 List<BView*, true> views; 264 265 views.Add(this); 266 views.Add(fIconView); 267 views.Add(fInstalledIconView); 268 views.Add(fTitleView); 269 views.Add(fPublisherView); 270 views.Add(fSummaryView); 271 views.Add(fRatingView); 272 views.Add(fAvgRating); 273 views.Add(fVoteInfo); 274 275 for (int32 i = 0; i < views.CountItems(); i++) { 276 BView* view = views.ItemAtFast(i); 277 278 view->SetViewUIColor(bgColor); 279 view->SetLowUIColor(bgColor); 280 view->SetHighUIColor(textColor); 281 view->Invalidate(); 282 } 283 284 BFont font(be_plain_font); 285 rgb_color color = HighColor(); 286 fSummaryView->SetFontAndColor(&font, B_FONT_ALL, &color); 287 } 288 289 void _SetRating(const RatingSummary& ratingSummary) 290 { 291 fRatingView->SetRating(ratingSummary.averageRating); 292 293 if (ratingSummary.ratingCount > 0) { 294 BString avgRating; 295 avgRating.SetToFormat("%.1f", ratingSummary.averageRating); 296 fAvgRating->SetText(avgRating); 297 298 BString votes; 299 votes.SetToFormat("%d", ratingSummary.ratingCount); 300 301 BString voteInfo(B_TRANSLATE("(%Votes%)")); 302 voteInfo.ReplaceAll("%Votes%", votes); 303 304 fVoteInfo->SetText(voteInfo); 305 } else { 306 fAvgRating->SetText(""); 307 fVoteInfo->SetText(""); 308 } 309 } 310 311 void _SetInstalled(bool installed) 312 { 313 if (installed) { 314 fInstalledIconView->SetBitmap(sInstalledIcon, 315 SharedBitmap::SIZE_16); 316 } else 317 fInstalledIconView->UnsetBitmap(); 318 } 319 320 void _SetIcon(const BitmapRef& icon) 321 { 322 if (icon.Get() != NULL) { 323 fIconView->SetBitmap(icon, SharedBitmap::SIZE_64); 324 } else 325 fIconView->UnsetBitmap(); 326 } 327 328 private: 329 OnePackageMessagePackageListener* fPackageListener; 330 331 BitmapView* fIconView; 332 BitmapView* fInstalledIconView; 333 334 BStringView* fTitleView; 335 BStringView* fPublisherView; 336 337 BTextView* fSummaryView; 338 339 RatingView* fRatingView; 340 BStringView* fAvgRating; 341 BStringView* fVoteInfo; 342 343 bool fSelected; 344 345 BString fPackageName; 346 }; 347 348 349 // #pragma mark - FeaturedPackagesView 350 351 352 FeaturedPackagesView::FeaturedPackagesView() 353 : 354 BView(B_TRANSLATE("Featured packages"), 0) 355 { 356 BGroupLayout* layout = new BGroupLayout(B_VERTICAL); 357 SetLayout(layout); 358 359 fContainerView = new ScrollableGroupView(); 360 fContainerView->SetViewUIColor(B_LIST_BACKGROUND_COLOR); 361 fPackageListLayout = fContainerView->GroupLayout(); 362 363 BScrollView* scrollView = new BScrollView( 364 "featured packages scroll view", fContainerView, 365 0, false, true, B_FANCY_BORDER); 366 367 BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL); 368 if (scrollBar != NULL) 369 scrollBar->SetSteps(10.0f, 20.0f); 370 371 BLayoutBuilder::Group<>(this) 372 .Add(scrollView, 1.0f) 373 ; 374 } 375 376 377 FeaturedPackagesView::~FeaturedPackagesView() 378 { 379 } 380 381 382 /*! This method will add the package into the list to be displayed. The 383 insertion will occur in alphabetical order. 384 */ 385 386 void 387 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 388 { 389 int32 index = _InsertionIndex(package->Name()); 390 if (index != -1) { 391 PackageView* view = new PackageView(); 392 view->SetPackage(package); 393 fPackageListLayout->AddView(index, view); 394 } 395 } 396 397 398 const char* 399 FeaturedPackagesView::_PackageNameAtIndex(int32 index) const 400 { 401 BLayoutItem* item = fPackageListLayout->ItemAt(index); 402 PackageView* view = dynamic_cast<PackageView*>(item->View()); 403 return (view != NULL ? view->PackageName() : NULL); 404 // some of the items in the GroupLayout instance are not of type 405 // PackageView* and it is not immediately clear where they are 406 // coming from. 407 408 } 409 410 411 int32 412 FeaturedPackagesView::_InsertionIndex(const BString& packageName) const 413 { 414 int32 count = fPackageListLayout->CountItems(); 415 return _InsertionIndexBinary(packageName, 0, count - 1); 416 } 417 418 int32 419 FeaturedPackagesView::_InsertionIndexLinear(const BString& packageName, 420 int32 startIndex, int32 endIndex) const 421 { 422 for (int32 i = startIndex; i <= endIndex; i++) { 423 const char* iPackageName = _PackageNameAtIndex(i); 424 if (NULL != iPackageName) { 425 int compare = packageName.Compare(iPackageName); 426 if (compare == 0) 427 return -1; 428 if (compare < 0) 429 return i; 430 } 431 } 432 return endIndex; 433 } 434 435 /*! This performs a binary search to find the location at which to insert 436 the item. 437 */ 438 439 int32 440 FeaturedPackagesView::_InsertionIndexBinary(const BString& packageName, 441 int32 startIndex, int32 endIndex) const 442 { 443 if (startIndex == endIndex) 444 return startIndex; 445 446 int32 endStartSpan = endIndex - startIndex; 447 448 if (endStartSpan < 5) 449 return _InsertionIndexLinear(packageName, startIndex, endIndex); 450 451 int midIndex = startIndex + (endStartSpan / 2); 452 const char *midPackageName = _PackageNameAtIndex(midIndex); 453 454 if (midPackageName == NULL) 455 return _InsertionIndexLinear(packageName, startIndex, endIndex); 456 457 int compare = packageName.Compare(midPackageName); 458 459 if (compare == 0) 460 return -1; 461 // don't want to insert the same package twice. 462 if (compare < 0) 463 return _InsertionIndexBinary(packageName, startIndex, midIndex); 464 return _InsertionIndexBinary(packageName, midIndex, endIndex); 465 } 466 467 468 void 469 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 470 { 471 // Find the package 472 for (int32 i = 0; BLayoutItem* item = fPackageListLayout->ItemAt(i); i++) { 473 PackageView* view = dynamic_cast<PackageView*>(item->View()); 474 if (view == NULL) 475 break; 476 477 BString name = view->PackageName(); 478 if (name == package->Name()) { 479 view->RemoveSelf(); 480 delete view; 481 break; 482 } 483 } 484 } 485 486 487 void 488 FeaturedPackagesView::Clear() 489 { 490 for (int32 i = fPackageListLayout->CountItems() - 1; 491 BLayoutItem* item = fPackageListLayout->ItemAt(i); i--) { 492 BView* view = dynamic_cast<PackageView*>(item->View()); 493 if (view != NULL) { 494 view->RemoveSelf(); 495 delete view; 496 } 497 } 498 } 499 500 501 void 502 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 503 bool scrollToEntry) 504 { 505 BString selectedName; 506 if (package.Get() != NULL) 507 selectedName = package->Name(); 508 509 for (int32 i = 0; BLayoutItem* item = fPackageListLayout->ItemAt(i); i++) { 510 PackageView* view = dynamic_cast<PackageView*>(item->View()); 511 if (view == NULL) 512 break; 513 514 BString name = view->PackageName(); 515 bool match = (name == selectedName); 516 view->SetSelected(match); 517 518 if (match && scrollToEntry) { 519 // Scroll the view so that the package entry shows up in the middle 520 fContainerView->ScrollTo(0, 521 view->Frame().top 522 - fContainerView->Bounds().Height() / 2 523 + view->Bounds().Height() / 2); 524 } 525 } 526 } 527 528 529 void 530 FeaturedPackagesView::CleanupIcons() 531 { 532 sInstalledIcon.Unset(); 533 } 534