1 /* 2 * Copyright 2007-2009 Stephan Aßmus <superstippi@gmx.de>. 3 * All rights reserved. Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "PlaylistListView.h" 8 9 #include <new> 10 #include <stdio.h> 11 12 #include <Autolock.h> 13 #include <Catalog.h> 14 #include <GradientLinear.h> 15 #include <MenuItem.h> 16 #include <Message.h> 17 #include <PopUpMenu.h> 18 #include <ScrollBar.h> 19 #include <ScrollView.h> 20 #include <Shape.h> 21 #include <Window.h> 22 23 #include "CommandStack.h" 24 #include "Controller.h" 25 #include "ControllerObserver.h" 26 #include "CopyPLItemsCommand.h" 27 #include "DurationToString.h" 28 #include "ImportPLItemsCommand.h" 29 #include "ListViews.h" 30 #include "MovePLItemsCommand.h" 31 #include "PlaybackState.h" 32 #include "Playlist.h" 33 #include "PlaylistItem.h" 34 #include "PlaylistObserver.h" 35 #include "RandomizePLItemsCommand.h" 36 #include "RemovePLItemsCommand.h" 37 38 #undef B_TRANSLATION_CONTEXT 39 #define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistListView" 40 41 using std::nothrow; 42 43 44 enum { 45 DISPLAY_NAME = 0, 46 DISPLAY_PATH = 1, 47 M_ADD_SORTED, 48 M_ADD_UNSORTED 49 }; 50 51 52 static float 53 playback_mark_size(const font_height& fh) 54 { 55 return ceilf(fh.ascent * 0.7); 56 } 57 58 59 static float 60 text_offset(const font_height& fh) 61 { 62 return ceilf(fh.ascent * 0.8); 63 } 64 65 66 class PlaylistListView::Item : public SimpleItem, 67 public PlaylistItem::Listener { 68 public: 69 Item(PlaylistItem* item); 70 virtual ~Item(); 71 72 void Draw(BView* owner, BRect frame, 73 const font_height& fh, 74 bool tintedLine, uint32 mode, 75 bool active, 76 uint32 playbackState); 77 78 virtual void ItemChanged(const PlaylistItem* item); 79 80 #if __GNUC__ == 2 81 virtual void Draw(BView* owner, BRect frame, uint32 flags); 82 #else 83 using SimpleItem::Draw; 84 #endif 85 86 private: 87 PlaylistItemRef fItem; 88 89 }; 90 91 92 // #pragma mark - 93 94 95 PlaylistListView::Item::Item(PlaylistItem* item) 96 : 97 SimpleItem(item->Name().String()), 98 fItem(item) 99 { 100 fItem->AddListener(this); 101 } 102 103 104 PlaylistListView::Item::~Item() 105 { 106 fItem->RemoveListener(this); 107 } 108 109 110 void 111 PlaylistListView::Item::Draw(BView* owner, BRect frame, const font_height& fh, 112 bool tintedLine, uint32 mode, bool active, uint32 playbackState) 113 { 114 rgb_color color = ui_color(B_LIST_BACKGROUND_COLOR); 115 116 if (IsSelected()) 117 color = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR); 118 if (tintedLine) 119 color = tint_color(color, 1.04); 120 // background 121 owner->SetLowColor(color); 122 owner->FillRect(frame, B_SOLID_LOW); 123 // label 124 if (IsSelected()) 125 owner->SetHighColor(ui_color(B_LIST_SELECTED_ITEM_TEXT_COLOR)); 126 else 127 owner->SetHighColor(ui_color(B_LIST_ITEM_TEXT_COLOR)); 128 const char* text = Text(); 129 switch (mode) { 130 case DISPLAY_NAME: 131 // TODO 132 break; 133 case DISPLAY_PATH: 134 // TODO 135 break; 136 default: 137 break; 138 } 139 140 float playbackMarkSize = playback_mark_size(fh); 141 float textOffset = text_offset(fh); 142 143 char buffer[64]; 144 bigtime_t duration = fItem->Duration(); 145 duration /= 1000000; 146 duration_to_string(duration, buffer, sizeof(buffer)); 147 148 BString truncatedDuration(buffer); 149 owner->TruncateString(&truncatedDuration, B_TRUNCATE_END, 150 frame.Width() - playbackMarkSize - textOffset); 151 float truncatedWidth = owner->StringWidth(truncatedDuration.String()); 152 owner->DrawString(truncatedDuration.String(), 153 BPoint(frame.right - truncatedWidth, 154 floorf(frame.top + frame.bottom + fh.ascent) / 2 - 1)); 155 156 BString truncatedString(text); 157 owner->TruncateString(&truncatedString, B_TRUNCATE_MIDDLE, 158 frame.Width() - playbackMarkSize - textOffset - truncatedWidth); 159 owner->DrawString(truncatedString.String(), 160 BPoint(frame.left + playbackMarkSize + textOffset, 161 floorf(frame.top + frame.bottom + fh.ascent) / 2 - 1)); 162 163 // playmark 164 if (active) { 165 rgb_color green = (rgb_color){ 0, 255, 0, 255 }; 166 if (playbackState != PLAYBACK_STATE_PLAYING) 167 green = tint_color(color, B_DARKEN_1_TINT); 168 169 BRect r(0, 0, playbackMarkSize, playbackMarkSize); 170 r.OffsetTo(frame.left + 4, 171 ceilf((frame.top + frame.bottom - playbackMarkSize) / 2)); 172 173 uint32 flags = owner->Flags(); 174 owner->SetFlags(flags | B_SUBPIXEL_PRECISE); 175 176 BShape shape; 177 shape.MoveTo(r.LeftTop()); 178 shape.LineTo(r.LeftBottom()); 179 shape.LineTo(BPoint(r.right, (r.top + r.bottom) / 2)); 180 shape.Close(); 181 182 owner->MovePenTo(B_ORIGIN); 183 owner->FillShape(&shape); 184 185 shape.Clear(); 186 r.InsetBy(1, 1); 187 shape.MoveTo(r.LeftTop()); 188 shape.LineTo(r.LeftBottom()); 189 shape.LineTo(BPoint(r.right, (r.top + r.bottom) / 2)); 190 shape.Close(); 191 192 BGradientLinear gradient; 193 gradient.SetStart(r.LeftTop()); 194 gradient.SetEnd(r.LeftBottom()); 195 gradient.AddColor(tint_color(green, B_LIGHTEN_1_TINT), 0); 196 gradient.AddColor(tint_color(green, B_DARKEN_1_TINT), 255.0); 197 198 owner->FillShape(&shape, gradient); 199 200 owner->SetFlags(flags); 201 } 202 } 203 204 205 void 206 PlaylistListView::Item::ItemChanged(const PlaylistItem* item) 207 { 208 // TODO: Invalidate 209 } 210 211 212 #if __GNUC__ == 2 213 214 void 215 PlaylistListView::Item::Draw(BView* owner, BRect frame, uint32 flags) 216 { 217 SimpleItem::Draw(owner, frame, flags); 218 } 219 220 #endif 221 222 223 // #pragma mark - 224 225 226 PlaylistListView::PlaylistListView(BRect frame, Playlist* playlist, 227 Controller* controller, CommandStack* stack) 228 : 229 SimpleListView(frame, "playlist listview", NULL), 230 231 fPlaylist(playlist), 232 fPlaylistObserver(new PlaylistObserver(this)), 233 234 fController(controller), 235 fControllerObserver(new ControllerObserver(this, 236 OBSERVE_PLAYBACK_STATE_CHANGES)), 237 238 fCommandStack(stack), 239 240 fCurrentPlaylistIndex(-1), 241 fPlaybackState(PLAYBACK_STATE_STOPPED), 242 243 fLastClickedItem(NULL) 244 { 245 fPlaylist->AddListener(fPlaylistObserver); 246 fController->AddListener(fControllerObserver); 247 _AddDropContextMenu(); 248 249 SetFlags(Flags() | B_SUBPIXEL_PRECISE); 250 } 251 252 253 PlaylistListView::~PlaylistListView() 254 { 255 for (int32 i = CountItems() - 1; i >= 0; i--) 256 _RemoveItem(i); 257 fPlaylist->RemoveListener(fPlaylistObserver); 258 delete fPlaylistObserver; 259 fController->RemoveListener(fControllerObserver); 260 delete fControllerObserver; 261 } 262 263 264 void 265 PlaylistListView::AttachedToWindow() 266 { 267 _FullSync(); 268 SimpleListView::AttachedToWindow(); 269 270 GetFontHeight(&fFontHeight); 271 MakeFocus(true); 272 } 273 274 275 void 276 PlaylistListView::MessageReceived(BMessage* message) 277 { 278 switch (message->what) { 279 // PlaylistObserver messages 280 case MSG_PLAYLIST_ITEM_ADDED: 281 { 282 PlaylistItem* item; 283 int32 index; 284 if (message->FindPointer("item", (void**)&item) == B_OK 285 && message->FindInt32("index", &index) == B_OK) 286 _AddItem(item, index); 287 break; 288 } 289 case MSG_PLAYLIST_ITEM_REMOVED: 290 { 291 int32 index; 292 if (message->FindInt32("index", &index) == B_OK) 293 _RemoveItem(index); 294 break; 295 } 296 case MSG_PLAYLIST_ITEMS_SORTED: 297 _FullSync(); 298 break; 299 case MSG_PLAYLIST_CURRENT_ITEM_CHANGED: 300 { 301 int32 index; 302 if (message->FindInt32("index", &index) == B_OK) 303 _SetCurrentPlaylistIndex(index); 304 break; 305 } 306 case MSG_PLAYLIST_IMPORT_FAILED: 307 break; 308 309 // ControllerObserver messages 310 case MSG_CONTROLLER_PLAYBACK_STATE_CHANGED: 311 { 312 uint32 state; 313 if (message->FindInt32("state", (int32*)&state) == B_OK) 314 _SetPlaybackState(state); 315 break; 316 } 317 318 case B_SIMPLE_DATA: 319 if (message->HasRef("refs")) 320 ItemsReceived(message, fDropIndex); 321 else if (message->HasPointer("list")) 322 SimpleListView::MessageReceived(message); 323 break; 324 case B_REFS_RECEIVED: 325 ItemsReceived(message, fDropIndex); 326 break; 327 328 default: 329 SimpleListView::MessageReceived(message); 330 break; 331 } 332 } 333 334 335 void 336 PlaylistListView::MouseDown(BPoint where) 337 { 338 if (!IsFocus()) 339 MakeFocus(true); 340 341 int32 clicks; 342 if (Window()->CurrentMessage()->FindInt32("clicks", &clicks) < B_OK) 343 clicks = 1; 344 345 bool handled = false; 346 347 float playbackMarkSize = playback_mark_size(fFontHeight); 348 float textOffset = text_offset(fFontHeight); 349 350 for (int32 i = 0; 351 Item* item = dynamic_cast<Item*>(ItemAt(i)); i++) { 352 BRect r = ItemFrame(i); 353 if (r.Contains(where)) { 354 if (clicks == 2) { 355 // only do something if user clicked the same item twice 356 if (fLastClickedItem == item) { 357 BAutolock _(fPlaylist); 358 fPlaylist->SetCurrentItemIndex(i, true); 359 handled = true; 360 } 361 } else { 362 // remember last clicked item 363 fLastClickedItem = item; 364 if (i == fCurrentPlaylistIndex) { 365 r.right = r.left + playbackMarkSize + textOffset; 366 if (r.Contains (where)) { 367 fController->TogglePlaying(); 368 handled = true; 369 } 370 } 371 } 372 break; 373 } 374 } 375 376 if (!handled) 377 SimpleListView::MouseDown(where); 378 } 379 380 381 void 382 PlaylistListView::KeyDown(const char* bytes, int32 numBytes) 383 { 384 if (numBytes < 1) 385 return; 386 387 BMessage* msg = Window()->CurrentMessage(); 388 uint32 modifier = msg->FindInt32("modifiers"); 389 390 int32 count; 391 int32 index; 392 393 switch (bytes[0]) { 394 case B_SPACE: 395 fController->TogglePlaying(); 396 break; 397 398 case B_BACKSPACE: 399 case B_DELETE: 400 RemoveSelected(); 401 break; 402 403 case B_ENTER: 404 count = CountItems(); 405 if (count == 0) 406 break; 407 index = CurrentSelection(0); 408 if (index < 0) 409 break; 410 fPlaylist->SetCurrentItemIndex(index, true); 411 fController->Play(); 412 break; 413 414 case B_ESCAPE: 415 fController->Stop(); 416 break; 417 418 case B_RIGHT_ARROW: 419 if ((modifier & B_SHIFT_KEY) != 0) 420 _Wind(30000000LL, 5); 421 else 422 _Wind(5000000LL, 1); 423 break; 424 425 case B_LEFT_ARROW: 426 if ((modifier & B_SHIFT_KEY) != 0) 427 _Wind(-30000000LL, -5); 428 else 429 _Wind(-5000000LL, -1); 430 break; 431 default: 432 DragSortableListView::KeyDown(bytes, numBytes); 433 } 434 } 435 436 437 void 438 PlaylistListView::SkipBackward() 439 { 440 BAutolock _(fPlaylist); 441 int32 index = fPlaylist->CurrentItemIndex() - 1; 442 if (index < 0) 443 index = 0; 444 fPlaylist->SetCurrentItemIndex(index, true); 445 } 446 447 448 void 449 PlaylistListView::SkipForward() 450 { 451 BAutolock _(fPlaylist); 452 int32 index = fPlaylist->CurrentItemIndex() + 1; 453 if (index >= fPlaylist->CountItems()) 454 index = fPlaylist->CountItems() - 1; 455 fPlaylist->SetCurrentItemIndex(index, true); 456 } 457 458 459 void 460 PlaylistListView::_Wind(bigtime_t howMuch, int64 frames) 461 { 462 if (!fController->Lock()) 463 return; 464 465 if (frames != 0 && !fController->IsPlaying()) { 466 int64 newFrame = fController->CurrentFrame() + frames; 467 fController->SetFramePosition(newFrame); 468 } else { 469 bigtime_t seekTime = fController->TimePosition() + howMuch; 470 if (seekTime < 0) { 471 SkipBackward(); 472 } else if (seekTime > fController->TimeDuration()) { 473 SkipForward(); 474 } else 475 fController->SetTimePosition(seekTime); 476 } 477 478 fController->Unlock(); 479 } 480 481 482 void 483 PlaylistListView::MoveItems(const BList& indices, int32 toIndex) 484 { 485 fCommandStack->Perform(new (nothrow) MovePLItemsCommand(fPlaylist, 486 indices, toIndex)); 487 } 488 489 490 void 491 PlaylistListView::CopyItems(const BList& indices, int32 toIndex) 492 { 493 fCommandStack->Perform(new (nothrow) CopyPLItemsCommand(fPlaylist, 494 indices, toIndex)); 495 } 496 497 498 void 499 PlaylistListView::RemoveItemList(const BList& indices) 500 { 501 RemoveItemList(indices, false); 502 } 503 504 505 void 506 PlaylistListView::DrawListItem(BView* owner, int32 index, BRect frame) const 507 { 508 if (Item* item = dynamic_cast<Item*>(ItemAt(index))) { 509 item->Draw(owner, frame, fFontHeight, index % 2, 510 DISPLAY_NAME, index == fCurrentPlaylistIndex, fPlaybackState); 511 } 512 } 513 514 515 void 516 PlaylistListView::ItemsReceived(const BMessage* message, int32 appendIndex) 517 { 518 BPoint dropPoint; 519 bool sorting = false; 520 entry_ref ref; 521 522 if (message->FindRef("refs", 1, &ref) == B_OK 523 && message->FindPoint("_drop_point_", &dropPoint) == B_OK 524 && message->GetInt32("buttons", 0) == 2) 525 if (_ShowDropContextMenu(dropPoint) == M_ADD_SORTED) 526 sorting = true; 527 528 if (fCommandStack->Perform(new (nothrow) ImportPLItemsCommand(fPlaylist, 529 message, appendIndex, sorting)) != B_OK) { 530 fPlaylist->NotifyImportFailed(); 531 } 532 } 533 534 535 void 536 PlaylistListView::Randomize() 537 { 538 int32 count = CountItems(); 539 if (count == 0) 540 return; 541 542 BList indices; 543 544 // add current selection 545 count = 0; 546 while (true) { 547 int32 index = CurrentSelection(count); 548 if (index < 0) 549 break; 550 if (!indices.AddItem((void*)(addr_t)index)) 551 return; 552 count++; 553 } 554 555 // was anything selected? 556 if (count == 0) { 557 // no selection, simply add all items 558 count = CountItems(); 559 for (int32 i = 0; i < count; i++) { 560 if (!indices.AddItem((void*)(addr_t)i)) 561 return; 562 } 563 } 564 565 fCommandStack->Perform(new (nothrow) RandomizePLItemsCommand(fPlaylist, 566 indices)); 567 } 568 569 570 void 571 PlaylistListView::RemoveSelectionToTrash() 572 { 573 BList indices; 574 GetSelectedItems(indices); 575 RemoveItemList(indices, true); 576 } 577 578 579 void 580 PlaylistListView::RemoveToTrash(int32 index) 581 { 582 BList indices; 583 indices.AddItem((void*)(addr_t)index); 584 RemoveItemList(indices, true); 585 } 586 587 588 void 589 PlaylistListView::RemoveItemList(const BList& indices, bool intoTrash) 590 { 591 fCommandStack->Perform(new (nothrow) RemovePLItemsCommand(fPlaylist, 592 indices, intoTrash)); 593 } 594 595 596 // #pragma mark - 597 598 599 void 600 PlaylistListView::_FullSync() 601 { 602 if (!fPlaylist->Lock()) 603 return; 604 605 // detaching the scrollbar temporarily will 606 // make this much quicker 607 BScrollBar* scrollBar = ScrollBar(B_VERTICAL); 608 if (scrollBar) { 609 if (Window()) 610 Window()->UpdateIfNeeded(); 611 scrollBar->SetTarget((BView*)NULL); 612 } 613 614 for (int32 i = CountItems() - 1; i >= 0; i--) 615 _RemoveItem(i); 616 617 int32 count = fPlaylist->CountItems(); 618 for (int32 i = 0; i < count; i++) 619 _AddItem(fPlaylist->ItemAt(i), i); 620 621 _SetCurrentPlaylistIndex(fPlaylist->CurrentItemIndex()); 622 _SetPlaybackState(fController->PlaybackState()); 623 624 // reattach scrollbar and sync it by calling FrameResized() 625 if (scrollBar) { 626 scrollBar->SetTarget(this); 627 FrameResized(Bounds().Width(), Bounds().Height()); 628 } 629 630 fPlaylist->Unlock(); 631 } 632 633 634 void 635 PlaylistListView::_AddItem(PlaylistItem* _item, int32 index) 636 { 637 if (_item == NULL) 638 return; 639 640 Item* item = new (nothrow) Item(_item); 641 if (item != NULL) 642 AddItem(item, index); 643 } 644 645 646 void 647 PlaylistListView::_RemoveItem(int32 index) 648 { 649 delete RemoveItem(index); 650 } 651 652 653 void 654 PlaylistListView::_SetCurrentPlaylistIndex(int32 index) 655 { 656 if (fCurrentPlaylistIndex == index) 657 return; 658 659 InvalidateItem(fCurrentPlaylistIndex); 660 fCurrentPlaylistIndex = index; 661 InvalidateItem(fCurrentPlaylistIndex); 662 } 663 664 665 void 666 PlaylistListView::_SetPlaybackState(uint32 state) 667 { 668 if (fPlaybackState == state) 669 return; 670 671 fPlaybackState = state; 672 InvalidateItem(fCurrentPlaylistIndex); 673 } 674 675 676 void 677 PlaylistListView::_AddDropContextMenu() 678 { 679 fDropContextMenu = new BPopUpMenu("DropContext"); 680 681 fDropContextMenu->AddItem(new BMenuItem(B_TRANSLATE("Add sorted"), 682 new BMessage(M_ADD_SORTED))); 683 fDropContextMenu->AddItem(new BMenuItem(B_TRANSLATE("Add unsorted"), 684 new BMessage(M_ADD_UNSORTED))); 685 } 686 687 688 uint32 689 PlaylistListView::_ShowDropContextMenu(BPoint dropPoint) 690 { 691 BMenuItem* item; 692 693 item = fDropContextMenu->Go(dropPoint, true, true); 694 if (item != NULL) 695 return item->Command(); 696 return 0; 697 } 698 699 700