PlaylistWindow.cpp revision 8487f27d
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 <MediaFile.h>
26#include <MediaTrack.h>
27#include <Menu.h>
28#include <MenuBar.h>
29#include <MenuItem.h>
30#include <NodeInfo.h>
31#include <Path.h>
32#include <Roster.h>
33#include <ScrollBar.h>
34#include <ScrollView.h>
35#include <String.h>
36#include <StringView.h>
37
38#include "AudioTrackSupplier.h"
39#include "CommandStack.h"
40#include "DurationToString.h"
41#include "MainApp.h"
42#include "PlaylistListView.h"
43#include "RWLocker.h"
44#include "TrackSupplier.h"
45#include "VideoTrackSupplier.h"
46
47#undef B_TRANSLATION_CONTEXT
48#define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow"
49
50
51// TODO:
52// Maintaining a playlist file on disk is a bit tricky. The playlist ref should
53// be discarded when the user
54// * loads a new playlist via Open,
55// * loads a new playlist via dropping it on the MainWindow,
56// * loads a new playlist via dropping it into the ListView while replacing
57//   the contents,
58// * replacing the contents by other stuff.
59
60
61static void
62display_save_alert(const char* message)
63{
64	BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message,
65		B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
66	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
67	alert->Go(NULL);
68}
69
70
71static void
72display_save_alert(status_t error)
73{
74	BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: "));
75	errorMessage << strerror(error);
76	display_save_alert(errorMessage.String());
77}
78
79
80// #pragma mark -
81
82
83PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist,
84		Controller* controller)
85	:
86	BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK,
87		B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS),
88	fPlaylist(playlist),
89	fLocker(new RWLocker("command stack lock")),
90	fCommandStack(new CommandStack(fLocker)),
91	fCommandStackListener(this),
92	fDurationListener(new DurationListener(*this))
93{
94	frame = Bounds();
95
96	_CreateMenu(frame);
97		// will adjust frame to account for menubar
98
99	frame.right -= B_V_SCROLL_BAR_WIDTH;
100	frame.bottom -= B_H_SCROLL_BAR_HEIGHT;
101	fListView = new PlaylistListView(frame, playlist, controller,
102		fCommandStack);
103
104	BScrollView* scrollView = new BScrollView("playlist scrollview", fListView,
105		B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER);
106
107	fTopView = 	scrollView;
108	AddChild(fTopView);
109
110	// small visual tweak
111	if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) {
112		// make it so the frame of the menubar is also the frame of
113		// the scroll bar (appears to be)
114		scrollBar->MoveBy(0, -1);
115		scrollBar->ResizeBy(0, 2);
116	}
117
118	frame.top += frame.Height();
119	frame.bottom += B_H_SCROLL_BAR_HEIGHT;
120
121	fTotalDuration = new BStringView(frame, "fDuration", "",
122		B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT);
123	AddChild(fTotalDuration);
124
125	_UpdateTotalDuration(0);
126
127	{
128		BAutolock _(fPlaylist);
129
130		_QueryInitialDurations();
131		fPlaylist->AddListener(fDurationListener);
132	}
133
134	fCommandStack->AddListener(&fCommandStackListener);
135	_ObjectChanged(fCommandStack);
136}
137
138
139PlaylistWindow::~PlaylistWindow()
140{
141	// give listeners a chance to detach themselves
142	fTopView->RemoveSelf();
143	delete fTopView;
144
145	fCommandStack->RemoveListener(&fCommandStackListener);
146	delete fCommandStack;
147	delete fLocker;
148
149	fPlaylist->RemoveListener(fDurationListener);
150	BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED);
151}
152
153
154bool
155PlaylistWindow::QuitRequested()
156{
157	Hide();
158	return false;
159}
160
161
162void
163PlaylistWindow::MessageReceived(BMessage* message)
164{
165	switch (message->what) {
166		case B_MODIFIERS_CHANGED:
167			if (LastMouseMovedView())
168				PostMessage(message, LastMouseMovedView());
169			break;
170
171		case B_UNDO:
172			fCommandStack->Undo();
173			break;
174		case B_REDO:
175			fCommandStack->Redo();
176			break;
177
178		case MSG_OBJECT_CHANGED: {
179			Notifier* notifier;
180			if (message->FindPointer("object", (void**)&notifier) == B_OK)
181				_ObjectChanged(notifier);
182			break;
183		}
184
185		case B_REFS_RECEIVED:
186			// Used for when we open a playlist from playlist window
187			if (!message->HasInt32("append_index")) {
188				message->AddInt32("append_index",
189					APPEND_INDEX_REPLACE_PLAYLIST);
190			}
191			// supposed to fall through
192		case B_SIMPLE_DATA:
193		{
194			// only accept this message when it comes from the
195			// player window, _not_ when it is dropped in this window
196			// outside of the playlist!
197			int32 appendIndex;
198			if (message->FindInt32("append_index", &appendIndex) == B_OK)
199				fListView->RefsReceived(message, appendIndex);
200			break;
201		}
202
203		case M_PLAYLIST_OPEN:
204		{
205			BMessenger target(this);
206			BMessage result(B_REFS_RECEIVED);
207			BMessage appMessage(M_SHOW_OPEN_PANEL);
208			appMessage.AddMessenger("target", target);
209			appMessage.AddMessage("message", &result);
210			appMessage.AddString("title", B_TRANSLATE("Open Playlist"));
211			appMessage.AddString("label", B_TRANSLATE("Open"));
212			be_app->PostMessage(&appMessage);
213			break;
214		}
215
216		case M_PLAYLIST_SAVE:
217			if (fSavedPlaylistRef != entry_ref()) {
218				_SavePlaylist(fSavedPlaylistRef);
219				break;
220			}
221			// supposed to fall through
222		case M_PLAYLIST_SAVE_AS:
223		{
224			BMessenger target(this);
225			BMessage result(M_PLAYLIST_SAVE_RESULT);
226			BMessage appMessage(M_SHOW_SAVE_PANEL);
227			appMessage.AddMessenger("target", target);
228			appMessage.AddMessage("message", &result);
229			appMessage.AddString("title", B_TRANSLATE("Save Playlist"));
230			appMessage.AddString("label", B_TRANSLATE("Save"));
231			be_app->PostMessage(&appMessage);
232			break;
233		}
234
235		case M_PLAYLIST_SAVE_RESULT:
236			_SavePlaylist(message);
237			break;
238
239		case B_SELECT_ALL:
240			fListView->SelectAll();
241			break;
242
243		case M_PLAYLIST_RANDOMIZE:
244			fListView->Randomize();
245			break;
246		case M_PLAYLIST_REMOVE:
247			fListView->RemoveSelected();
248			break;
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		default:
259			BWindow::MessageReceived(message);
260			break;
261	}
262}
263
264
265// #pragma mark -
266
267
268void
269PlaylistWindow::_CreateMenu(BRect& frame)
270{
271	frame.bottom = 15;
272	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
273	BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist"));
274	menuBar->AddItem(fileMenu);
275	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS),
276		new BMessage(M_PLAYLIST_OPEN), 'O'));
277	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS),
278		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
279//	fileMenu->AddItem(new BMenuItem("Save",
280//		new BMessage(M_PLAYLIST_SAVE), 'S'));
281
282	fileMenu->AddSeparatorItem();
283
284	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
285		new BMessage(B_QUIT_REQUESTED), 'W'));
286
287	BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));
288	fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z');
289	editMenu->AddItem(fUndoMI);
290	fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z',
291		B_SHIFT_KEY);
292	editMenu->AddItem(fRedoMI);
293	editMenu->AddSeparatorItem();
294	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"),
295		new BMessage(B_SELECT_ALL), 'A'));
296	editMenu->AddSeparatorItem();
297	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"),
298		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
299	editMenu->AddSeparatorItem();
300	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"),
301		new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
302			// TODO: See if we can support the modifier-less B_DELETE
303			// and draw it properly too. B_NO_MODIFIER?
304	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"),
305		new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T'));
306
307	menuBar->AddItem(editMenu);
308
309	AddChild(menuBar);
310	fileMenu->SetTargetForItems(this);
311	editMenu->SetTargetForItems(this);
312
313	menuBar->ResizeToPreferred();
314	frame = Bounds();
315	frame.top = menuBar->Frame().bottom + 1;
316}
317
318
319void
320PlaylistWindow::_ObjectChanged(const Notifier* object)
321{
322	if (object == fCommandStack) {
323		// relable Undo item and update enabled status
324		BString label(B_TRANSLATE("Undo"));
325		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
326		if (fUndoMI->IsEnabled())
327			fUndoMI->SetLabel(label.String());
328		else
329			fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>"));
330
331		// relable Redo item and update enabled status
332		label.SetTo(B_TRANSLATE("Redo"));
333		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
334		if (fRedoMI->IsEnabled())
335			fRedoMI->SetLabel(label.String());
336		else
337			fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>"));
338	}
339}
340
341
342void
343PlaylistWindow::_SavePlaylist(const BMessage* message)
344{
345	entry_ref ref;
346	const char* name;
347	if (message->FindRef("directory", &ref) != B_OK
348		|| message->FindString("name", &name) != B_OK) {
349		display_save_alert(B_TRANSLATE("Internal error (malformed message). "
350			"Saving the playlist failed."));
351		return;
352	}
353
354	BString tempName(name);
355	tempName << system_time();
356
357	BPath origPath(&ref);
358	BPath tempPath(&ref);
359	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
360		|| origPath.Append(name) != B_OK
361		|| tempPath.Append(tempName.String()) != B_OK) {
362		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
363			"Saving the playlist failed."));
364		return;
365	}
366
367	BEntry origEntry(origPath.Path());
368	BEntry tempEntry(tempPath.Path());
369	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
370		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
371			"Saving the playlist failed."));
372		return;
373	}
374
375	_SavePlaylist(origEntry, tempEntry, name);
376}
377
378
379void
380PlaylistWindow::_SavePlaylist(const entry_ref& ref)
381{
382	BString tempName(ref.name);
383	tempName << system_time();
384	entry_ref tempRef(ref);
385	tempRef.set_name(tempName.String());
386
387	BEntry origEntry(&ref);
388	BEntry tempEntry(&tempRef);
389
390	_SavePlaylist(origEntry, tempEntry, ref.name);
391}
392
393
394void
395PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
396	const char* finalName)
397{
398	class TempEntryRemover {
399	public:
400		TempEntryRemover(BEntry* entry)
401			: fEntry(entry)
402		{
403		}
404		~TempEntryRemover()
405		{
406			if (fEntry)
407				fEntry->Remove();
408		}
409		void Detach()
410		{
411			fEntry = NULL;
412		}
413	private:
414		BEntry* fEntry;
415	} remover(&tempEntry);
416
417	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
418	if (file.InitCheck() != B_OK) {
419		BString errorMessage(B_TRANSLATE(
420			"Saving the playlist failed:\n\nError: "));
421		errorMessage << strerror(file.InitCheck());
422		display_save_alert(errorMessage.String());
423		return;
424	}
425
426	AutoLocker<Playlist> lock(fPlaylist);
427	if (!lock.IsLocked()) {
428		display_save_alert(B_TRANSLATE("Internal error (locking failed). "
429			"Saving the playlist failed."));
430		return;
431	}
432
433	status_t ret = fPlaylist->Flatten(&file);
434	if (ret != B_OK) {
435		display_save_alert(ret);
436		return;
437	}
438	lock.Unlock();
439
440	if (origEntry.Exists()) {
441		// TODO: copy attributes
442	}
443
444	// clobber original entry, if it exists
445	tempEntry.Rename(finalName, true);
446	remover.Detach();
447
448	BNodeInfo info(&file);
449	info.SetType("application/x-vnd.haiku-playlist");
450}
451
452
453void
454PlaylistWindow::_QueryInitialDurations()
455{
456	BAutolock lock(fPlaylist);
457
458	BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED);
459	for (int32 i = 0; i < fPlaylist->CountItems(); i++) {
460		addMessage.AddPointer("item", fPlaylist->ItemAt(i));
461		addMessage.AddInt32("index", i);
462	}
463
464	BMessenger(fDurationListener).SendMessage(&addMessage);
465}
466
467
468void
469PlaylistWindow::_UpdateTotalDuration(bigtime_t duration)
470{
471	BAutolock lock(this);
472
473	char buffer[64];
474	duration /= 1000000;
475	duration_to_string(duration, buffer, sizeof(buffer));
476
477	BString text;
478	text.SetToFormat(B_TRANSLATE("Total duration : %s"), buffer);
479
480	fTotalDuration->SetText(text.String());
481}
482
483
484// #pragma mark -
485
486
487PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
488	:
489	PlaylistObserver(this),
490	fKnown(20, true),
491	fTotalDuration(0),
492	fParent(parent)
493{
494	Run();
495}
496
497
498PlaylistWindow::DurationListener::~DurationListener()
499{
500}
501
502
503void
504PlaylistWindow::DurationListener::MessageReceived(BMessage* message)
505{
506	switch (message->what) {
507		case MSG_PLAYLIST_ITEM_ADDED:
508		{
509			void* item;
510			int32 index;
511
512			int32 currentItem = 0;
513			while (message->FindPointer("item", currentItem, &item) == B_OK
514				&& message->FindInt32("index", currentItem, &index) == B_OK) {
515				_HandleItemAdded(static_cast<PlaylistItem*>(item), index);
516				++currentItem;
517			}
518
519			break;
520		}
521
522		case MSG_PLAYLIST_ITEM_REMOVED:
523		{
524			int32 index;
525
526			if (message->FindInt32("index", &index) == B_OK) {
527				_HandleItemRemoved(index);
528			}
529
530			break;
531		}
532
533		default:
534			BLooper::MessageReceived(message);
535			break;
536	}
537}
538
539
540bigtime_t
541PlaylistWindow::DurationListener::TotalDuration()
542{
543	return fTotalDuration;
544}
545
546
547void
548PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item,
549	int32 index)
550{
551	bigtime_t duration = _DetermineItemDuration(item);
552	fTotalDuration += duration;
553	fParent._UpdateTotalDuration(fTotalDuration);
554	fKnown.AddItem(new bigtime_t(duration), index);
555}
556
557
558void
559PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index)
560{
561	bigtime_t* deleted = fKnown.RemoveItemAt(index);
562	if (deleted == NULL)
563		return;
564
565	fTotalDuration -= *deleted;
566	fParent._UpdateTotalDuration(fTotalDuration);
567
568	delete deleted;
569}
570
571
572bigtime_t
573PlaylistWindow::DurationListener::_DetermineItemDuration(PlaylistItem* item)
574{
575	bigtime_t duration;
576	if (item->GetAttribute(PlaylistItem::ATTR_INT64_DURATION, duration) == B_OK)
577		return duration;
578
579	// We have to find out the duration ourselves
580	if (FilePlaylistItem* file = dynamic_cast<FilePlaylistItem*>(item)) {
581		// We are dealing with a file
582		BMediaFile mediaFile(&file->Ref());
583
584		if (mediaFile.InitCheck() != B_OK || mediaFile.CountTracks() < 1)
585			return 0;
586
587		duration =  mediaFile.TrackAt(0)->Duration();
588	} else {
589		// Not a file, so fall back to the generic TrackSupplier solution
590		TrackSupplier* supplier = item->CreateTrackSupplier();
591
592		AudioTrackSupplier* au = supplier->CreateAudioTrackForIndex(0);
593		VideoTrackSupplier* vi = supplier->CreateVideoTrackForIndex(0);
594
595		duration = max_c(au == NULL ? 0 : au->Duration(),
596			vi == NULL ? 0 : vi->Duration());
597
598		delete vi;
599		delete au;
600		delete supplier;
601	}
602
603	// Store the duration for later use
604	item->SetAttribute(PlaylistItem::ATTR_INT64_DURATION, duration);
605
606	return duration;
607}
608
609