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
display_save_alert(const char * message)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
display_save_alert(status_t error)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
PlaylistWindow(BRect frame,Playlist * playlist,Controller * controller)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
~PlaylistWindow()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
QuitRequested()152 PlaylistWindow::QuitRequested()
153 {
154 Hide();
155 return false;
156 }
157
158
159 void
MessageReceived(BMessage * message)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
_CreateMenu(BRect & frame)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
_ObjectChanged(const Notifier * object)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
_SavePlaylist(const BMessage * message)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
_SavePlaylist(const entry_ref & ref)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
_SavePlaylist(BEntry & origEntry,BEntry & tempEntry,const char * finalName)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
_QueryInitialDurations()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
_UpdateTotalDuration(bigtime_t duration)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
DurationListener(PlaylistWindow & parent)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
~DurationListener()499 PlaylistWindow::DurationListener::~DurationListener()
500 {
501 }
502
503
504 void
MessageReceived(BMessage * message)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
TotalDuration()542 PlaylistWindow::DurationListener::TotalDuration()
543 {
544 return fTotalDuration;
545 }
546
547
548 void
_HandleItemAdded(PlaylistItem * item,int32 index)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
_HandleItemRemoved(int32 index)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