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