1 /* 2 * Copyright 2013-214, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>. 4 * Copyright 2020-2021, 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 <algorithm> 11 #include <vector> 12 13 #include <Bitmap.h> 14 #include <Catalog.h> 15 #include <Font.h> 16 #include <LayoutBuilder.h> 17 #include <LayoutItem.h> 18 #include <Message.h> 19 #include <ScrollView.h> 20 #include <StringView.h> 21 #include <SpaceLayoutItem.h> 22 23 #include "BitmapView.h" 24 #include "HaikuDepotConstants.h" 25 #include "Logger.h" 26 #include "MainWindow.h" 27 #include "MarkupTextView.h" 28 #include "MessagePackageListener.h" 29 #include "RatingUtils.h" 30 #include "RatingView.h" 31 #include "ScrollableGroupView.h" 32 #include "SharedBitmap.h" 33 34 35 #undef B_TRANSLATION_CONTEXT 36 #define B_TRANSLATION_CONTEXT "FeaturedPackagesView" 37 38 39 #define HEIGHT_PACKAGE 84.0f 40 #define SIZE_ICON 64.0f 41 #define X_POSITION_RATING 350.0f 42 #define X_POSITION_SUMMARY 500.0f 43 #define WIDTH_RATING 100.0f 44 #define Y_PROPORTION_TITLE 0.4f 45 #define Y_PROPORTION_PUBLISHER 0.7f 46 #define PADDING 8.0f 47 48 49 static BitmapRef sInstalledIcon(new(std::nothrow) 50 SharedBitmap(RSRC_INSTALLED), true); 51 52 53 // #pragma mark - PackageView 54 55 56 class StackedFeaturedPackagesView : public BView { 57 public: 58 StackedFeaturedPackagesView(Model& model) 59 : 60 BView("stacked featured packages view", B_WILL_DRAW | B_FRAME_EVENTS), 61 fModel(model), 62 fSelectedIndex(-1), 63 fPackageListener( 64 new(std::nothrow) OnePackageMessagePackageListener(this)), 65 fLowestIndexAddedOrRemoved(-1) 66 { 67 SetEventMask(B_POINTER_EVENTS); 68 Clear(); 69 } 70 71 72 virtual ~StackedFeaturedPackagesView() 73 { 74 fPackageListener->SetPackage(PackageInfoRef(NULL)); 75 fPackageListener->ReleaseReference(); 76 } 77 78 // #pragma mark - message handling and events 79 80 virtual void MessageReceived(BMessage* message) 81 { 82 switch (message->what) { 83 case MSG_UPDATE_PACKAGE: 84 { 85 BString name; 86 if (message->FindString("name", &name) != B_OK) 87 HDINFO("expected 'name' key on package update message"); 88 else 89 _HandleUpdatePackage(name); 90 break; 91 } 92 93 case B_COLORS_UPDATED: 94 { 95 Invalidate(); 96 break; 97 } 98 99 default: 100 BView::MessageReceived(message); 101 break; 102 } 103 } 104 105 106 virtual void MouseDown(BPoint where) 107 { 108 if (Window()->IsActive() && !IsHidden()) { 109 BRect bounds = Bounds(); 110 BRect parentBounds = Parent()->Bounds(); 111 ConvertFromParent(&parentBounds); 112 bounds = bounds & parentBounds; 113 if (bounds.Contains(where)) { 114 _MessageSelectIndex(_IndexOfY(where.y)); 115 MakeFocus(); 116 } 117 } 118 } 119 120 121 virtual void KeyDown(const char* bytes, int32 numBytes) 122 { 123 char key = bytes[0]; 124 125 switch (key) { 126 case B_RIGHT_ARROW: 127 case B_DOWN_ARROW: 128 { 129 int32 lastIndex = static_cast<int32>(fPackages.size()) - 1; 130 if (!IsEmpty() && fSelectedIndex != -1 131 && fSelectedIndex < lastIndex) { 132 _MessageSelectIndex(fSelectedIndex + 1); 133 } 134 break; 135 } 136 case B_LEFT_ARROW: 137 case B_UP_ARROW: 138 if (fSelectedIndex > 0) 139 _MessageSelectIndex( fSelectedIndex - 1); 140 break; 141 case B_PAGE_UP: 142 { 143 BRect bounds = Bounds(); 144 ScrollTo(0, fmaxf(0, bounds.top - bounds.Height())); 145 break; 146 } 147 case B_PAGE_DOWN: 148 { 149 BRect bounds = Bounds(); 150 float height = fPackages.size() * HEIGHT_PACKAGE; 151 float maxScrollY = height - bounds.Height(); 152 float pageDownScrollY = bounds.top + bounds.Height(); 153 ScrollTo(0, fminf(maxScrollY, pageDownScrollY)); 154 break; 155 } 156 default: 157 BView::KeyDown(bytes, numBytes); 158 break; 159 } 160 } 161 162 163 /*! This method will send a message to the Window so that it can signal 164 back to this and other views that a package has been selected. This 165 method won't actually change the state of this view directly. 166 */ 167 168 void _MessageSelectIndex(int32 index) const 169 { 170 if (index != -1) { 171 BMessage message(MSG_PACKAGE_SELECTED); 172 BString packageName = fPackages[index]->Name(); 173 message.AddString("name", packageName); 174 Window()->PostMessage(&message); 175 } 176 } 177 178 179 virtual void FrameResized(float width, float height) 180 { 181 BView::FrameResized(width, height); 182 183 // because the summary text will wrap, a resize of the frame will 184 // result in all of the summary area needing to be redrawn. 185 186 BRect rectToInvalidate = Bounds(); 187 rectToInvalidate.left = X_POSITION_SUMMARY; 188 Invalidate(rectToInvalidate); 189 } 190 191 192 // #pragma mark - update / add / remove / clear data 193 194 195 void UpdatePackage(uint32 changeMask, const PackageInfoRef& package) 196 { 197 // TODO; could optimize the invalidation? 198 int32 index = _IndexOfPackage(package); 199 if (index >= 0) { 200 fPackages[index] = package; 201 Invalidate(_RectOfIndex(index)); 202 } 203 } 204 205 206 void Clear() 207 { 208 for (std::vector<PackageInfoRef>::iterator it = fPackages.begin(); 209 it != fPackages.end(); it++) { 210 (*it)->RemoveListener(fPackageListener); 211 } 212 fPackages.clear(); 213 fSelectedIndex = -1; 214 Invalidate(); 215 } 216 217 218 bool IsEmpty() const 219 { 220 return fPackages.size() == 0; 221 } 222 223 224 void _HandleUpdatePackage(const BString& name) 225 { 226 int32 index = _IndexOfName(name); 227 if (index != -1) 228 Invalidate(_RectOfIndex(index)); 229 } 230 231 232 static int _CmpProminences(int64 a, int64 b) 233 { 234 if (a <= 0) 235 a = PROMINANCE_ORDERING_MAX; 236 if (b <= 0) 237 b = PROMINANCE_ORDERING_MAX; 238 if (a == b) 239 return 0; 240 if (a > b) 241 return 1; 242 return -1; 243 } 244 245 246 /*! This method will return true if the packageA is ordered before 247 packageB. 248 */ 249 250 static bool _IsPackageBefore(const PackageInfoRef& packageA, 251 const PackageInfoRef& packageB) 252 { 253 if (!packageA.IsSet() || !packageB.IsSet()) 254 HDFATAL("unexpected NULL reference in a referencable"); 255 int c = _CmpProminences(packageA->Prominence(), packageB->Prominence()); 256 if (c == 0) 257 c = packageA->Title().ICompare(packageB->Title()); 258 if (c == 0) 259 c = packageA->Name().Compare(packageB->Name()); 260 return c < 0; 261 } 262 263 264 void BeginAddRemove() 265 { 266 fLowestIndexAddedOrRemoved = INT32_MAX; 267 } 268 269 270 void EndAddRemove() 271 { 272 if (fLowestIndexAddedOrRemoved < INT32_MAX) { 273 if (fPackages.empty()) 274 Invalidate(); 275 else { 276 BRect invalidRect = Bounds(); 277 invalidRect.top = _YOfIndex(fLowestIndexAddedOrRemoved); 278 Invalidate(invalidRect); 279 } 280 } 281 } 282 283 284 void AddPackage(const PackageInfoRef& package) 285 { 286 // fPackages is sorted and for this reason it is possible to find the 287 // insertion point by identifying the first item in fPackages that does 288 // not return true from the method '_IsPackageBefore'. 289 290 std::vector<PackageInfoRef>::iterator itInsertionPt 291 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 292 &_IsPackageBefore); 293 294 if (itInsertionPt == fPackages.end() 295 || package->Name() != (*itInsertionPt)->Name()) { 296 int32 insertionIndex = 297 std::distance<std::vector<PackageInfoRef>::const_iterator>( 298 fPackages.begin(), itInsertionPt); 299 if (fSelectedIndex >= insertionIndex) 300 fSelectedIndex++; 301 fPackages.insert(itInsertionPt, package); 302 package->AddListener(fPackageListener); 303 if (insertionIndex < fLowestIndexAddedOrRemoved) 304 fLowestIndexAddedOrRemoved = insertionIndex; 305 } 306 } 307 308 309 void RemovePackage(const PackageInfoRef& package) 310 { 311 int32 index = _IndexOfPackage(package); 312 if (index >= 0) { 313 if (fSelectedIndex == index) 314 fSelectedIndex = -1; 315 if (fSelectedIndex > index) 316 fSelectedIndex--; 317 fPackages[index]->RemoveListener(fPackageListener); 318 fPackages.erase(fPackages.begin() + index); 319 if (index < fLowestIndexAddedOrRemoved) 320 fLowestIndexAddedOrRemoved = index; 321 } 322 } 323 324 325 // #pragma mark - selection and index handling 326 327 328 void SelectPackage(const PackageInfoRef& package) 329 { 330 _SelectIndex(_IndexOfPackage(package)); 331 } 332 333 334 void _SelectIndex(int32 index) 335 { 336 if (index != fSelectedIndex) { 337 int32 previousSelectedIndex = fSelectedIndex; 338 fSelectedIndex = index; 339 if (fSelectedIndex >= 0) 340 Invalidate(_RectOfIndex(fSelectedIndex)); 341 if (previousSelectedIndex >= 0) 342 Invalidate(_RectOfIndex(previousSelectedIndex)); 343 _EnsureIndexVisible(index); 344 } 345 } 346 347 348 int32 _IndexOfPackage(PackageInfoRef package) const 349 { 350 std::vector<PackageInfoRef>::const_iterator it 351 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 352 &_IsPackageBefore); 353 354 return (it == fPackages.end() || (*it)->Name() != package->Name()) 355 ? -1 : it - fPackages.begin(); 356 } 357 358 359 int32 _IndexOfName(const BString& name) const 360 { 361 // TODO; slow linear search. 362 // the fPackages is not sorted on name and for this reason it is not 363 // possible to do a binary search. 364 for (uint32 i = 0; i < fPackages.size(); i++) { 365 if (fPackages[i]->Name() == name) 366 return i; 367 } 368 return -1; 369 } 370 371 372 // #pragma mark - drawing and rendering 373 374 375 virtual void Draw(BRect updateRect) 376 { 377 SetHighUIColor(B_LIST_BACKGROUND_COLOR); 378 FillRect(updateRect); 379 380 int32 iStart = _IndexRoundedOfY(updateRect.top); 381 382 if (iStart != -1) { 383 int32 iEnd = _IndexRoundedOfY(updateRect.bottom); 384 for (int32 i = iStart; i <= iEnd; i++) 385 _DrawPackageAtIndex(updateRect, i); 386 } 387 } 388 389 390 void _DrawPackageAtIndex(BRect updateRect, int32 index) 391 { 392 _DrawPackage(updateRect, fPackages[index], index, _YOfIndex(index), 393 index == fSelectedIndex); 394 } 395 396 397 void _DrawPackage(BRect updateRect, PackageInfoRef pkg, int index, float y, 398 bool selected) 399 { 400 if (selected) { 401 SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR); 402 FillRect(_RectOfY(y), B_SOLID_LOW); 403 } else { 404 SetLowUIColor(B_LIST_BACKGROUND_COLOR); 405 } 406 // TODO; optimization; the updateRect may only cover some of this? 407 _DrawPackageIcon(updateRect, pkg, y, selected); 408 _DrawPackageTitle(updateRect, pkg, y, selected); 409 _DrawPackagePublisher(updateRect, pkg, y, selected); 410 _DrawPackageRating(updateRect, pkg, y, selected); 411 _DrawPackageSummary(updateRect, pkg, y, selected); 412 } 413 414 415 void _DrawPackageIcon(BRect updateRect, PackageInfoRef pkg, float y, 416 bool selected) 417 { 418 BitmapRef icon; 419 status_t iconResult = fModel.GetPackageIconRepository().GetIcon( 420 pkg->Name(), BITMAP_SIZE_64, icon); 421 422 if (iconResult == B_OK) { 423 if (icon.IsSet()) { 424 float inset = (HEIGHT_PACKAGE - SIZE_ICON) / 2.0; 425 BRect targetRect = BRect(inset, y + inset, SIZE_ICON + inset, 426 y + SIZE_ICON + inset); 427 const BBitmap* bitmap = icon->Bitmap(BITMAP_SIZE_64); 428 SetDrawingMode(B_OP_ALPHA); 429 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 430 B_FILTER_BITMAP_BILINEAR); 431 } 432 } 433 } 434 435 436 void _DrawPackageTitle(BRect updateRect, PackageInfoRef pkg, float y, 437 bool selected) 438 { 439 static BFont* sFont = NULL; 440 441 if (sFont == NULL) { 442 sFont = new BFont(be_plain_font); 443 GetFont(sFont); 444 font_family family; 445 font_style style; 446 sFont->SetSize(ceilf(sFont->Size() * 1.8f)); 447 sFont->GetFamilyAndStyle(&family, &style); 448 sFont->SetFamilyAndStyle(family, "Bold"); 449 } 450 451 SetDrawingMode(B_OP_COPY); 452 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 453 : B_LIST_ITEM_TEXT_COLOR); 454 SetFont(sFont); 455 BPoint pt(HEIGHT_PACKAGE, y + (HEIGHT_PACKAGE * Y_PROPORTION_TITLE)); 456 DrawString(pkg->Title(), pt); 457 458 if (pkg->State() == ACTIVATED) { 459 const BBitmap* bitmap = sInstalledIcon->Bitmap( 460 BITMAP_SIZE_16); 461 float stringWidth = StringWidth(pkg->Title()); 462 float offsetX = pt.x + stringWidth + PADDING; 463 BRect targetRect(offsetX, pt.y - 16, offsetX + 16, pt.y); 464 SetDrawingMode(B_OP_ALPHA); 465 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 466 B_FILTER_BITMAP_BILINEAR); 467 } 468 } 469 470 471 void _DrawPackagePublisher(BRect updateRect, PackageInfoRef pkg, float y, 472 bool selected) 473 { 474 static BFont* sFont = NULL; 475 476 if (sFont == NULL) { 477 sFont = new BFont(be_plain_font); 478 font_family family; 479 font_style style; 480 sFont->SetSize(std::max(9.0f, floorf(sFont->Size() * 0.92f))); 481 sFont->GetFamilyAndStyle(&family, &style); 482 sFont->SetFamilyAndStyle(family, "Italic"); 483 } 484 485 SetDrawingMode(B_OP_COPY); 486 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 487 : B_LIST_ITEM_TEXT_COLOR); 488 SetFont(sFont); 489 490 float maxTextWidth = (X_POSITION_RATING - HEIGHT_PACKAGE) - PADDING; 491 BString publisherName(pkg->Publisher().Name()); 492 TruncateString(&publisherName, B_TRUNCATE_END, maxTextWidth); 493 494 DrawString(publisherName, BPoint(HEIGHT_PACKAGE, 495 y + (HEIGHT_PACKAGE * Y_PROPORTION_PUBLISHER))); 496 } 497 498 499 // TODO; show the sample size 500 void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y, 501 bool selected) 502 { 503 BPoint at(X_POSITION_RATING, 504 y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f); 505 RatingUtils::Draw(this, at, 506 pkg->CalculateRatingSummary().averageRating); 507 } 508 509 510 // TODO; handle multi-line rendering of the text 511 void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y, 512 bool selected) 513 { 514 BRect bounds = Bounds(); 515 516 SetDrawingMode(B_OP_COPY); 517 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 518 : B_LIST_ITEM_TEXT_COLOR); 519 SetFont(be_plain_font); 520 521 float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING; 522 BString summary(pkg->ShortDescription()); 523 TruncateString(&summary, B_TRUNCATE_END, maxTextWidth); 524 525 DrawString(summary, BPoint(X_POSITION_SUMMARY, 526 y + (HEIGHT_PACKAGE * 0.5))); 527 } 528 529 530 // #pragma mark - geometry and scrolling 531 532 533 /*! This method will make sure that the package at the given index is 534 visible. If the whole of the package can be seen already then it will 535 do nothing. If the package is located above the visible region then it 536 will scroll up to it. If the package is located below the visible 537 region then it will scroll down to it. 538 */ 539 540 void _EnsureIndexVisible(int32 index) 541 { 542 if (!_IsIndexEntirelyVisible(index)) { 543 BRect bounds = Bounds(); 544 int32 indexOfCentreVisible = _IndexOfY( 545 bounds.top + bounds.Height() / 2); 546 if (index < indexOfCentreVisible) 547 ScrollTo(0, _YOfIndex(index)); 548 else { 549 float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE) 550 - bounds.Height(); 551 ScrollTo(0, scrollPointY); 552 } 553 } 554 } 555 556 557 /*! This method will return true if the package at the supplied index is 558 entirely visible. 559 */ 560 561 bool _IsIndexEntirelyVisible(int32 index) 562 { 563 BRect bounds = Bounds(); 564 return bounds == (bounds | _RectOfIndex(index)); 565 } 566 567 568 BRect _RectOfIndex(int32 index) const 569 { 570 if (index < 0) 571 return BRect(0, 0, 0, 0); 572 return _RectOfY(_YOfIndex(index)); 573 } 574 575 576 /*! Provides the top coordinate (offset from the top of view) of the package 577 supplied. If the package does not exist in the view then the coordinate 578 returned will be B_SIZE_UNSET. 579 */ 580 581 float TopOfPackage(const PackageInfoRef& package) 582 { 583 if (package.IsSet()) { 584 int index = _IndexOfPackage(package); 585 if (-1 != index) 586 return _YOfIndex(index); 587 } 588 return B_SIZE_UNSET; 589 } 590 591 592 BRect _RectOfY(float y) const 593 { 594 return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE); 595 } 596 597 598 float _YOfIndex(int32 i) const 599 { 600 return i * HEIGHT_PACKAGE; 601 } 602 603 604 /*! Finds the offset into the list of packages for the y-coord in the view's 605 coordinate space. If the y is above or below the list of packages then 606 this will return -1 to signal this. 607 */ 608 609 int32 _IndexOfY(float y) const 610 { 611 if (fPackages.empty()) 612 return -1; 613 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 614 if (i < 0 || i >= static_cast<int32>(fPackages.size())) 615 return -1; 616 return i; 617 } 618 619 620 /*! Find the offset into the list of packages for the y-coord in the view's 621 coordinate space. If the y is above or below the list of packages then 622 this will return the first or last package index respectively. If there 623 are no packages then this will return -1; 624 */ 625 626 int32 _IndexRoundedOfY(float y) const 627 { 628 if (fPackages.empty()) 629 return -1; 630 int32 i = static_cast<int32>(y / HEIGHT_PACKAGE); 631 if (i < 0) 632 return 0; 633 return std::min(i, (int32) (fPackages.size() - 1)); 634 } 635 636 637 virtual BSize PreferredSize() 638 { 639 return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size()); 640 } 641 642 643 private: 644 Model& fModel; 645 std::vector<PackageInfoRef> 646 fPackages; 647 int32 fSelectedIndex; 648 OnePackageMessagePackageListener* 649 fPackageListener; 650 int32 fLowestIndexAddedOrRemoved; 651 }; 652 653 654 // #pragma mark - FeaturedPackagesView 655 656 657 FeaturedPackagesView::FeaturedPackagesView(Model& model) 658 : 659 BView(B_TRANSLATE("Featured packages"), 0), 660 fModel(model) 661 { 662 fPackagesView = new StackedFeaturedPackagesView(fModel); 663 664 fScrollView = new BScrollView("featured packages scroll view", 665 fPackagesView, 0, false, true, B_FANCY_BORDER); 666 667 BLayoutBuilder::Group<>(this) 668 .Add(fScrollView, 1.0f); 669 } 670 671 672 FeaturedPackagesView::~FeaturedPackagesView() 673 { 674 } 675 676 677 void 678 FeaturedPackagesView::BeginAddRemove() 679 { 680 fPackagesView->BeginAddRemove(); 681 } 682 683 684 void 685 FeaturedPackagesView::EndAddRemove() 686 { 687 fPackagesView->EndAddRemove(); 688 _AdjustViews(); 689 } 690 691 692 /*! This method will add the package into the list to be displayed. The 693 insertion will occur in alphabetical order. 694 */ 695 696 void 697 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 698 { 699 fPackagesView->AddPackage(package); 700 } 701 702 703 void 704 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 705 { 706 fPackagesView->RemovePackage(package); 707 } 708 709 710 void 711 FeaturedPackagesView::Clear() 712 { 713 HDINFO("did clear the featured packages view"); 714 fPackagesView->Clear(); 715 _AdjustViews(); 716 } 717 718 719 void 720 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 721 bool scrollToEntry) 722 { 723 fPackagesView->SelectPackage(package); 724 725 if (scrollToEntry) { 726 float offset = fPackagesView->TopOfPackage(package); 727 if (offset != B_SIZE_UNSET) 728 fPackagesView->ScrollTo(0, offset); 729 } 730 } 731 732 733 void 734 FeaturedPackagesView::DoLayout() 735 { 736 BView::DoLayout(); 737 _AdjustViews(); 738 } 739 740 741 void 742 FeaturedPackagesView::_AdjustViews() 743 { 744 fScrollView->FrameResized(fScrollView->Frame().Width(), 745 fScrollView->Frame().Height()); 746 } 747 748 749 void 750 FeaturedPackagesView::CleanupIcons() 751 { 752 sInstalledIcon.Unset(); 753 } 754