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 35 #include "CommandStack.h" 36 #include "MainApp.h" 37 #include "PlaylistListView.h" 38 #include "RWLocker.h" 39 40 41 #undef B_TRANSLATION_CONTEXT 42 #define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow" 43 44 45 // TODO: 46 // Maintaining a playlist file on disk is a bit tricky. The playlist ref should 47 // be discarded when the user 48 // * loads a new playlist via Open, 49 // * loads a new playlist via dropping it on the MainWindow, 50 // * loads a new playlist via dropping it into the ListView while replacing 51 // the contents, 52 // * replacing the contents by other stuff. 53 54 55 static void 56 display_save_alert(const char* message) 57 { 58 BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message, 59 B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT); 60 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 61 alert->Go(NULL); 62 } 63 64 65 static void 66 display_save_alert(status_t error) 67 { 68 BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: ")); 69 errorMessage << strerror(error); 70 display_save_alert(errorMessage.String()); 71 } 72 73 74 // #pragma mark - 75 76 77 PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist, 78 Controller* controller) 79 : 80 BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK, 81 B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS), 82 fPlaylist(playlist), 83 fLocker(new RWLocker("command stack lock")), 84 fCommandStack(new CommandStack(fLocker)), 85 fCommandStackListener(this) 86 { 87 frame = Bounds(); 88 89 _CreateMenu(frame); 90 // will adjust frame to account for menubar 91 92 frame.right -= B_V_SCROLL_BAR_WIDTH; 93 fListView = new PlaylistListView(frame, playlist, controller, 94 fCommandStack); 95 96 BScrollView* scrollView = new BScrollView("playlist scrollview", fListView, 97 B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER); 98 99 fTopView = scrollView; 100 AddChild(fTopView); 101 102 // small visual tweak 103 if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) { 104 // make it so the frame of the menubar is also the frame of 105 // the scroll bar (appears to be) 106 scrollBar->MoveBy(0, -1); 107 scrollBar->ResizeBy(0, -(B_H_SCROLL_BAR_HEIGHT - 2)); 108 } 109 110 fCommandStack->AddListener(&fCommandStackListener); 111 _ObjectChanged(fCommandStack); 112 } 113 114 115 PlaylistWindow::~PlaylistWindow() 116 { 117 // give listeners a chance to detach themselves 118 fTopView->RemoveSelf(); 119 delete fTopView; 120 121 fCommandStack->RemoveListener(&fCommandStackListener); 122 delete fCommandStack; 123 delete fLocker; 124 } 125 126 127 bool 128 PlaylistWindow::QuitRequested() 129 { 130 Hide(); 131 return false; 132 } 133 134 135 void 136 PlaylistWindow::MessageReceived(BMessage* message) 137 { 138 switch (message->what) { 139 case B_MODIFIERS_CHANGED: 140 if (LastMouseMovedView()) 141 PostMessage(message, LastMouseMovedView()); 142 break; 143 144 case B_UNDO: 145 fCommandStack->Undo(); 146 break; 147 case B_REDO: 148 fCommandStack->Redo(); 149 break; 150 151 case MSG_OBJECT_CHANGED: { 152 Notifier* notifier; 153 if (message->FindPointer("object", (void**)¬ifier) == B_OK) 154 _ObjectChanged(notifier); 155 break; 156 } 157 158 case B_REFS_RECEIVED: 159 // Used for when we open a playlist from playlist window 160 if (!message->HasInt32("append_index")) { 161 message->AddInt32("append_index", 162 APPEND_INDEX_REPLACE_PLAYLIST); 163 } 164 // supposed to fall through 165 case B_SIMPLE_DATA: 166 { 167 // only accept this message when it comes from the 168 // player window, _not_ when it is dropped in this window 169 // outside of the playlist! 170 int32 appendIndex; 171 if (message->FindInt32("append_index", &appendIndex) == B_OK) 172 fListView->RefsReceived(message, appendIndex); 173 break; 174 } 175 176 case M_PLAYLIST_OPEN: 177 { 178 BMessenger target(this); 179 BMessage result(B_REFS_RECEIVED); 180 BMessage appMessage(M_SHOW_OPEN_PANEL); 181 appMessage.AddMessenger("target", target); 182 appMessage.AddMessage("message", &result); 183 appMessage.AddString("title", B_TRANSLATE("Open Playlist")); 184 appMessage.AddString("label", B_TRANSLATE("Open")); 185 be_app->PostMessage(&appMessage); 186 break; 187 } 188 189 case M_PLAYLIST_SAVE: 190 if (fSavedPlaylistRef != entry_ref()) { 191 _SavePlaylist(fSavedPlaylistRef); 192 break; 193 } 194 // supposed to fall through 195 case M_PLAYLIST_SAVE_AS: 196 { 197 BMessenger target(this); 198 BMessage result(M_PLAYLIST_SAVE_RESULT); 199 BMessage appMessage(M_SHOW_SAVE_PANEL); 200 appMessage.AddMessenger("target", target); 201 appMessage.AddMessage("message", &result); 202 appMessage.AddString("title", B_TRANSLATE("Save Playlist")); 203 appMessage.AddString("label", B_TRANSLATE("Save")); 204 be_app->PostMessage(&appMessage); 205 break; 206 } 207 208 case M_PLAYLIST_SAVE_RESULT: 209 _SavePlaylist(message); 210 break; 211 212 case B_SELECT_ALL: 213 fListView->SelectAll(); 214 break; 215 216 case M_PLAYLIST_RANDOMIZE: 217 fListView->Randomize(); 218 break; 219 case M_PLAYLIST_REMOVE: 220 fListView->RemoveSelected(); 221 break; 222 case M_PLAYLIST_MOVE_TO_TRASH: 223 { 224 int32 index; 225 if (message->FindInt32("playlist index", &index) == B_OK) 226 fListView->RemoveToTrash(index); 227 else 228 fListView->RemoveSelectionToTrash(); 229 break; 230 } 231 default: 232 BWindow::MessageReceived(message); 233 break; 234 } 235 } 236 237 238 // #pragma mark - 239 240 241 void 242 PlaylistWindow::_CreateMenu(BRect& frame) 243 { 244 frame.bottom = 15; 245 BMenuBar* menuBar = new BMenuBar(frame, "main menu"); 246 BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist")); 247 menuBar->AddItem(fileMenu); 248 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS), 249 new BMessage(M_PLAYLIST_OPEN), 'O')); 250 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS), 251 new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY)); 252 // fileMenu->AddItem(new BMenuItem("Save", 253 // new BMessage(M_PLAYLIST_SAVE), 'S')); 254 255 fileMenu->AddSeparatorItem(); 256 257 fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"), 258 new BMessage(B_QUIT_REQUESTED), 'W')); 259 260 BMenu* editMenu = new BMenu(B_TRANSLATE("Edit")); 261 fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z'); 262 editMenu->AddItem(fUndoMI); 263 fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z', 264 B_SHIFT_KEY); 265 editMenu->AddItem(fRedoMI); 266 editMenu->AddSeparatorItem(); 267 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"), 268 new BMessage(B_SELECT_ALL), 'A')); 269 editMenu->AddSeparatorItem(); 270 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"), 271 new BMessage(M_PLAYLIST_RANDOMIZE), 'R')); 272 editMenu->AddSeparatorItem(); 273 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"), 274 new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/)); 275 // TODO: See if we can support the modifier-less B_DELETE 276 // and draw it properly too. B_NO_MODIFIER? 277 editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"), 278 new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T')); 279 280 menuBar->AddItem(editMenu); 281 282 AddChild(menuBar); 283 fileMenu->SetTargetForItems(this); 284 editMenu->SetTargetForItems(this); 285 286 menuBar->ResizeToPreferred(); 287 frame = Bounds(); 288 frame.top = menuBar->Frame().bottom + 1; 289 } 290 291 292 void 293 PlaylistWindow::_ObjectChanged(const Notifier* object) 294 { 295 if (object == fCommandStack) { 296 // relable Undo item and update enabled status 297 BString label(B_TRANSLATE("Undo")); 298 fUndoMI->SetEnabled(fCommandStack->GetUndoName(label)); 299 if (fUndoMI->IsEnabled()) 300 fUndoMI->SetLabel(label.String()); 301 else 302 fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>")); 303 304 // relable Redo item and update enabled status 305 label.SetTo(B_TRANSLATE("Redo")); 306 fRedoMI->SetEnabled(fCommandStack->GetRedoName(label)); 307 if (fRedoMI->IsEnabled()) 308 fRedoMI->SetLabel(label.String()); 309 else 310 fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>")); 311 } 312 } 313 314 315 void 316 PlaylistWindow::_SavePlaylist(const BMessage* message) 317 { 318 entry_ref ref; 319 const char* name; 320 if (message->FindRef("directory", &ref) != B_OK 321 || message->FindString("name", &name) != B_OK) { 322 display_save_alert(B_TRANSLATE("Internal error (malformed message). " 323 "Saving the playlist failed.")); 324 return; 325 } 326 327 BString tempName(name); 328 tempName << system_time(); 329 330 BPath origPath(&ref); 331 BPath tempPath(&ref); 332 if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK 333 || origPath.Append(name) != B_OK 334 || tempPath.Append(tempName.String()) != B_OK) { 335 display_save_alert(B_TRANSLATE("Internal error (out of memory). " 336 "Saving the playlist failed.")); 337 return; 338 } 339 340 BEntry origEntry(origPath.Path()); 341 BEntry tempEntry(tempPath.Path()); 342 if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) { 343 display_save_alert(B_TRANSLATE("Internal error (out of memory). " 344 "Saving the playlist failed.")); 345 return; 346 } 347 348 _SavePlaylist(origEntry, tempEntry, name); 349 } 350 351 352 void 353 PlaylistWindow::_SavePlaylist(const entry_ref& ref) 354 { 355 BString tempName(ref.name); 356 tempName << system_time(); 357 entry_ref tempRef(ref); 358 tempRef.set_name(tempName.String()); 359 360 BEntry origEntry(&ref); 361 BEntry tempEntry(&tempRef); 362 363 _SavePlaylist(origEntry, tempEntry, ref.name); 364 } 365 366 367 void 368 PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry, 369 const char* finalName) 370 { 371 class TempEntryRemover { 372 public: 373 TempEntryRemover(BEntry* entry) 374 : fEntry(entry) 375 { 376 } 377 ~TempEntryRemover() 378 { 379 if (fEntry) 380 fEntry->Remove(); 381 } 382 void Detach() 383 { 384 fEntry = NULL; 385 } 386 private: 387 BEntry* fEntry; 388 } remover(&tempEntry); 389 390 BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY); 391 if (file.InitCheck() != B_OK) { 392 BString errorMessage(B_TRANSLATE( 393 "Saving the playlist failed:\n\nError: ")); 394 errorMessage << strerror(file.InitCheck()); 395 display_save_alert(errorMessage.String()); 396 return; 397 } 398 399 AutoLocker<Playlist> lock(fPlaylist); 400 if (!lock.IsLocked()) { 401 display_save_alert(B_TRANSLATE("Internal error (locking failed). " 402 "Saving the playlist failed.")); 403 return; 404 } 405 406 status_t ret = fPlaylist->Flatten(&file); 407 if (ret != B_OK) { 408 display_save_alert(ret); 409 return; 410 } 411 lock.Unlock(); 412 413 if (origEntry.Exists()) { 414 // TODO: copy attributes 415 } 416 417 // clobber original entry, if it exists 418 tempEntry.Rename(finalName, true); 419 remover.Detach(); 420 421 BNodeInfo info(&file); 422 info.SetType("application/x-vnd.haiku-playlist"); 423 } 424 425