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
56static void
57display_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
66static void
67display_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
78PlaylistWindow::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
136PlaylistWindow::~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
151bool
152PlaylistWindow::QuitRequested()
153{
154	Hide();
155	return false;
156}
157
158
159void
160PlaylistWindow::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**)&notifier) == 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
269void
270PlaylistWindow::_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
320void
321PlaylistWindow::_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
343void
344PlaylistWindow::_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
380void
381PlaylistWindow::_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
395void
396PlaylistWindow::_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
454void
455PlaylistWindow::_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
469void
470PlaylistWindow::_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
488PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
489	:
490	PlaylistObserver(this),
491	fKnown(20, true),
492	fTotalDuration(0),
493	fParent(parent)
494{
495	Run();
496}
497
498
499PlaylistWindow::DurationListener::~DurationListener()
500{
501}
502
503
504void
505PlaylistWindow::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
541bigtime_t
542PlaylistWindow::DurationListener::TotalDuration()
543{
544	return fTotalDuration;
545}
546
547
548void
549PlaylistWindow::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
559void
560PlaylistWindow::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