1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2013, Rene Gollent, <rene@gollent.com>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "PackageListView.h" 8 9 #include <algorithm> 10 #include <stdio.h> 11 12 #include <Autolock.h> 13 #include <Catalog.h> 14 #include <MessageFormat.h> 15 #include <ScrollBar.h> 16 #include <Window.h> 17 18 19 #undef B_TRANSLATION_CONTEXT 20 #define B_TRANSLATION_CONTEXT "PackageListView" 21 22 23 static const char* skPackageStateAvailable = B_TRANSLATE_MARK("Available"); 24 static const char* skPackageStateUninstalled = B_TRANSLATE_MARK("Uninstalled"); 25 static const char* skPackageStateActive = B_TRANSLATE_MARK("Active"); 26 static const char* skPackageStateInactive = B_TRANSLATE_MARK("Inactive"); 27 static const char* skPackageStatePending = B_TRANSLATE_MARK( 28 "Pending" B_UTF8_ELLIPSIS); 29 30 31 inline BString 32 package_state_to_string(PackageInfoRef ref) 33 { 34 switch (ref->State()) { 35 case NONE: 36 return B_TRANSLATE(skPackageStateAvailable); 37 case INSTALLED: 38 return B_TRANSLATE(skPackageStateInactive); 39 case ACTIVATED: 40 return B_TRANSLATE(skPackageStateActive); 41 case UNINSTALLED: 42 return B_TRANSLATE(skPackageStateUninstalled); 43 case DOWNLOADING: 44 { 45 BString data; 46 data.SetToFormat("%3.2f%%", ref->DownloadProgress() * 100.0); 47 return data; 48 } 49 case PENDING: 50 return B_TRANSLATE(skPackageStatePending); 51 } 52 53 return B_TRANSLATE("Unknown"); 54 } 55 56 57 // A field type displaying both a bitmap and a string so that the 58 // tree display looks nicer (both text and bitmap are indented) 59 // TODO: Code-duplication with DriveSetup PartitionList.h 60 class BBitmapStringField : public BStringField { 61 typedef BStringField Inherited; 62 public: 63 BBitmapStringField(const BBitmap* bitmap, 64 const char* string); 65 virtual ~BBitmapStringField(); 66 67 void SetBitmap(const BBitmap* bitmap); 68 const BBitmap* Bitmap() const 69 { return fBitmap; } 70 71 private: 72 const BBitmap* fBitmap; 73 }; 74 75 76 class RatingField : public BField { 77 public: 78 RatingField(float rating); 79 virtual ~RatingField(); 80 81 void SetRating(float rating); 82 float Rating() const 83 { return fRating; } 84 private: 85 float fRating; 86 }; 87 88 89 // BColumn for PackageListView which knows how to render 90 // a BBitmapStringField 91 // TODO: Code-duplication with DriveSetup PartitionList.h 92 class PackageColumn : public BTitledColumn { 93 typedef BTitledColumn Inherited; 94 public: 95 PackageColumn(const char* title, 96 float width, float minWidth, 97 float maxWidth, uint32 truncateMode, 98 alignment align = B_ALIGN_LEFT); 99 100 virtual void DrawField(BField* field, BRect rect, 101 BView* parent); 102 virtual int CompareFields(BField* field1, BField* field2); 103 virtual float GetPreferredWidth(BField* field, 104 BView* parent) const; 105 106 virtual bool AcceptsField(const BField* field) const; 107 108 static void InitTextMargin(BView* parent); 109 110 private: 111 uint32 fTruncateMode; 112 static float sTextMargin; 113 }; 114 115 116 // BRow for the PartitionListView 117 class PackageRow : public BRow { 118 typedef BRow Inherited; 119 public: 120 PackageRow(const PackageInfoRef& package, 121 PackageListener* listener); 122 virtual ~PackageRow(); 123 124 const PackageInfoRef& Package() const 125 { return fPackage; } 126 127 void UpdateTitle(); 128 void UpdateSummary(); 129 void UpdateState(); 130 void UpdateRating(); 131 132 private: 133 PackageInfoRef fPackage; 134 PackageInfoListenerRef fPackageListener; 135 }; 136 137 138 enum { 139 MSG_UPDATE_PACKAGE = 'updp' 140 }; 141 142 143 class PackageListener : public PackageInfoListener { 144 public: 145 PackageListener(PackageListView* view) 146 : 147 fView(view) 148 { 149 } 150 151 virtual ~PackageListener() 152 { 153 } 154 155 virtual void PackageChanged(const PackageInfoEvent& event) 156 { 157 BMessenger messenger(fView); 158 if (!messenger.IsValid()) 159 return; 160 161 const PackageInfo& package = *event.Package().Get(); 162 163 BMessage message(MSG_UPDATE_PACKAGE); 164 message.AddString("title", package.Title()); 165 message.AddUInt32("changes", event.Changes()); 166 167 messenger.SendMessage(&message); 168 } 169 170 private: 171 PackageListView* fView; 172 }; 173 174 175 // #pragma mark - BBitmapStringField 176 177 178 // TODO: Code-duplication with DriveSetup PartitionList.cpp 179 BBitmapStringField::BBitmapStringField(const BBitmap* bitmap, 180 const char* string) 181 : 182 Inherited(string), 183 fBitmap(bitmap) 184 { 185 } 186 187 188 BBitmapStringField::~BBitmapStringField() 189 { 190 } 191 192 193 void 194 BBitmapStringField::SetBitmap(const BBitmap* bitmap) 195 { 196 fBitmap = bitmap; 197 // TODO: cause a redraw? 198 } 199 200 201 // #pragma mark - RatingField 202 203 204 RatingField::RatingField(float rating) 205 : 206 fRating(0.0f) 207 { 208 SetRating(rating); 209 } 210 211 212 RatingField::~RatingField() 213 { 214 } 215 216 217 void 218 RatingField::SetRating(float rating) 219 { 220 if (rating < 0.0f) 221 rating = 0.0f; 222 if (rating > 5.0f) 223 rating = 5.0f; 224 225 if (rating == fRating) 226 return; 227 228 fRating = rating; 229 } 230 231 232 // #pragma mark - PackageColumn 233 234 235 // TODO: Code-duplication with DriveSetup PartitionList.cpp 236 237 238 float PackageColumn::sTextMargin = 0.0; 239 240 241 PackageColumn::PackageColumn(const char* title, float width, float minWidth, 242 float maxWidth, uint32 truncateMode, alignment align) 243 : 244 Inherited(title, width, minWidth, maxWidth, align), 245 fTruncateMode(truncateMode) 246 { 247 SetWantsEvents(true); 248 } 249 250 251 void 252 PackageColumn::DrawField(BField* field, BRect rect, BView* parent) 253 { 254 BBitmapStringField* bitmapField 255 = dynamic_cast<BBitmapStringField*>(field); 256 BStringField* stringField = dynamic_cast<BStringField*>(field); 257 RatingField* ratingField = dynamic_cast<RatingField*>(field); 258 259 if (bitmapField != NULL) { 260 const BBitmap* bitmap = bitmapField->Bitmap(); 261 262 // figure out the placement 263 float x = 0.0; 264 BRect r = bitmap ? bitmap->Bounds() : BRect(0, 0, 15, 15); 265 float y = rect.top + ((rect.Height() - r.Height()) / 2); 266 float width = 0.0; 267 268 switch (Alignment()) { 269 default: 270 case B_ALIGN_LEFT: 271 case B_ALIGN_CENTER: 272 x = rect.left + sTextMargin; 273 width = rect.right - (x + r.Width()) - (2 * sTextMargin); 274 r.Set(x + r.Width(), rect.top, rect.right - width, rect.bottom); 275 break; 276 277 case B_ALIGN_RIGHT: 278 x = rect.right - sTextMargin - r.Width(); 279 width = (x - rect.left - (2 * sTextMargin)); 280 r.Set(rect.left, rect.top, rect.left + width, rect.bottom); 281 break; 282 } 283 284 if (width != bitmapField->Width()) { 285 BString truncatedString(bitmapField->String()); 286 parent->TruncateString(&truncatedString, fTruncateMode, width + 2); 287 bitmapField->SetClippedString(truncatedString.String()); 288 bitmapField->SetWidth(width); 289 } 290 291 // draw the bitmap 292 if (bitmap != NULL) { 293 parent->SetDrawingMode(B_OP_ALPHA); 294 parent->DrawBitmap(bitmap, BPoint(x, y)); 295 parent->SetDrawingMode(B_OP_OVER); 296 } 297 298 // draw the string 299 DrawString(bitmapField->ClippedString(), parent, r); 300 301 } else if (stringField != NULL) { 302 303 float width = rect.Width() - (2 * sTextMargin); 304 305 if (width != stringField->Width()) { 306 BString truncatedString(stringField->String()); 307 308 parent->TruncateString(&truncatedString, fTruncateMode, width + 2); 309 stringField->SetClippedString(truncatedString.String()); 310 stringField->SetWidth(width); 311 } 312 313 DrawString(stringField->ClippedString(), parent, rect); 314 315 } else if (ratingField != NULL) { 316 317 const float kDefaultTextMargin = 8; 318 319 float width = rect.Width() - (2 * kDefaultTextMargin); 320 321 BString string = "★★★★★"; 322 float stringWidth = parent->StringWidth(string); 323 bool drawOverlay = true; 324 325 if (width < stringWidth) { 326 string.SetToFormat("%.1f", ratingField->Rating()); 327 drawOverlay = false; 328 stringWidth = parent->StringWidth(string); 329 } 330 331 switch (Alignment()) { 332 default: 333 case B_ALIGN_LEFT: 334 rect.left += kDefaultTextMargin; 335 break; 336 case B_ALIGN_CENTER: 337 rect.left = rect.left + (width - stringWidth) / 2.0f; 338 break; 339 340 case B_ALIGN_RIGHT: 341 rect.left = rect.right - (stringWidth + kDefaultTextMargin); 342 break; 343 } 344 345 rect.left = floorf(rect.left); 346 rect.right = rect.left + stringWidth; 347 348 if (drawOverlay) 349 parent->SetHighColor(0, 170, 255); 350 351 font_height fontHeight; 352 parent->GetFontHeight(&fontHeight); 353 float y = rect.top + (rect.Height() 354 - (fontHeight.ascent + fontHeight.descent)) / 2 355 + fontHeight.ascent; 356 357 parent->DrawString(string, BPoint(rect.left, y)); 358 359 if (drawOverlay) { 360 rect.left = ceilf(rect.left 361 + (ratingField->Rating() / 5.0f) * rect.Width()); 362 363 rgb_color color = parent->LowColor(); 364 color.alpha = 190; 365 parent->SetHighColor(color); 366 367 parent->SetDrawingMode(B_OP_ALPHA); 368 parent->FillRect(rect, B_SOLID_HIGH); 369 370 } 371 } 372 } 373 374 375 int 376 PackageColumn::CompareFields(BField* field1, BField* field2) 377 { 378 BStringField* stringField1 = dynamic_cast<BStringField*>(field1); 379 BStringField* stringField2 = dynamic_cast<BStringField*>(field2); 380 if (stringField1 != NULL && stringField2 != NULL) { 381 // TODO: Locale aware string compare... not too important if 382 // package names are not translated. 383 return strcmp(stringField1->String(), stringField2->String()); 384 } 385 386 RatingField* ratingField1 = dynamic_cast<RatingField*>(field1); 387 RatingField* ratingField2 = dynamic_cast<RatingField*>(field2); 388 if (ratingField1 != NULL && ratingField2 != NULL) { 389 if (ratingField1->Rating() > ratingField2->Rating()) 390 return -1; 391 else if (ratingField1->Rating() < ratingField2->Rating()) 392 return 1; 393 return 0; 394 } 395 396 return Inherited::CompareFields(field1, field2); 397 } 398 399 400 float 401 PackageColumn::GetPreferredWidth(BField *_field, BView* parent) const 402 { 403 BBitmapStringField* bitmapField 404 = dynamic_cast<BBitmapStringField*>(_field); 405 BStringField* stringField = dynamic_cast<BStringField*>(_field); 406 407 float parentWidth = Inherited::GetPreferredWidth(_field, parent); 408 float width = 0.0; 409 410 if (bitmapField) { 411 const BBitmap* bitmap = bitmapField->Bitmap(); 412 BFont font; 413 parent->GetFont(&font); 414 width = font.StringWidth(bitmapField->String()) + 3 * sTextMargin; 415 if (bitmap) 416 width += bitmap->Bounds().Width(); 417 else 418 width += 16; 419 } else if (stringField) { 420 BFont font; 421 parent->GetFont(&font); 422 width = font.StringWidth(stringField->String()) + 2 * sTextMargin; 423 } 424 return max_c(width, parentWidth); 425 } 426 427 428 bool 429 PackageColumn::AcceptsField(const BField* field) const 430 { 431 return dynamic_cast<const BStringField*>(field) != NULL 432 || dynamic_cast<const RatingField*>(field) != NULL; 433 } 434 435 436 void 437 PackageColumn::InitTextMargin(BView* parent) 438 { 439 BFont font; 440 parent->GetFont(&font); 441 sTextMargin = ceilf(font.Size() * 0.8); 442 } 443 444 445 // #pragma mark - PackageRow 446 447 448 enum { 449 kTitleColumn, 450 kRatingColumn, 451 kDescriptionColumn, 452 kSizeColumn, 453 kStatusColumn, 454 }; 455 456 457 PackageRow::PackageRow(const PackageInfoRef& packageRef, 458 PackageListener* packageListener) 459 : 460 Inherited(ceilf(be_plain_font->Size() * 1.8f)), 461 fPackage(packageRef), 462 fPackageListener(packageListener) 463 { 464 if (packageRef.Get() == NULL) 465 return; 466 467 PackageInfo& package = *packageRef.Get(); 468 469 // Package icon and title 470 // NOTE: The icon BBitmap is referenced by the fPackage member. 471 UpdateTitle(); 472 473 // Rating 474 UpdateRating(); 475 476 // Summary 477 UpdateSummary(); 478 479 // Size 480 // TODO: Store package size 481 SetField(new BStringField("0 KiB"), kSizeColumn); 482 483 // Status 484 SetField(new BStringField(package_state_to_string(fPackage)), 485 kStatusColumn); 486 487 package.AddListener(fPackageListener); 488 } 489 490 491 PackageRow::~PackageRow() 492 { 493 if (fPackage.Get() != NULL) 494 fPackage->RemoveListener(fPackageListener); 495 } 496 497 498 void 499 PackageRow::UpdateTitle() 500 { 501 if (fPackage.Get() == NULL) 502 return; 503 504 const BBitmap* icon = NULL; 505 if (fPackage->Icon().Get() != NULL) 506 icon = fPackage->Icon()->Bitmap(SharedBitmap::SIZE_16); 507 SetField(new BBitmapStringField(icon, fPackage->Title()), kTitleColumn); 508 } 509 510 511 void 512 PackageRow::UpdateState() 513 { 514 if (fPackage.Get() == NULL) 515 return; 516 517 SetField(new BStringField(package_state_to_string(fPackage)), 518 kStatusColumn); 519 } 520 521 522 void 523 PackageRow::UpdateSummary() 524 { 525 if (fPackage.Get() == NULL) 526 return; 527 528 SetField(new BStringField(fPackage->ShortDescription()), 529 kDescriptionColumn); 530 } 531 532 533 void 534 PackageRow::UpdateRating() 535 { 536 if (fPackage.Get() == NULL) 537 return; 538 RatingSummary summary = fPackage->CalculateRatingSummary(); 539 SetField(new RatingField(summary.averageRating), kRatingColumn); 540 } 541 542 543 // #pragma mark - ItemCountView 544 545 546 class PackageListView::ItemCountView : public BView { 547 public: 548 ItemCountView() 549 : 550 BView("item count view", B_WILL_DRAW), 551 fItemCount(0) 552 { 553 BFont font(be_plain_font); 554 font.SetSize(9.0f); 555 SetFont(&font); 556 557 SetViewColor(B_TRANSPARENT_COLOR); 558 SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR)); 559 560 SetHighColor(tint_color(LowColor(), B_DARKEN_4_TINT)); 561 } 562 563 virtual BSize MinSize() 564 { 565 BString label(_GetLabel()); 566 return BSize(StringWidth(label) + 10, B_H_SCROLL_BAR_HEIGHT); 567 } 568 569 virtual BSize PreferredSize() 570 { 571 return MinSize(); 572 } 573 574 virtual BSize MaxSize() 575 { 576 return MinSize(); 577 } 578 579 virtual void Draw(BRect updateRect) 580 { 581 FillRect(updateRect, B_SOLID_LOW); 582 583 BString label(_GetLabel()); 584 585 font_height fontHeight; 586 GetFontHeight(&fontHeight); 587 588 BRect bounds(Bounds()); 589 float width = StringWidth(label); 590 591 BPoint offset; 592 offset.x = bounds.left + (bounds.Width() - width) / 2.0f; 593 offset.y = bounds.top + (bounds.Height() 594 - (fontHeight.ascent + fontHeight.descent)) / 2.0f 595 + fontHeight.ascent; 596 597 DrawString(label, offset); 598 } 599 600 void SetItemCount(int32 count) 601 { 602 if (count == fItemCount) 603 return; 604 fItemCount = count; 605 InvalidateLayout(); 606 Invalidate(); 607 } 608 609 private: 610 BString _GetLabel() const 611 { 612 BString label; 613 BMessageFormat().Format(label, B_TRANSLATE("{0, plural, one{# item} " 614 "other{# items}}"), fItemCount); 615 return label; 616 } 617 618 int32 fItemCount; 619 }; 620 621 622 // #pragma mark - PackageListView 623 624 625 PackageListView::PackageListView(BLocker* modelLock) 626 : 627 BColumnListView("package list view", 0, B_FANCY_BORDER, true), 628 fModelLock(modelLock), 629 fPackageListener(new(std::nothrow) PackageListener(this)) 630 { 631 AddColumn(new PackageColumn(B_TRANSLATE("Name"), 150, 50, 300, 632 B_TRUNCATE_MIDDLE), kTitleColumn); 633 AddColumn(new PackageColumn(B_TRANSLATE("Rating"), 80, 50, 100, 634 B_TRUNCATE_MIDDLE), kRatingColumn); 635 AddColumn(new PackageColumn(B_TRANSLATE("Description"), 300, 80, 1000, 636 B_TRUNCATE_MIDDLE), kDescriptionColumn); 637 AddColumn(new PackageColumn(B_TRANSLATE("Size"), 60, 50, 100, 638 B_TRUNCATE_END), kSizeColumn); 639 AddColumn(new PackageColumn(B_TRANSLATE("Status"), 60, 60, 100, 640 B_TRUNCATE_END), kStatusColumn); 641 642 fItemCountView = new ItemCountView(); 643 AddStatusView(fItemCountView); 644 } 645 646 647 PackageListView::~PackageListView() 648 { 649 Clear(); 650 delete fPackageListener; 651 } 652 653 654 void 655 PackageListView::AttachedToWindow() 656 { 657 BColumnListView::AttachedToWindow(); 658 659 PackageColumn::InitTextMargin(ScrollView()); 660 } 661 662 663 void 664 PackageListView::AllAttached() 665 { 666 BColumnListView::AllAttached(); 667 668 SetSortingEnabled(true); 669 SetSortColumn(ColumnAt(0), false, true); 670 } 671 672 673 void 674 PackageListView::MessageReceived(BMessage* message) 675 { 676 switch (message->what) { 677 case MSG_UPDATE_PACKAGE: 678 { 679 BString title; 680 uint32 changes; 681 if (message->FindString("title", &title) != B_OK 682 || message->FindUInt32("changes", &changes) != B_OK) { 683 break; 684 } 685 686 BAutolock _(fModelLock); 687 PackageRow* row = _FindRow(title); 688 if (row != NULL) { 689 if ((changes & PKG_CHANGED_SUMMARY) != 0) 690 row->UpdateSummary(); 691 if ((changes & PKG_CHANGED_RATINGS) != 0) 692 row->UpdateRating(); 693 if ((changes & PKG_CHANGED_STATE) != 0) 694 row->UpdateState(); 695 if ((changes & PKG_CHANGED_ICON) != 0) 696 row->UpdateTitle(); 697 } 698 break; 699 } 700 701 default: 702 BColumnListView::MessageReceived(message); 703 break; 704 } 705 } 706 707 708 void 709 PackageListView::SelectionChanged() 710 { 711 BColumnListView::SelectionChanged(); 712 713 BMessage message(MSG_PACKAGE_SELECTED); 714 715 PackageRow* selected = dynamic_cast<PackageRow*>(CurrentSelection()); 716 if (selected != NULL) 717 message.AddString("title", selected->Package()->Title()); 718 719 Window()->PostMessage(&message); 720 } 721 722 723 void 724 PackageListView::AddPackage(const PackageInfoRef& package) 725 { 726 PackageRow* packageRow = _FindRow(package); 727 728 // forget about it if this package is already in the listview 729 if (packageRow != NULL) 730 return; 731 732 BAutolock _(fModelLock); 733 734 // create the row for this package 735 packageRow = new PackageRow(package, fPackageListener); 736 737 // add the row, parent may be NULL (add at top level) 738 AddRow(packageRow); 739 740 // make sure the row is initially expanded 741 ExpandOrCollapse(packageRow, true); 742 743 fItemCountView->SetItemCount(CountRows()); 744 } 745 746 747 PackageRow* 748 PackageListView::_FindRow(const PackageInfoRef& package, PackageRow* parent) 749 { 750 for (int32 i = CountRows(parent) - 1; i >= 0; i--) { 751 PackageRow* row = dynamic_cast<PackageRow*>(RowAt(i, parent)); 752 if (row != NULL && row->Package() == package) 753 return row; 754 if (CountRows(row) > 0) { 755 // recurse into child rows 756 row = _FindRow(package, row); 757 if (row != NULL) 758 return row; 759 } 760 } 761 762 return NULL; 763 } 764 765 766 PackageRow* 767 PackageListView::_FindRow(const BString& packageTitle, PackageRow* parent) 768 { 769 for (int32 i = CountRows(parent) - 1; i >= 0; i--) { 770 PackageRow* row = dynamic_cast<PackageRow*>(RowAt(i, parent)); 771 if (row != NULL && row->Package().Get() != NULL 772 && row->Package()->Title() == packageTitle) { 773 return row; 774 } 775 if (CountRows(row) > 0) { 776 // recurse into child rows 777 row = _FindRow(packageTitle, row); 778 if (row != NULL) 779 return row; 780 } 781 } 782 783 return NULL; 784 } 785 786