1 /* 2 * Copyright 2007-2010, Haiku. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Stephan Aßmus <superstippi@gmx.de> 7 * Fredrik Modéen <fredrik@modeen.se> 8 */ 9 10 11 #include "PlaylistWindow.h" 12 13 #include <stdio.h> 14 15 #include <Alert.h> 16 #include <Application.h> 17 #include <Autolock.h> 18 #include <Box.h> 19 #include <Button.h> 20 #include <Catalog.h> 21 #include <Entry.h> 22 #include <File.h> 23 #include <FilePanel.h> 24 #include <Locale.h> 25 #include <Menu.h> 26 #include <MenuBar.h> 27 #include <MenuItem.h> 28 #include <NodeInfo.h> 29 #include <Path.h> 30 #include <Roster.h> 31 #include <ScrollBar.h> 32 #include <ScrollView.h> 33 #include <String.h> 34 #include <StringView.h> 35 36 #include "CommandStack.h" 37 #include "DurationToString.h" 38 #include "MainApp.h" 39 #include "PlaylistListView.h" 40 #include "RWLocker.h" 41 42 #undef B_TRANSLATION_CONTEXT 43 #define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow" 44 45 46 // TODO: 47 // Maintaining a playlist file on disk is a bit tricky. The playlist ref should 48 // be discarded when the user 49 // * loads a new playlist via Open, 50 // * loads a new playlist via dropping it on the MainWindow, 51 // * loads a new playlist via dropping it into the ListView while replacing 52 // the contents, 53 // * replacing the contents by other stuff. 54 55 56 static void 57 display_save_alert(const char* message) 58 { 59 BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message, 60 B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT); 61 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 62 alert->Go(NULL); 63 } 64 65 66 static void 67 display_save_alert(status_t error) 68 { 69 BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: ")); 70 errorMessage << strerror(error); 71 display_save_alert(errorMessage.String()); 72 } 73 74 75 // #pragma mark - 76 77 78 PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist, 79 Controller* controller) 80 : 81 BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK, 82 B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS), 83 fPlaylist(playlist), 84 fLocker(new RWLocker("command stack lock")), 85 fCommandStack(new CommandStack(fLocker)), 86 fCommandStackListener(this), 87 fDurationListener(new DurationListener(*this)) 88 { 89 frame = Bounds(); 90 91 _CreateMenu(frame); 92 // will adjust frame to account for menubar 93 94 frame.right -= B_V_SCROLL_BAR_WIDTH; 95 frame.bottom -= B_H_SCROLL_BAR_HEIGHT; 96 fListView = new PlaylistListView(frame, playlist, controller, 97 fCommandStack); 98 99 BScrollView* scrollView = new BScrollView("playlist scrollview", fListView, 100 B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER); 101 102 fTopView = scrollView; 103 AddChild(fTopView); 104 105 // small visual tweak 106 if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) { 107 // make it so the frame of the menubar is also the frame of 108 // the scroll bar (appears to be) 109 scrollBar->MoveBy(0, -1); 110 scrollBar->ResizeBy(0, 2); 111 } 112 113 frame.top += frame.Height(); 114 frame.bottom += B_H_SCROLL_BAR_HEIGHT; 115 116 fTotalDuration = new BStringView(frame, "fDuration", "", 117 B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT); 118 fTotalDuration->SetAlignment(B_ALIGN_RIGHT); 119 fTotalDuration->SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 120 AddChild(fTotalDuration); 121 122 _UpdateTotalDuration(0); 123 124 { 125 BAutolock _(fPlaylist); 126 127 _QueryInitialDurations(); 128 fPlaylist->AddListener(fDurationListener); 129 } 130 131 fCommandStack->AddListener(&fCommandStackListener); 132 _ObjectChanged(fCommandStack); 133 } 134 135 136 PlaylistWindow::~PlaylistWindow() 137 { 138 // give listeners a chance to detach themselves 139 fTopView->RemoveSelf(); 140 delete fTopView; 141 142 fCommandStack->RemoveListener(&fCommandStackListener); 143 delete fCommandStack; 144 delete fLocker; 145 146 fPlaylist->RemoveListener(fDurationListener); 147 BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED); 148 } 149 150 151 bool 152 PlaylistWindow::QuitRequested() 153 { 154 Hide(); 155 return false; 156 } 157 158 159 void 160 PlaylistWindow::MessageReceived(BMessage* message) 161 { 162 switch (message->what) { 163 case B_MODIFIERS_CHANGED: 164 if (LastMouseMovedView()) 165 PostMessage(message, LastMouseMovedView()); 166 break; 167 168 case B_UNDO: 169 fCommandStack->Undo(); 170 break; 171 case B_REDO: 172 fCommandStack->Redo(); 173 break; 174 175 case MSG_OBJECT_CHANGED: { 176 Notifier* notifier; 177 if (message->FindPointer("object", (void**)¬ifier) == B_OK) 178 _ObjectChanged(notifier); 179 break; 180 } 181 182 case M_URL_RECEIVED: 183 case B_REFS_RECEIVED: 184 // Used for when we open a playlist from playlist window 185 if (!message->HasInt32("append_index")) { 186 message->AddInt32("append_index", 187 APPEND_INDEX_REPLACE_PLAYLIST); 188 } 189 // supposed to fall through 190 case B_SIMPLE_DATA: 191 { 192 // only accept this message when it comes from the 193 // player window, _not_ when it is dropped in this window 194 // outside of the playlist! 195 int32 appendIndex; 196 if (message->FindInt32("append_index", &appendIndex) == B_OK) 197 fListView->ItemsReceived(message, appendIndex); 198 break; 199 } 200 201 case M_PLAYLIST_OPEN: 202 { 203 BMessenger target(this); 204 BMessage result(B_REFS_RECEIVED); 205 BMessage appMessage(M_SHOW_OPEN_PANEL); 206 appMessage.AddMessenger("target", target); 207 appMessage.AddMessage("message", &result); 208 appMessage.AddString("title", B_TRANSLATE("Open Playlist")); 209 appMessage.AddString("label", B_TRANSLATE("Open")); 210 be_app->PostMessage(&appMessage); 211 break; 212 } 213 214 case M_PLAYLIST_SAVE: 215 if (fSavedPlaylistRef != entry_ref()) { 216 _SavePlaylist(fSavedPlaylistRef); 217 break; 218 } 219 // supposed to fall through 220 case M_PLAYLIST_SAVE_AS: 221 { 222 BMessenger target(this); 223 BMessage result(M_PLAYLIST_SAVE_RESULT); 224 BMessage appMessage(M_SHOW_SAVE_PANEL); 225 appMessage.AddMessenger("target", target); 226 appMessage.AddMessage("message", &result); 227 appMessage.AddString("title", B_TRANSLATE("Save Playlist")); 228 appMessage.AddString("label", B_TRANSLATE("Save")); 229 be_app->PostMessage(&appMessage); 230 break; 231 } 232 233 case M_PLAYLIST_SAVE_RESULT: 234 _SavePlaylist(message); 235 break; 236 237 case B_SELECT_ALL: 238 fListView->SelectAll(); 239 break; 240 241 case M_PLAYLIST_RANDOMIZE: 242 fListView->Randomize(); 243 break; 244 245 case M_PLAYLIST_REMOVE: 246 fListView->RemoveSelected(); 247 break; 248 249 case M_PLAYLIST_MOVE_TO_TRASH: 250 { 251 int32 index; 252 if (message->FindInt32("playlist index", &index) == B_OK) 253 fListView->RemoveToTrash(index); 254 else 255 fListView->RemoveSelectionToTrash(); 256 break; 257 } 258 259 default: 260 BWindow::MessageReceived(message); 261 break; 262 } 263 } 264 265 266 // #pragma mark - 267 268 269 void 270 PlaylistWindow::_CreateMenu(BRect& frame) 271 { 272 frame.bottom = 15; 273 BMenuBar* menuBar = new BMenuBar(frame, "main menu"); 274 BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist")); 275 menuBar->AddItem(fileMenu); 276 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS), 277 new BMessage(M_PLAYLIST_OPEN), 'O')); 278 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS), 279 new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY)); 280 // fileMenu->AddItem(new BMenuItem("Save", 281 // new BMessage(M_PLAYLIST_SAVE), 'S')); 282 283 fileMenu->AddSeparatorItem(); 284 285 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"), 286 new BMessage(B_QUIT_REQUESTED), 'W')); 287 288 BMenu* editMenu = new BMenu(B_TRANSLATE("Edit")); 289 fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z'); 290 editMenu->AddItem(fUndoMI); 291 fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z', 292 B_SHIFT_KEY); 293 editMenu->AddItem(fRedoMI); 294 editMenu->AddSeparatorItem(); 295 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"), 296 new BMessage(B_SELECT_ALL), 'A')); 297 editMenu->AddSeparatorItem(); 298 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"), 299 new BMessage(M_PLAYLIST_RANDOMIZE), 'R')); 300 editMenu->AddSeparatorItem(); 301 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"), 302 new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/)); 303 // TODO: See if we can support the modifier-less B_DELETE 304 // and draw it properly too. B_NO_MODIFIER? 305 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"), 306 new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T')); 307 308 menuBar->AddItem(editMenu); 309 310 AddChild(menuBar); 311 fileMenu->SetTargetForItems(this); 312 editMenu->SetTargetForItems(this); 313 314 menuBar->ResizeToPreferred(); 315 frame = Bounds(); 316 frame.top = menuBar->Frame().bottom + 1; 317 } 318 319 320 void 321 PlaylistWindow::_ObjectChanged(const Notifier* object) 322 { 323 if (object == fCommandStack) { 324 // relable Undo item and update enabled status 325 BString label(B_TRANSLATE("Undo")); 326 fUndoMI->SetEnabled(fCommandStack->GetUndoName(label)); 327 if (fUndoMI->IsEnabled()) 328 fUndoMI->SetLabel(label.String()); 329 else 330 fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>")); 331 332 // relable Redo item and update enabled status 333 label.SetTo(B_TRANSLATE("Redo")); 334 fRedoMI->SetEnabled(fCommandStack->GetRedoName(label)); 335 if (fRedoMI->IsEnabled()) 336 fRedoMI->SetLabel(label.String()); 337 else 338 fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>")); 339 } 340 } 341 342 343 void 344 PlaylistWindow::_SavePlaylist(const BMessage* message) 345 { 346 entry_ref ref; 347 const char* name; 348 if (message->FindRef("directory", &ref) != B_OK 349 || message->FindString("name", &name) != B_OK) { 350 display_save_alert(B_TRANSLATE("Internal error (malformed message). " 351 "Saving the playlist failed.")); 352 return; 353 } 354 355 BString tempName(name); 356 tempName << system_time(); 357 358 BPath origPath(&ref); 359 BPath tempPath(&ref); 360 if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK 361 || origPath.Append(name) != B_OK 362 || tempPath.Append(tempName.String()) != B_OK) { 363 display_save_alert(B_TRANSLATE("Internal error (out of memory). " 364 "Saving the playlist failed.")); 365 return; 366 } 367 368 BEntry origEntry(origPath.Path()); 369 BEntry tempEntry(tempPath.Path()); 370 if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) { 371 display_save_alert(B_TRANSLATE("Internal error (out of memory). " 372 "Saving the playlist failed.")); 373 return; 374 } 375 376 _SavePlaylist(origEntry, tempEntry, name); 377 } 378 379 380 void 381 PlaylistWindow::_SavePlaylist(const entry_ref& ref) 382 { 383 BString tempName(ref.name); 384 tempName << system_time(); 385 entry_ref tempRef(ref); 386 tempRef.set_name(tempName.String()); 387 388 BEntry origEntry(&ref); 389 BEntry tempEntry(&tempRef); 390 391 _SavePlaylist(origEntry, tempEntry, ref.name); 392 } 393 394 395 void 396 PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry, 397 const char* finalName) 398 { 399 class TempEntryRemover { 400 public: 401 TempEntryRemover(BEntry* entry) 402 : fEntry(entry) 403 { 404 } 405 ~TempEntryRemover() 406 { 407 if (fEntry) 408 fEntry->Remove(); 409 } 410 void Detach() 411 { 412 fEntry = NULL; 413 } 414 private: 415 BEntry* fEntry; 416 } remover(&tempEntry); 417 418 BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 419 if (file.InitCheck() != B_OK) { 420 BString errorMessage(B_TRANSLATE( 421 "Saving the playlist failed:\n\nError: ")); 422 errorMessage << strerror(file.InitCheck()); 423 display_save_alert(errorMessage.String()); 424 return; 425 } 426 427 AutoLocker<Playlist> lock(fPlaylist); 428 if (!lock.IsLocked()) { 429 display_save_alert(B_TRANSLATE("Internal error (locking failed). " 430 "Saving the playlist failed.")); 431 return; 432 } 433 434 status_t ret = fPlaylist->Flatten(&file); 435 if (ret != B_OK) { 436 display_save_alert(ret); 437 return; 438 } 439 lock.Unlock(); 440 441 if (origEntry.Exists()) { 442 // TODO: copy attributes 443 } 444 445 // clobber original entry, if it exists 446 tempEntry.Rename(finalName, true); 447 remover.Detach(); 448 449 BNodeInfo info(&file); 450 info.SetType("application/x-vnd.haiku-playlist"); 451 } 452 453 454 void 455 PlaylistWindow::_QueryInitialDurations() 456 { 457 BAutolock lock(fPlaylist); 458 459 BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED); 460 for (int32 i = 0; i < fPlaylist->CountItems(); i++) { 461 addMessage.AddPointer("item", fPlaylist->ItemAt(i)); 462 addMessage.AddInt32("index", i); 463 } 464 465 BMessenger(fDurationListener).SendMessage(&addMessage); 466 } 467 468 469 void 470 PlaylistWindow::_UpdateTotalDuration(bigtime_t duration) 471 { 472 BAutolock lock(this); 473 474 char buffer[64]; 475 duration /= 1000000; 476 duration_to_string(duration, buffer, sizeof(buffer)); 477 478 BString text; 479 text.SetToFormat(B_TRANSLATE("Total duration: %s"), buffer); 480 481 fTotalDuration->SetText(text.String()); 482 } 483 484 485 // #pragma mark - 486 487 488 PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent) 489 : 490 PlaylistObserver(this), 491 fKnown(20, true), 492 fTotalDuration(0), 493 fParent(parent) 494 { 495 Run(); 496 } 497 498 499 PlaylistWindow::DurationListener::~DurationListener() 500 { 501 } 502 503 504 void 505 PlaylistWindow::DurationListener::MessageReceived(BMessage* message) 506 { 507 switch (message->what) { 508 case MSG_PLAYLIST_ITEM_ADDED: 509 { 510 void* item; 511 int32 index; 512 513 int32 currentItem = 0; 514 while (message->FindPointer("item", currentItem, &item) == B_OK 515 && message->FindInt32("index", currentItem, &index) == B_OK) { 516 _HandleItemAdded(static_cast<PlaylistItem*>(item), index); 517 ++currentItem; 518 } 519 520 break; 521 } 522 523 case MSG_PLAYLIST_ITEM_REMOVED: 524 { 525 int32 index; 526 527 if (message->FindInt32("index", &index) == B_OK) { 528 _HandleItemRemoved(index); 529 } 530 531 break; 532 } 533 534 default: 535 BLooper::MessageReceived(message); 536 break; 537 } 538 } 539 540 541 bigtime_t 542 PlaylistWindow::DurationListener::TotalDuration() 543 { 544 return fTotalDuration; 545 } 546 547 548 void 549 PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item, 550 int32 index) 551 { 552 bigtime_t duration = item->Duration(); 553 fTotalDuration += duration; 554 fParent._UpdateTotalDuration(fTotalDuration); 555 fKnown.AddItem(new bigtime_t(duration), index); 556 } 557 558 559 void 560 PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index) 561 { 562 bigtime_t* deleted = fKnown.RemoveItemAt(index); 563 if (deleted == NULL) 564 return; 565 566 fTotalDuration -= *deleted; 567 fParent._UpdateTotalDuration(fTotalDuration); 568 569 delete deleted; 570 } 571 572