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