xref: /haiku/src/apps/mediaplayer/playlist/PlaylistWindow.cpp (revision 50b3e74489a1a46fec88df793e4f6780e4de933c)
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 
49 enum {
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 
64 static void
65 display_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 
73 static void
74 display_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 
85 PlaylistWindow::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 
123 PlaylistWindow::~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 
135 bool
136 PlaylistWindow::QuitRequested()
137 {
138 	Hide();
139 	return false;
140 }
141 
142 
143 void
144 PlaylistWindow::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 			break;
182 		}
183 
184 		case M_PLAYLIST_OPEN:
185 		{
186 			BMessenger target(this);
187 			BMessage result(B_REFS_RECEIVED);
188 			BMessage appMessage(M_SHOW_OPEN_PANEL);
189 			appMessage.AddMessenger("target", target);
190 			appMessage.AddMessage("message", &result);
191 			appMessage.AddString("title", "Open Playlist");
192 			appMessage.AddString("label", "Open");
193 			be_app->PostMessage(&appMessage);
194 			break;
195 		}
196 
197 		case M_PLAYLIST_SAVE:
198 			if (fSavedPlaylistRef != entry_ref()) {
199 				_SavePlaylist(fSavedPlaylistRef);
200 				break;
201 			}
202 			// supposed to fall through
203 		case M_PLAYLIST_SAVE_AS:
204 		{
205 			BMessenger target(this);
206 			BMessage result(M_PLAYLIST_SAVE_RESULT);
207 			BMessage appMessage(M_SHOW_SAVE_PANEL);
208 			appMessage.AddMessenger("target", target);
209 			appMessage.AddMessage("message", &result);
210 			appMessage.AddString("title", "Save Playlist");
211 			appMessage.AddString("label", "Save");
212 			be_app->PostMessage(&appMessage);
213 			break;
214 		}
215 
216 		case M_PLAYLIST_SAVE_RESULT:
217 			_SavePlaylist(message);
218 			break;
219 
220 		case M_PLAYLIST_EMPTY:
221 			fListView->RemoveAll();
222 			break;
223 		case M_PLAYLIST_RANDOMIZE:
224 			fListView->Randomize();
225 			break;
226 		case M_PLAYLIST_REMOVE:
227 			fListView->RemoveSelected();
228 			break;
229 		case M_PLAYLIST_REMOVE_AND_PUT_INTO_TRASH:
230 		{
231 			int32 index;
232 			if (message->FindInt32("playlist index", &index) == B_OK)
233 				fListView->RemoveToTrash(index);
234 			else
235 				fListView->RemoveSelectionToTrash();
236 			break;
237 		}
238 		default:
239 			BWindow::MessageReceived(message);
240 			break;
241 	}
242 }
243 
244 
245 // #pragma mark -
246 
247 
248 void
249 PlaylistWindow::_CreateMenu(BRect& frame)
250 {
251 	frame.bottom = 15;
252 	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
253 	BMenu* fileMenu = new BMenu("Playlist");
254 	menuBar->AddItem(fileMenu);
255 	fileMenu->AddItem(new BMenuItem("Open"B_UTF8_ELLIPSIS,
256 		new BMessage(M_PLAYLIST_OPEN), 'O'));
257 	fileMenu->AddItem(new BMenuItem("Save as"B_UTF8_ELLIPSIS,
258 		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
259 //	fileMenu->AddItem(new BMenuItem("Save",
260 //		new BMessage(M_PLAYLIST_SAVE), 'S'));
261 
262 	fileMenu->AddSeparatorItem();
263 
264 	fileMenu->AddItem(new BMenuItem("Close",
265 		new BMessage(B_QUIT_REQUESTED), 'W'));
266 
267 	BMenu* editMenu = new BMenu("Edit");
268 	fUndoMI = new BMenuItem("Undo", new BMessage(B_UNDO), 'Z');
269 	editMenu->AddItem(fUndoMI);
270 	fRedoMI = new BMenuItem("Redo", new BMessage(B_REDO), 'Z', B_SHIFT_KEY);
271 	editMenu->AddItem(fRedoMI);
272 	editMenu->AddSeparatorItem();
273 	editMenu->AddItem(new BMenuItem("Randomize",
274 		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
275 	editMenu->AddSeparatorItem();
276 	editMenu->AddItem(new BMenuItem("Remove (Del)",
277 		new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
278 	editMenu->AddItem(new BMenuItem("Remove and put into Trash",
279 		new BMessage(M_PLAYLIST_REMOVE_AND_PUT_INTO_TRASH), 'T'));
280 	editMenu->AddItem(new BMenuItem("Remove all",
281 		new BMessage(M_PLAYLIST_EMPTY), 'N'));
282 
283 	menuBar->AddItem(editMenu);
284 
285 	AddChild(menuBar);
286 	fileMenu->SetTargetForItems(this);
287 	editMenu->SetTargetForItems(this);
288 
289 	menuBar->ResizeToPreferred();
290 	frame = Bounds();
291 	frame.top = menuBar->Frame().bottom + 1;
292 }
293 
294 
295 void
296 PlaylistWindow::_ObjectChanged(const Notifier* object)
297 {
298 	if (object == fCommandStack) {
299 		// relable Undo item and update enabled status
300 		BString label("Undo");
301 		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
302 		if (fUndoMI->IsEnabled())
303 			fUndoMI->SetLabel(label.String());
304 		else
305 			fUndoMI->SetLabel("<nothing to undo>");
306 
307 		// relable Redo item and update enabled status
308 		label.SetTo("Redo");
309 		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
310 		if (fRedoMI->IsEnabled())
311 			fRedoMI->SetLabel(label.String());
312 		else
313 			fRedoMI->SetLabel("<nothing to redo>");
314 	}
315 }
316 
317 
318 void
319 PlaylistWindow::_SavePlaylist(const BMessage* message)
320 {
321 	entry_ref ref;
322 	const char* name;
323 	if (message->FindRef("directory", &ref) != B_OK
324 		|| message->FindString("name", &name) != B_OK) {
325 		display_save_alert("Internal error (malformed message). "
326 			"Saving the playlist failed.");
327 		return;
328 	}
329 
330 	BString tempName(name);
331 	tempName << system_time();
332 
333 	BPath origPath(&ref);
334 	BPath tempPath(&ref);
335 	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
336 		|| origPath.Append(name) != B_OK
337 		|| tempPath.Append(tempName.String()) != B_OK) {
338 		display_save_alert("Internal error (out of memory). "
339 			"Saving the playlist failed.");
340 		return;
341 	}
342 
343 	BEntry origEntry(origPath.Path());
344 	BEntry tempEntry(tempPath.Path());
345 	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
346 		display_save_alert("Internal error (out of memory). "
347 			"Saving the playlist failed.");
348 		return;
349 	}
350 
351 	_SavePlaylist(origEntry, tempEntry, name);
352 }
353 
354 
355 void
356 PlaylistWindow::_SavePlaylist(const entry_ref& ref)
357 {
358 	BString tempName(ref.name);
359 	tempName << system_time();
360 	entry_ref tempRef(ref);
361 	tempRef.set_name(tempName.String());
362 
363 	BEntry origEntry(&ref);
364 	BEntry tempEntry(&tempRef);
365 
366 	_SavePlaylist(origEntry, tempEntry, ref.name);
367 }
368 
369 
370 void
371 PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
372 	const char* finalName)
373 {
374 	class TempEntryRemover {
375 	public:
376 		TempEntryRemover(BEntry* entry)
377 			: fEntry(entry)
378 		{
379 		}
380 		~TempEntryRemover()
381 		{
382 			if (fEntry)
383 				fEntry->Remove();
384 		}
385 		void Detach()
386 		{
387 			fEntry = NULL;
388 		}
389 	private:
390 		BEntry* fEntry;
391 	} remover(&tempEntry);
392 
393 	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
394 	if (file.InitCheck() != B_OK) {
395 		BString errorMessage("Saving the playlist failed:\n\nError: ");
396 		errorMessage << strerror(file.InitCheck());
397 		display_save_alert(errorMessage.String());
398 		return;
399 	}
400 
401 	AutoLocker<Playlist> lock(fPlaylist);
402 	if (!lock.IsLocked()) {
403 		display_save_alert("Internal error (locking failed). "
404 			"Saving the playlist failed.");
405 		return;
406 	}
407 
408 	status_t ret = fPlaylist->Flatten(&file);
409 	if (ret != B_OK) {
410 		display_save_alert(ret);
411 		return;
412 	}
413 	lock.Unlock();
414 
415 	if (origEntry.Exists()) {
416 		// TODO: copy attributes
417 	}
418 
419 	// clobber original entry, if it exists
420 	tempEntry.Rename(finalName, true);
421 	remover.Detach();
422 
423 	BNodeInfo info(&file);
424 	info.SetType("application/x-vnd.haiku-playlist");
425 }
426 
427