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 41using std::nothrow; 42 43 44enum { 45 DISPLAY_NAME = 0, 46 DISPLAY_PATH = 1, 47 M_ADD_SORTED, 48 M_ADD_UNSORTED 49}; 50 51 52static float 53playback_mark_size(const font_height& fh) 54{ 55 return ceilf(fh.ascent * 0.7); 56} 57 58 59static float 60text_offset(const font_height& fh) 61{ 62 return ceilf(fh.ascent * 0.8); 63} 64 65 66class PlaylistListView::Item : public SimpleItem, 67 public PlaylistItem::Listener { 68public: 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 86private: 87 PlaylistItemRef fItem; 88 89}; 90 91 92// #pragma mark - 93 94 95PlaylistListView::Item::Item(PlaylistItem* item) 96 : 97 SimpleItem(item->Name().String()), 98 fItem(item) 99{ 100 fItem->AddListener(this); 101} 102 103 104PlaylistListView::Item::~Item() 105{ 106 fItem->RemoveListener(this); 107} 108 109 110void 111PlaylistListView::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 205void 206PlaylistListView::Item::ItemChanged(const PlaylistItem* item) 207{ 208 // TODO: Invalidate 209} 210 211 212#if __GNUC__ == 2 213 214void 215PlaylistListView::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 226PlaylistListView::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 253PlaylistListView::~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 264void 265PlaylistListView::AttachedToWindow() 266{ 267 _FullSync(); 268 SimpleListView::AttachedToWindow(); 269 270 GetFontHeight(&fFontHeight); 271 MakeFocus(true); 272} 273 274 275void 276PlaylistListView::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 335void 336PlaylistListView::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 381void 382PlaylistListView::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 437void 438PlaylistListView::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 448void 449PlaylistListView::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 459void 460PlaylistListView::_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 482void 483PlaylistListView::MoveItems(const BList& indices, int32 toIndex) 484{ 485 fCommandStack->Perform(new (nothrow) MovePLItemsCommand(fPlaylist, 486 indices, toIndex)); 487} 488 489 490void 491PlaylistListView::CopyItems(const BList& indices, int32 toIndex) 492{ 493 fCommandStack->Perform(new (nothrow) CopyPLItemsCommand(fPlaylist, 494 indices, toIndex)); 495} 496 497 498void 499PlaylistListView::RemoveItemList(const BList& indices) 500{ 501 RemoveItemList(indices, false); 502} 503 504 505void 506PlaylistListView::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 515void 516PlaylistListView::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 535void 536PlaylistListView::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 570void 571PlaylistListView::RemoveSelectionToTrash() 572{ 573 BList indices; 574 GetSelectedItems(indices); 575 RemoveItemList(indices, true); 576} 577 578 579void 580PlaylistListView::RemoveToTrash(int32 index) 581{ 582 BList indices; 583 indices.AddItem((void*)(addr_t)index); 584 RemoveItemList(indices, true); 585} 586 587 588void 589PlaylistListView::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 599void 600PlaylistListView::_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 634void 635PlaylistListView::_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 646void 647PlaylistListView::_RemoveItem(int32 index) 648{ 649 delete RemoveItem(index); 650} 651 652 653void 654PlaylistListView::_SetCurrentPlaylistIndex(int32 index) 655{ 656 if (fCurrentPlaylistIndex == index) 657 return; 658 659 InvalidateItem(fCurrentPlaylistIndex); 660 fCurrentPlaylistIndex = index; 661 InvalidateItem(fCurrentPlaylistIndex); 662} 663 664 665void 666PlaylistListView::_SetPlaybackState(uint32 state) 667{ 668 if (fPlaybackState == state) 669 return; 670 671 fPlaybackState = state; 672 InvalidateItem(fCurrentPlaylistIndex); 673} 674 675 676void 677PlaylistListView::_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 688uint32 689PlaylistListView::_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