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() 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 HDINFO("expected 'name' key on package update message") 86 } 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 /*! This method will return true if the packageA is ordered before 228 packageB. 229 */ 230 231 static bool _IsPackageBefore(const PackageInfoRef& packageA, 232 const PackageInfoRef& packageB) 233 { 234 if (packageA.Get() == NULL || packageB.Get() == NULL) 235 debugger("unexpected NULL reference in a referencable"); 236 int c = packageA->Title().ICompare(packageB->Title()); 237 if (c == 0) 238 c = packageA->Name().Compare(packageB->Name()); 239 return c < 0; 240 } 241 242 void AddPackage(const PackageInfoRef& package) 243 { 244 // fPackages is sorted and for this reason it is possible to find the 245 // insertion point by identifying the first item in fPackages that does 246 // not return true from the method '_IsPackageBefore'. 247 248 std::vector<PackageInfoRef>::iterator itInsertionPt 249 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 250 &_IsPackageBefore); 251 252 if (itInsertionPt == fPackages.end() 253 || package->Name() != (*itInsertionPt)->Name()) { 254 int32 insertionIndex = 255 std::distance<std::vector<PackageInfoRef>::const_iterator>( 256 fPackages.begin(), itInsertionPt); 257 if (fSelectedIndex >= insertionIndex) 258 fSelectedIndex++; 259 fPackages.insert(itInsertionPt, package); 260 Invalidate(_RectOfIndex(insertionIndex) 261 | _RectOfIndex(fPackages.size() - 1)); 262 package->AddListener(fPackageListener); 263 } 264 } 265 266 267 void RemovePackage(const PackageInfoRef& package) 268 { 269 int32 index = _IndexOfPackage(package); 270 if (index >= 0) { 271 if (fSelectedIndex == index) 272 fSelectedIndex = -1; 273 if (fSelectedIndex > index) 274 fSelectedIndex--; 275 fPackages[index]->RemoveListener(fPackageListener); 276 fPackages.erase(fPackages.begin() + index); 277 if (fPackages.empty()) 278 Invalidate(); 279 else { 280 Invalidate(_RectOfIndex(index) 281 | _RectOfIndex(fPackages.size() - 1)); 282 } 283 } 284 } 285 286 287 // #pragma mark - selection and index handling 288 289 290 void SelectPackage(const PackageInfoRef& package) 291 { 292 _SelectIndex(_IndexOfPackage(package)); 293 } 294 295 296 void _SelectIndex(int32 index) 297 { 298 if (index != fSelectedIndex) { 299 int32 previousSelectedIndex = fSelectedIndex; 300 fSelectedIndex = index; 301 if (fSelectedIndex >= 0) 302 Invalidate(_RectOfIndex(fSelectedIndex)); 303 if (previousSelectedIndex >= 0) 304 Invalidate(_RectOfIndex(previousSelectedIndex)); 305 _EnsureIndexVisible(index); 306 } 307 } 308 309 310 int32 _IndexOfPackage(PackageInfoRef package) const 311 { 312 std::vector<PackageInfoRef>::const_iterator it 313 = std::lower_bound(fPackages.begin(), fPackages.end(), package, 314 &_IsPackageBefore); 315 316 return (it == fPackages.end() || (*it)->Name() != package->Name()) 317 ? -1 : it - fPackages.begin(); 318 } 319 320 321 int32 _IndexOfName(const BString& name) const 322 { 323 // TODO; slow linear search. 324 // the fPackages is not sorted on name and for this reason it is not 325 // possible to do a binary search. 326 for (uint32 i = 0; i < fPackages.size(); i++) { 327 if (fPackages[i]->Name() == name) 328 return i; 329 } 330 return -1; 331 } 332 333 334 // #pragma mark - drawing and rendering 335 336 337 virtual void Draw(BRect updateRect) 338 { 339 SetHighUIColor(B_LIST_BACKGROUND_COLOR); 340 FillRect(updateRect); 341 342 int32 iStart = _IndexRoundedOfY(updateRect.top); 343 344 if (iStart != -1) { 345 int32 iEnd = _IndexRoundedOfY(updateRect.bottom); 346 for (int32 i = iStart; i <= iEnd; i++) 347 _DrawPackageAtIndex(updateRect, i); 348 } 349 } 350 351 352 void _DrawPackageAtIndex(BRect updateRect, int32 index) 353 { 354 _DrawPackage(updateRect, fPackages[index], index, _YOfIndex(index), 355 index == fSelectedIndex); 356 } 357 358 359 void _DrawPackage(BRect updateRect, PackageInfoRef pkg, int index, float y, 360 bool selected) 361 { 362 if (selected) { 363 SetLowUIColor(B_LIST_SELECTED_BACKGROUND_COLOR); 364 FillRect(_RectOfY(y), B_SOLID_LOW); 365 } else { 366 SetLowUIColor(B_LIST_BACKGROUND_COLOR); 367 } 368 // TODO; optimization; the updateRect may only cover some of this? 369 _DrawPackageIcon(updateRect, pkg, y, selected); 370 _DrawPackageTitle(updateRect, pkg, y, selected); 371 _DrawPackagePublisher(updateRect, pkg, y, selected); 372 _DrawPackageRating(updateRect, pkg, y, selected); 373 _DrawPackageSummary(updateRect, pkg, y, selected); 374 } 375 376 377 void _DrawPackageIcon(BRect updateRect, PackageInfoRef pkg, float y, 378 bool selected) 379 { 380 BitmapRef icon = pkg->Icon(); 381 382 if (icon.Get() != NULL) { 383 float inset = (HEIGHT_PACKAGE - SIZE_ICON) / 2.0; 384 BRect sourceRect = BRect(0, 0, SIZE_ICON, SIZE_ICON); 385 BRect targetRect = BRect(inset, y + inset, SIZE_ICON + inset, 386 y + SIZE_ICON + inset); 387 const BBitmap* bitmap = icon->Bitmap(SharedBitmap::SIZE_64); 388 SetDrawingMode(B_OP_ALPHA); 389 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 390 B_FILTER_BITMAP_BILINEAR); 391 } 392 } 393 394 395 void _DrawPackageTitle(BRect updateRect, PackageInfoRef pkg, float y, 396 bool selected) 397 { 398 static BFont* sFont = NULL; 399 400 if (sFont == NULL) { 401 sFont = new BFont(be_plain_font); 402 GetFont(sFont); 403 font_family family; 404 font_style style; 405 sFont->SetSize(ceilf(sFont->Size() * 1.8f)); 406 sFont->GetFamilyAndStyle(&family, &style); 407 sFont->SetFamilyAndStyle(family, "Bold"); 408 } 409 410 SetDrawingMode(B_OP_COPY); 411 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 412 : B_LIST_ITEM_TEXT_COLOR); 413 SetFont(sFont); 414 BPoint pt(HEIGHT_PACKAGE, y + (HEIGHT_PACKAGE * Y_PROPORTION_TITLE)); 415 DrawString(pkg->Title(), pt); 416 417 if (pkg->State() == ACTIVATED) { 418 const BBitmap* bitmap = sInstalledIcon->Bitmap( 419 SharedBitmap::SIZE_16); 420 float stringWidth = StringWidth(pkg->Title()); 421 float offsetX = pt.x + stringWidth + PADDING; 422 BRect targetRect(offsetX, pt.y - 16, offsetX + 16, pt.y); 423 SetDrawingMode(B_OP_ALPHA); 424 DrawBitmap(bitmap, bitmap->Bounds(), targetRect, 425 B_FILTER_BITMAP_BILINEAR); 426 } 427 } 428 429 430 void _DrawPackagePublisher(BRect updateRect, PackageInfoRef pkg, float y, 431 bool selected) 432 { 433 static BFont* sFont = NULL; 434 435 if (sFont == NULL) { 436 sFont = new BFont(be_plain_font); 437 font_family family; 438 font_style style; 439 sFont->SetSize(std::max(9.0f, floorf(sFont->Size() * 0.92f))); 440 sFont->GetFamilyAndStyle(&family, &style); 441 sFont->SetFamilyAndStyle(family, "Italic"); 442 } 443 444 SetDrawingMode(B_OP_COPY); 445 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 446 : B_LIST_ITEM_TEXT_COLOR); 447 SetFont(sFont); 448 449 float maxTextWidth = (X_POSITION_RATING - HEIGHT_PACKAGE) - PADDING; 450 BString publisherName(pkg->Publisher().Name()); 451 TruncateString(&publisherName, B_TRUNCATE_END, maxTextWidth); 452 453 DrawString(publisherName, BPoint(HEIGHT_PACKAGE, 454 y + (HEIGHT_PACKAGE * Y_PROPORTION_PUBLISHER))); 455 } 456 457 458 // TODO; show the sample size 459 void _DrawPackageRating(BRect updateRect, PackageInfoRef pkg, float y, 460 bool selected) 461 { 462 BPoint at(X_POSITION_RATING, 463 y + (HEIGHT_PACKAGE - SIZE_RATING_STAR) / 2.0f); 464 RatingUtils::Draw(this, at, 465 pkg->CalculateRatingSummary().averageRating); 466 } 467 468 469 // TODO; handle multi-line rendering of the text 470 void _DrawPackageSummary(BRect updateRect, PackageInfoRef pkg, float y, 471 bool selected) 472 { 473 BRect bounds = Bounds(); 474 475 SetDrawingMode(B_OP_COPY); 476 SetHighUIColor(selected ? B_LIST_SELECTED_ITEM_TEXT_COLOR 477 : B_LIST_ITEM_TEXT_COLOR); 478 SetFont(be_plain_font); 479 480 float maxTextWidth = bounds.Width() - X_POSITION_SUMMARY - PADDING; 481 BString summary(pkg->ShortDescription()); 482 TruncateString(&summary, B_TRUNCATE_END, maxTextWidth); 483 484 DrawString(summary, BPoint(X_POSITION_SUMMARY, 485 y + (HEIGHT_PACKAGE * 0.5))); 486 } 487 488 489 // #pragma mark - geometry and scrolling 490 491 492 /*! This method will make sure that the package at the given index is 493 visible. If the whole of the package can be seen already then it will 494 do nothing. If the package is located above the visible region then it 495 will scroll up to it. If the package is located below the visible 496 region then it will scroll down to it. 497 */ 498 499 void _EnsureIndexVisible(int32 index) 500 { 501 if (!_IsIndexEntirelyVisible(index)) { 502 BRect bounds = Bounds(); 503 int32 indexOfCentreVisible = _IndexOfY( 504 bounds.top + bounds.Height() / 2); 505 if (index < indexOfCentreVisible) 506 ScrollTo(0, _YOfIndex(index)); 507 else { 508 float scrollPointY = (_YOfIndex(index) + HEIGHT_PACKAGE) 509 - bounds.Height(); 510 ScrollTo(0, scrollPointY); 511 } 512 } 513 } 514 515 516 /*! This method will return true if the package at the supplied index is 517 entirely visible. 518 */ 519 520 bool _IsIndexEntirelyVisible(int32 index) 521 { 522 BRect bounds = Bounds(); 523 return bounds == (bounds | _RectOfIndex(index)); 524 } 525 526 527 BRect _RectOfIndex(int32 index) const 528 { 529 if (index < 0) 530 return BRect(0, 0, 0, 0); 531 return _RectOfY(_YOfIndex(index)); 532 } 533 534 535 /*! Provides the top coordinate (offset from the top of view) of the package 536 supplied. If the package does not exist in the view then the coordinate 537 returned will be B_SIZE_UNSET. 538 */ 539 540 float TopOfPackage(const PackageInfoRef& package) 541 { 542 if (package.Get() != NULL) { 543 int index = _IndexOfPackage(package); 544 if (-1 != index) 545 return _YOfIndex(index); 546 } 547 return B_SIZE_UNSET; 548 } 549 550 551 BRect _RectOfY(float y) const 552 { 553 return BRect(0, y, Bounds().Width(), y + HEIGHT_PACKAGE); 554 } 555 556 557 float _YOfIndex(int32 i) const 558 { 559 return i * HEIGHT_PACKAGE; 560 } 561 562 563 /*! Finds the offset into the list of packages for the y-coord in the view's 564 coordinate space. If the y is above or below the list of packages then 565 this will return -1 to signal this. 566 */ 567 568 int32 _IndexOfY(float y) const 569 { 570 if (fPackages.empty()) 571 return -1; 572 int32 i = y / HEIGHT_PACKAGE; 573 if (i < 0 || i >= fPackages.size()) 574 return -1; 575 return i; 576 } 577 578 579 /*! Find the offset into the list of packages for the y-coord in the view's 580 coordinate space. If the y is above or below the list of packages then 581 this will return the first or last package index respectively. If there 582 are no packages then this will return -1; 583 */ 584 585 int32 _IndexRoundedOfY(float y) const 586 { 587 if (fPackages.empty()) 588 return -1; 589 int32 i = y / HEIGHT_PACKAGE; 590 if (i < 0) 591 return 0; 592 return std::min(i, (int32) (fPackages.size() - 1)); 593 } 594 595 596 virtual BSize PreferredSize() 597 { 598 return BSize(B_SIZE_UNLIMITED, HEIGHT_PACKAGE * fPackages.size()); 599 } 600 601 602 private: 603 std::vector<PackageInfoRef> 604 fPackages; 605 int32 fSelectedIndex; 606 607 OnePackageMessagePackageListener* 608 fPackageListener; 609 }; 610 611 612 // #pragma mark - FeaturedPackagesView 613 614 615 FeaturedPackagesView::FeaturedPackagesView() 616 : 617 BView(B_TRANSLATE("Featured packages"), 0) 618 { 619 fPackagesView = new StackedFeaturedPackagesView(); 620 621 fScrollView = new BScrollView("featured packages scroll view", 622 fPackagesView, B_FOLLOW_ALL_SIDES, 0, false, true, B_FANCY_BORDER); 623 624 BLayoutBuilder::Group<>(this) 625 .Add(fScrollView, 1.0f); 626 } 627 628 629 FeaturedPackagesView::~FeaturedPackagesView() 630 { 631 } 632 633 634 /*! This method will add the package into the list to be displayed. The 635 insertion will occur in alphabetical order. 636 */ 637 638 void 639 FeaturedPackagesView::AddPackage(const PackageInfoRef& package) 640 { 641 fPackagesView->AddPackage(package); 642 _AdjustViews(); 643 } 644 645 646 void 647 FeaturedPackagesView::RemovePackage(const PackageInfoRef& package) 648 { 649 fPackagesView->RemovePackage(package); 650 _AdjustViews(); 651 } 652 653 654 void 655 FeaturedPackagesView::Clear() 656 { 657 HDINFO("did clear the featured packages view") 658 fPackagesView->Clear(); 659 _AdjustViews(); 660 } 661 662 663 void 664 FeaturedPackagesView::SelectPackage(const PackageInfoRef& package, 665 bool scrollToEntry) 666 { 667 fPackagesView->SelectPackage(package); 668 669 if (scrollToEntry) { 670 float offset = fPackagesView->TopOfPackage(package); 671 if (offset != B_SIZE_UNSET) 672 fPackagesView->ScrollTo(0, offset); 673 } 674 } 675 676 677 void 678 FeaturedPackagesView::DoLayout() 679 { 680 BView::DoLayout(); 681 _AdjustViews(); 682 } 683 684 685 void 686 FeaturedPackagesView::FrameResized(float width, float height) 687 { 688 BView::FrameResized(width, height); 689 _AdjustViews(); 690 } 691 692 693 void 694 FeaturedPackagesView::_AdjustViews() 695 { 696 BScrollBar* scrollBar = fScrollView->ScrollBar(B_VERTICAL); 697 BSize scrollViewSize = fScrollView->Frame().Size(); 698 float width = scrollViewSize.Width() - (B_V_SCROLL_BAR_WIDTH + 4.0f); 699 // +2 for border; both sides 700 float height = scrollViewSize.Height() - 4.0; 701 // +2 for border; top and bottom 702 703 fPackagesView->ResizeTo(width, height); 704 705 if (fPackagesView->IsEmpty()) { 706 scrollBar->SetRange(0, 0); 707 scrollBar->SetProportion(1); 708 } 709 else { 710 float packagesHeight = fPackagesView->PreferredSize().Height(); 711 float packagesActualHeight = fmaxf(packagesHeight, height); 712 scrollBar->SetRange(0, packagesActualHeight - scrollViewSize.Height()); 713 scrollBar->SetProportion( 714 scrollViewSize.Height() / packagesActualHeight); 715 } 716 } 717 718 719 void 720 FeaturedPackagesView::CleanupIcons() 721 { 722 sInstalledIcon.Unset(); 723 } 724