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