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