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