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