PlaylistWindow.cpp revision b5387eff
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	AddChild(fTotalDuration);
119
120	_UpdateTotalDuration(0);
121
122	{
123		BAutolock _(fPlaylist);
124
125		_QueryInitialDurations();
126		fPlaylist->AddListener(fDurationListener);
127	}
128
129	fCommandStack->AddListener(&fCommandStackListener);
130	_ObjectChanged(fCommandStack);
131}
132
133
134PlaylistWindow::~PlaylistWindow()
135{
136	// give listeners a chance to detach themselves
137	fTopView->RemoveSelf();
138	delete fTopView;
139
140	fCommandStack->RemoveListener(&fCommandStackListener);
141	delete fCommandStack;
142	delete fLocker;
143
144	fPlaylist->RemoveListener(fDurationListener);
145	BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED);
146}
147
148
149bool
150PlaylistWindow::QuitRequested()
151{
152	Hide();
153	return false;
154}
155
156
157void
158PlaylistWindow::MessageReceived(BMessage* message)
159{
160	switch (message->what) {
161		case B_MODIFIERS_CHANGED:
162			if (LastMouseMovedView())
163				PostMessage(message, LastMouseMovedView());
164			break;
165
166		case B_UNDO:
167			fCommandStack->Undo();
168			break;
169		case B_REDO:
170			fCommandStack->Redo();
171			break;
172
173		case MSG_OBJECT_CHANGED: {
174			Notifier* notifier;
175			if (message->FindPointer("object", (void**)&notifier) == B_OK)
176				_ObjectChanged(notifier);
177			break;
178		}
179
180		case B_REFS_RECEIVED:
181			// Used for when we open a playlist from playlist window
182			if (!message->HasInt32("append_index")) {
183				message->AddInt32("append_index",
184					APPEND_INDEX_REPLACE_PLAYLIST);
185			}
186			// supposed to fall through
187		case B_SIMPLE_DATA:
188		{
189			// only accept this message when it comes from the
190			// player window, _not_ when it is dropped in this window
191			// outside of the playlist!
192			int32 appendIndex;
193			if (message->FindInt32("append_index", &appendIndex) == B_OK)
194				fListView->RefsReceived(message, appendIndex);
195			break;
196		}
197
198		case M_PLAYLIST_OPEN:
199		{
200			BMessenger target(this);
201			BMessage result(B_REFS_RECEIVED);
202			BMessage appMessage(M_SHOW_OPEN_PANEL);
203			appMessage.AddMessenger("target", target);
204			appMessage.AddMessage("message", &result);
205			appMessage.AddString("title", B_TRANSLATE("Open Playlist"));
206			appMessage.AddString("label", B_TRANSLATE("Open"));
207			be_app->PostMessage(&appMessage);
208			break;
209		}
210
211		case M_PLAYLIST_SAVE:
212			if (fSavedPlaylistRef != entry_ref()) {
213				_SavePlaylist(fSavedPlaylistRef);
214				break;
215			}
216			// supposed to fall through
217		case M_PLAYLIST_SAVE_AS:
218		{
219			BMessenger target(this);
220			BMessage result(M_PLAYLIST_SAVE_RESULT);
221			BMessage appMessage(M_SHOW_SAVE_PANEL);
222			appMessage.AddMessenger("target", target);
223			appMessage.AddMessage("message", &result);
224			appMessage.AddString("title", B_TRANSLATE("Save Playlist"));
225			appMessage.AddString("label", B_TRANSLATE("Save"));
226			be_app->PostMessage(&appMessage);
227			break;
228		}
229
230		case M_PLAYLIST_SAVE_RESULT:
231			_SavePlaylist(message);
232			break;
233
234		case B_SELECT_ALL:
235			fListView->SelectAll();
236			break;
237
238		case M_PLAYLIST_RANDOMIZE:
239			fListView->Randomize();
240			break;
241		case M_PLAYLIST_REMOVE:
242			fListView->RemoveSelected();
243			break;
244		case M_PLAYLIST_MOVE_TO_TRASH:
245		{
246			int32 index;
247			if (message->FindInt32("playlist index", &index) == B_OK)
248				fListView->RemoveToTrash(index);
249			else
250				fListView->RemoveSelectionToTrash();
251			break;
252		}
253		default:
254			BWindow::MessageReceived(message);
255			break;
256	}
257}
258
259
260// #pragma mark -
261
262
263void
264PlaylistWindow::_CreateMenu(BRect& frame)
265{
266	frame.bottom = 15;
267	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
268	BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist"));
269	menuBar->AddItem(fileMenu);
270	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS),
271		new BMessage(M_PLAYLIST_OPEN), 'O'));
272	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS),
273		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
274//	fileMenu->AddItem(new BMenuItem("Save",
275//		new BMessage(M_PLAYLIST_SAVE), 'S'));
276
277	fileMenu->AddSeparatorItem();
278
279	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
280		new BMessage(B_QUIT_REQUESTED), 'W'));
281
282	BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));
283	fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z');
284	editMenu->AddItem(fUndoMI);
285	fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z',
286		B_SHIFT_KEY);
287	editMenu->AddItem(fRedoMI);
288	editMenu->AddSeparatorItem();
289	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"),
290		new BMessage(B_SELECT_ALL), 'A'));
291	editMenu->AddSeparatorItem();
292	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"),
293		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
294	editMenu->AddSeparatorItem();
295	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"),
296		new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
297			// TODO: See if we can support the modifier-less B_DELETE
298			// and draw it properly too. B_NO_MODIFIER?
299	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"),
300		new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T'));
301
302	menuBar->AddItem(editMenu);
303
304	AddChild(menuBar);
305	fileMenu->SetTargetForItems(this);
306	editMenu->SetTargetForItems(this);
307
308	menuBar->ResizeToPreferred();
309	frame = Bounds();
310	frame.top = menuBar->Frame().bottom + 1;
311}
312
313
314void
315PlaylistWindow::_ObjectChanged(const Notifier* object)
316{
317	if (object == fCommandStack) {
318		// relable Undo item and update enabled status
319		BString label(B_TRANSLATE("Undo"));
320		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
321		if (fUndoMI->IsEnabled())
322			fUndoMI->SetLabel(label.String());
323		else
324			fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>"));
325
326		// relable Redo item and update enabled status
327		label.SetTo(B_TRANSLATE("Redo"));
328		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
329		if (fRedoMI->IsEnabled())
330			fRedoMI->SetLabel(label.String());
331		else
332			fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>"));
333	}
334}
335
336
337void
338PlaylistWindow::_SavePlaylist(const BMessage* message)
339{
340	entry_ref ref;
341	const char* name;
342	if (message->FindRef("directory", &ref) != B_OK
343		|| message->FindString("name", &name) != B_OK) {
344		display_save_alert(B_TRANSLATE("Internal error (malformed message). "
345			"Saving the playlist failed."));
346		return;
347	}
348
349	BString tempName(name);
350	tempName << system_time();
351
352	BPath origPath(&ref);
353	BPath tempPath(&ref);
354	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
355		|| origPath.Append(name) != B_OK
356		|| tempPath.Append(tempName.String()) != B_OK) {
357		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
358			"Saving the playlist failed."));
359		return;
360	}
361
362	BEntry origEntry(origPath.Path());
363	BEntry tempEntry(tempPath.Path());
364	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
365		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
366			"Saving the playlist failed."));
367		return;
368	}
369
370	_SavePlaylist(origEntry, tempEntry, name);
371}
372
373
374void
375PlaylistWindow::_SavePlaylist(const entry_ref& ref)
376{
377	BString tempName(ref.name);
378	tempName << system_time();
379	entry_ref tempRef(ref);
380	tempRef.set_name(tempName.String());
381
382	BEntry origEntry(&ref);
383	BEntry tempEntry(&tempRef);
384
385	_SavePlaylist(origEntry, tempEntry, ref.name);
386}
387
388
389void
390PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
391	const char* finalName)
392{
393	class TempEntryRemover {
394	public:
395		TempEntryRemover(BEntry* entry)
396			: fEntry(entry)
397		{
398		}
399		~TempEntryRemover()
400		{
401			if (fEntry)
402				fEntry->Remove();
403		}
404		void Detach()
405		{
406			fEntry = NULL;
407		}
408	private:
409		BEntry* fEntry;
410	} remover(&tempEntry);
411
412	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
413	if (file.InitCheck() != B_OK) {
414		BString errorMessage(B_TRANSLATE(
415			"Saving the playlist failed:\n\nError: "));
416		errorMessage << strerror(file.InitCheck());
417		display_save_alert(errorMessage.String());
418		return;
419	}
420
421	AutoLocker<Playlist> lock(fPlaylist);
422	if (!lock.IsLocked()) {
423		display_save_alert(B_TRANSLATE("Internal error (locking failed). "
424			"Saving the playlist failed."));
425		return;
426	}
427
428	status_t ret = fPlaylist->Flatten(&file);
429	if (ret != B_OK) {
430		display_save_alert(ret);
431		return;
432	}
433	lock.Unlock();
434
435	if (origEntry.Exists()) {
436		// TODO: copy attributes
437	}
438
439	// clobber original entry, if it exists
440	tempEntry.Rename(finalName, true);
441	remover.Detach();
442
443	BNodeInfo info(&file);
444	info.SetType("application/x-vnd.haiku-playlist");
445}
446
447
448void
449PlaylistWindow::_QueryInitialDurations()
450{
451	BAutolock lock(fPlaylist);
452
453	BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED);
454	for (int32 i = 0; i < fPlaylist->CountItems(); i++) {
455		addMessage.AddPointer("item", fPlaylist->ItemAt(i));
456		addMessage.AddInt32("index", i);
457	}
458
459	BMessenger(fDurationListener).SendMessage(&addMessage);
460}
461
462
463void
464PlaylistWindow::_UpdateTotalDuration(bigtime_t duration)
465{
466	BAutolock lock(this);
467
468	char buffer[64];
469	duration /= 1000000;
470	duration_to_string(duration, buffer, sizeof(buffer));
471
472	BString text;
473	text.SetToFormat(B_TRANSLATE("Total duration : %s"), buffer);
474
475	fTotalDuration->SetText(text.String());
476}
477
478
479// #pragma mark -
480
481
482PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
483	:
484	PlaylistObserver(this),
485	fKnown(20, true),
486	fTotalDuration(0),
487	fParent(parent)
488{
489	Run();
490}
491
492
493PlaylistWindow::DurationListener::~DurationListener()
494{
495}
496
497
498void
499PlaylistWindow::DurationListener::MessageReceived(BMessage* message)
500{
501	switch (message->what) {
502		case MSG_PLAYLIST_ITEM_ADDED:
503		{
504			void* item;
505			int32 index;
506
507			int32 currentItem = 0;
508			while (message->FindPointer("item", currentItem, &item) == B_OK
509				&& message->FindInt32("index", currentItem, &index) == B_OK) {
510				_HandleItemAdded(static_cast<PlaylistItem*>(item), index);
511				++currentItem;
512			}
513
514			break;
515		}
516
517		case MSG_PLAYLIST_ITEM_REMOVED:
518		{
519			int32 index;
520
521			if (message->FindInt32("index", &index) == B_OK) {
522				_HandleItemRemoved(index);
523			}
524
525			break;
526		}
527
528		default:
529			BLooper::MessageReceived(message);
530			break;
531	}
532}
533
534
535bigtime_t
536PlaylistWindow::DurationListener::TotalDuration()
537{
538	return fTotalDuration;
539}
540
541
542void
543PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item,
544	int32 index)
545{
546	bigtime_t duration = item->Duration();
547	fTotalDuration += duration;
548	fParent._UpdateTotalDuration(fTotalDuration);
549	fKnown.AddItem(new bigtime_t(duration), index);
550}
551
552
553void
554PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index)
555{
556	bigtime_t* deleted = fKnown.RemoveItemAt(index);
557	if (deleted == NULL)
558		return;
559
560	fTotalDuration -= *deleted;
561	fParent._UpdateTotalDuration(fTotalDuration);
562
563	delete deleted;
564}
565
566