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