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