xref: /haiku/src/apps/mediaplayer/playlist/PlaylistWindow.cpp (revision 58481f0f6ef1a61ba07283f012cafbc2ed874ead)
1 /*
2  * Copyright 2007-2008, 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 	//
59 	M_PLAYLIST_DELETE_FILE	= 'dlfi',
60 	M_PLAYLIST_PER_DEL_FILE	= 'pdfi'
61 };
62 
63 #define SPACE 5
64 
65 PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist,
66 		Controller* controller)
67 	: BWindow(frame, "Playlist", B_DOCUMENT_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
68 		B_ASYNCHRONOUS_CONTROLS),
69 	  fPlaylist(playlist),
70 	  fLocker(new RWLocker("command stack lock")),
71 	  fCommandStack(new CommandStack(fLocker)),
72 	  fCommandStackListener(this)
73 {
74 	frame = Bounds();
75 
76 	_CreateMenu(frame);
77 		// will adjust frame to account for menubar
78 
79 	frame.right -= B_V_SCROLL_BAR_WIDTH;
80 	fListView = new PlaylistListView(frame, playlist, controller,
81 		fCommandStack);
82 
83 	BScrollView* scrollView = new BScrollView("playlist scrollview", fListView,
84 		B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER);
85 
86 	fTopView = 	scrollView;
87 	AddChild(fTopView);
88 
89 	// small visual tweak
90 	if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) {
91 		// make it so the frame of the menubar is also the frame of
92 		// the scroll bar (appears to be)
93 		scrollBar->MoveBy(0, -1);
94 		scrollBar->ResizeBy(0, -(B_H_SCROLL_BAR_HEIGHT - 2));
95 	}
96 
97 	fCommandStack->AddListener(&fCommandStackListener);
98 	_ObjectChanged(fCommandStack);
99 }
100 
101 
102 PlaylistWindow::~PlaylistWindow()
103 {
104 	// give listeners a chance to detach themselves
105 	fTopView->RemoveSelf();
106 	delete fTopView;
107 
108 	fCommandStack->RemoveListener(&fCommandStackListener);
109 	delete fCommandStack;
110 	delete fLocker;
111 }
112 
113 
114 bool
115 PlaylistWindow::QuitRequested()
116 {
117 	Hide();
118 	return false;
119 }
120 
121 
122 void
123 PlaylistWindow::MessageReceived(BMessage* message)
124 {
125 	switch (message->what) {
126 		case B_MODIFIERS_CHANGED:
127 			if (LastMouseMovedView())
128 				PostMessage(message, LastMouseMovedView());
129 			break;
130 
131 		case B_UNDO:
132 			fCommandStack->Undo();
133 			break;
134 		case B_REDO:
135 			fCommandStack->Redo();
136 			break;
137 
138 		case MSG_OBJECT_CHANGED: {
139 			Notifier* notifier;
140 			if (message->FindPointer("object", (void**)&notifier) == B_OK)
141 				_ObjectChanged(notifier);
142 			break;
143 		}
144 
145 		case B_REFS_RECEIVED:
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_DELETE_FILE:
199 			fListView->PermanentRemoveSelectedFile(false);
200 			break;
201 		case M_PLAYLIST_PER_DEL_FILE:
202 			fListView->PermanentRemoveSelectedFile(true);
203 			break;
204 		default:
205 			BWindow::MessageReceived(message);
206 			break;
207 	}
208 }
209 
210 
211 void
212 PlaylistWindow::DispatchMessage(BMessage *message, BHandler *handler)
213 {
214 	if (message->what == B_KEY_DOWN) {
215 
216 		uint32 key = message->FindInt32("key");
217 
218 		switch (key) {
219 			case 0x34:			//delete button
220 			case 0x3e: 			//d for delete
221 			case 0x2b:			//t for Trash
222 				if (modifiers() & B_COMMAND_KEY) {
223 					fListView->PermanentRemoveSelectedFile(true);
224 					return;
225 				}
226 				break;
227 			case 0x2a:			//r for Remove
228 				if (modifiers() & B_COMMAND_KEY) {
229 					fListView->PermanentRemoveSelectedFile(false);
230 					return;
231 				}
232 				break;
233 		}
234 	}
235 
236 	BWindow::DispatchMessage(message, handler);
237 }
238 
239 
240 // #pragma mark -
241 
242 
243 void
244 PlaylistWindow::_CreateMenu(BRect& frame)
245 {
246 	frame.bottom = 15;
247 	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
248 	BMenu* fileMenu = new BMenu("Playlist");
249 	menuBar->AddItem(fileMenu);
250 	fileMenu->AddItem(new BMenuItem("Open"B_UTF8_ELLIPSIS,
251 		new BMessage(M_PLAYLIST_OPEN), 'O'));
252 	fileMenu->AddItem(new BMenuItem("Save As"B_UTF8_ELLIPSIS,
253 		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
254 //	fileMenu->AddItem(new BMenuItem("Save",
255 //		new BMessage(M_PLAYLIST_SAVE), 'S'));
256 
257 	fileMenu->AddSeparatorItem();
258 
259 	fileMenu->AddItem(new BMenuItem("Close",
260 		new BMessage(B_QUIT_REQUESTED), 'W'));
261 
262 	BMenu* editMenu = new BMenu("Edit");
263 	fUndoMI = new BMenuItem("Undo", new BMessage(B_UNDO), 'Z');
264 	editMenu->AddItem(fUndoMI);
265 	fRedoMI = new BMenuItem("Redo", new BMessage(B_REDO), 'Z', B_SHIFT_KEY);
266 	editMenu->AddItem(fRedoMI);
267 	editMenu->AddSeparatorItem();
268 	editMenu->AddItem(new BMenuItem("Randomize",
269 		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
270 	editMenu->AddSeparatorItem();
271 	editMenu->AddItem(new BMenuItem("Remove",
272 		new BMessage(M_PLAYLIST_DELETE_FILE), 'R'));
273 	editMenu->AddItem(new BMenuItem("Remove Permanent",
274 		new BMessage(M_PLAYLIST_PER_DEL_FILE), 'T', B_COMMAND_KEY));
275 	editMenu->AddItem(new BMenuItem("Remove All",
276 		new BMessage(M_PLAYLIST_EMPTY), 'N'));
277 
278 	menuBar->AddItem(editMenu);
279 
280 	AddChild(menuBar);
281 	fileMenu->SetTargetForItems(this);
282 	editMenu->SetTargetForItems(this);
283 
284 	menuBar->ResizeToPreferred();
285 	frame = Bounds();
286 	frame.top = menuBar->Frame().bottom + 1;
287 }
288 
289 
290 void
291 PlaylistWindow::_ObjectChanged(const Notifier* object)
292 {
293 	if (object == fCommandStack) {
294 		// relable Undo item and update enabled status
295 		BString label("Undo");
296 		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
297 		if (fUndoMI->IsEnabled())
298 			fUndoMI->SetLabel(label.String());
299 		else
300 			fUndoMI->SetLabel("<nothing to undo>");
301 
302 		// relable Redo item and update enabled status
303 		label.SetTo("Redo");
304 		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
305 		if (fRedoMI->IsEnabled())
306 			fRedoMI->SetLabel(label.String());
307 		else
308 			fRedoMI->SetLabel("<nothing to redo>");
309 	}
310 }
311 
312 
313 static void
314 display_save_alert(const char* message)
315 {
316 	BAlert* alert = new BAlert("Save Error", message, "Ok", NULL, NULL,
317 		B_WIDTH_AS_USUAL, B_STOP_ALERT);
318 	alert->Go(NULL);
319 }
320 
321 
322 static void
323 display_save_alert(status_t error)
324 {
325 	BString errorMessage("Saving the playlist failed.\n\nError: ");
326 	errorMessage << strerror(error);
327 	display_save_alert(errorMessage.String());
328 }
329 
330 
331 void
332 PlaylistWindow::_SavePlaylist(const BMessage* message)
333 {
334 	entry_ref ref;
335 	const char* name;
336 	if (message->FindRef("directory", &ref) != B_OK
337 		|| message->FindString("name", &name) != B_OK) {
338 		display_save_alert("Internal error (malformed message). "
339 			"Saving the Playlist failed.");
340 		return;
341 	}
342 
343 	BString tempName(name);
344 	tempName << system_time();
345 
346 	BPath origPath(&ref);
347 	BPath tempPath(&ref);
348 	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
349 		|| origPath.Append(name) != B_OK
350 		|| tempPath.Append(tempName.String()) != B_OK) {
351 		display_save_alert("Internal error (out of memory). "
352 			"Saving the Playlist failed.");
353 		return;
354 	}
355 
356 	BEntry origEntry(origPath.Path());
357 	BEntry tempEntry(tempPath.Path());
358 	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
359 		display_save_alert("Internal error (out of memory). "
360 			"Saving the Playlist failed.");
361 		return;
362 	}
363 
364 	_SavePlaylist(origEntry, tempEntry, name);
365 }
366 
367 
368 void
369 PlaylistWindow::_SavePlaylist(const entry_ref& ref)
370 {
371 	BString tempName(ref.name);
372 	tempName << system_time();
373 	entry_ref tempRef(ref);
374 	tempRef.set_name(tempName.String());
375 
376 	BEntry origEntry(&ref);
377 	BEntry tempEntry(&tempRef);
378 
379 	_SavePlaylist(origEntry, tempEntry, ref.name);
380 }
381 
382 
383 void
384 PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
385 	const char* finalName)
386 {
387 	class TempEntryRemover {
388 	public:
389 		TempEntryRemover(BEntry* entry)
390 			: fEntry(entry)
391 		{
392 		}
393 		~TempEntryRemover()
394 		{
395 			if (fEntry)
396 				fEntry->Remove();
397 		}
398 		void Detach()
399 		{
400 			fEntry = NULL;
401 		}
402 	private:
403 		BEntry* fEntry;
404 	} remover(&tempEntry);
405 
406 	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
407 	if (file.InitCheck() != B_OK) {
408 		BString errorMessage("Saving the playlist failed:\n\nError: ");
409 		errorMessage << strerror(file.InitCheck());
410 		display_save_alert(errorMessage.String());
411 		return;
412 	}
413 
414 	AutoLocker<Playlist> lock(fPlaylist);
415 	if (!lock.IsLocked()) {
416 		display_save_alert("Internal error (locking failed). "
417 			"Saving the Playlist failed.");
418 		return;
419 	}
420 
421 	status_t ret = fPlaylist->Flatten(&file);
422 	if (ret != B_OK) {
423 		display_save_alert(ret);
424 		return;
425 	}
426 	lock.Unlock();
427 
428 	if (origEntry.Exists()) {
429 		// TODO: copy attributes
430 	}
431 
432 	// clobber original entry, if it exists
433 	tempEntry.Rename(finalName, true);
434 	remover.Detach();
435 
436 	BNodeInfo info(&file);
437 	info.SetType("application/x-vnd.haiku-playlist");
438 }
439 
440