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