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