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 56static void 57display_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 66static void 67display_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 78PlaylistWindow::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 136PlaylistWindow::~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 151bool 152PlaylistWindow::QuitRequested() 153{ 154 Hide(); 155 return false; 156} 157 158 159void 160PlaylistWindow::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 269void 270PlaylistWindow::_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 320void 321PlaylistWindow::_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 343void 344PlaylistWindow::_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 380void 381PlaylistWindow::_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 395void 396PlaylistWindow::_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 454void 455PlaylistWindow::_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 469void 470PlaylistWindow::_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 488PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent) 489 : 490 PlaylistObserver(this), 491 fKnown(20, true), 492 fTotalDuration(0), 493 fParent(parent) 494{ 495 Run(); 496} 497 498 499PlaylistWindow::DurationListener::~DurationListener() 500{ 501} 502 503 504void 505PlaylistWindow::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 541bigtime_t 542PlaylistWindow::DurationListener::TotalDuration() 543{ 544 return fTotalDuration; 545} 546 547 548void 549PlaylistWindow::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 559void 560PlaylistWindow::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