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