xref: /haiku/src/apps/mediaplayer/playlist/PlaylistWindow.cpp (revision 92cb0c5d18d60b3ce7dc816341444371cf9cefed)
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 #include <StringView.h>
35 
36 #include "CommandStack.h"
37 #include "DurationToString.h"
38 #include "MainApp.h"
39 #include "PlaylistListView.h"
40 #include "RWLocker.h"
41 
42 #undef B_TRANSLATION_CONTEXT
43 #define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow"
44 
45 
46 // TODO:
47 // Maintaining a playlist file on disk is a bit tricky. The playlist ref should
48 // be discarded when the user
49 // * loads a new playlist via Open,
50 // * loads a new playlist via dropping it on the MainWindow,
51 // * loads a new playlist via dropping it into the ListView while replacing
52 //   the contents,
53 // * replacing the contents by other stuff.
54 
55 
56 static void
display_save_alert(const char * message)57 display_save_alert(const char* message)
58 {
59 	BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message,
60 		B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
61 	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
62 	alert->Go(NULL);
63 }
64 
65 
66 static void
display_save_alert(status_t error)67 display_save_alert(status_t error)
68 {
69 	BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: "));
70 	errorMessage << strerror(error);
71 	display_save_alert(errorMessage.String());
72 }
73 
74 
75 // #pragma mark -
76 
77 
PlaylistWindow(BRect frame,Playlist * playlist,Controller * controller)78 PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist,
79 		Controller* controller)
80 	:
81 	BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK,
82 		B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS),
83 	fPlaylist(playlist),
84 	fLocker(new RWLocker("command stack lock")),
85 	fCommandStack(new CommandStack(fLocker)),
86 	fCommandStackListener(this),
87 	fDurationListener(new DurationListener(*this))
88 {
89 	frame = Bounds();
90 
91 	_CreateMenu(frame);
92 		// will adjust frame to account for menubar
93 
94 	frame.right -= B_V_SCROLL_BAR_WIDTH;
95 	frame.bottom -= B_H_SCROLL_BAR_HEIGHT;
96 	fListView = new PlaylistListView(frame, playlist, controller,
97 		fCommandStack);
98 
99 	BScrollView* scrollView = new BScrollView("playlist scrollview", fListView,
100 		B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER);
101 
102 	fTopView = 	scrollView;
103 	AddChild(fTopView);
104 
105 	// small visual tweak
106 	if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) {
107 		// make it so the frame of the menubar is also the frame of
108 		// the scroll bar (appears to be)
109 		scrollBar->MoveBy(0, -1);
110 		scrollBar->ResizeBy(0, 2);
111 	}
112 
113 	frame.top += frame.Height();
114 	frame.bottom += B_H_SCROLL_BAR_HEIGHT;
115 
116 	fTotalDuration = new BStringView(frame, "fDuration", "",
117 		B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT);
118 	fTotalDuration->SetAlignment(B_ALIGN_RIGHT);
119 	fTotalDuration->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
120 	AddChild(fTotalDuration);
121 
122 	_UpdateTotalDuration(0);
123 
124 	{
125 		BAutolock _(fPlaylist);
126 
127 		_QueryInitialDurations();
128 		fPlaylist->AddListener(fDurationListener);
129 	}
130 
131 	fCommandStack->AddListener(&fCommandStackListener);
132 	_ObjectChanged(fCommandStack);
133 }
134 
135 
~PlaylistWindow()136 PlaylistWindow::~PlaylistWindow()
137 {
138 	// give listeners a chance to detach themselves
139 	fTopView->RemoveSelf();
140 	delete fTopView;
141 
142 	fCommandStack->RemoveListener(&fCommandStackListener);
143 	delete fCommandStack;
144 	delete fLocker;
145 
146 	fPlaylist->RemoveListener(fDurationListener);
147 	BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED);
148 }
149 
150 
151 bool
QuitRequested()152 PlaylistWindow::QuitRequested()
153 {
154 	Hide();
155 	return false;
156 }
157 
158 
159 void
MessageReceived(BMessage * message)160 PlaylistWindow::MessageReceived(BMessage* message)
161 {
162 	switch (message->what) {
163 		case B_MODIFIERS_CHANGED:
164 			if (LastMouseMovedView())
165 				PostMessage(message, LastMouseMovedView());
166 			break;
167 
168 		case B_UNDO:
169 			fCommandStack->Undo();
170 			break;
171 		case B_REDO:
172 			fCommandStack->Redo();
173 			break;
174 
175 		case MSG_OBJECT_CHANGED: {
176 			Notifier* notifier;
177 			if (message->FindPointer("object", (void**)&notifier) == B_OK)
178 				_ObjectChanged(notifier);
179 			break;
180 		}
181 
182 		case M_URL_RECEIVED:
183 		case B_REFS_RECEIVED:
184 			// Used for when we open a playlist from playlist window
185 			if (!message->HasInt32("append_index")) {
186 				message->AddInt32("append_index",
187 					APPEND_INDEX_REPLACE_PLAYLIST);
188 			}
189 			// supposed to fall through
190 		case B_SIMPLE_DATA:
191 		{
192 			// only accept this message when it comes from the
193 			// player window, _not_ when it is dropped in this window
194 			// outside of the playlist!
195 			int32 appendIndex;
196 			if (message->FindInt32("append_index", &appendIndex) == B_OK)
197 				fListView->ItemsReceived(message, appendIndex);
198 			break;
199 		}
200 
201 		case M_PLAYLIST_OPEN:
202 		{
203 			BMessenger target(this);
204 			BMessage result(B_REFS_RECEIVED);
205 			BMessage appMessage(M_SHOW_OPEN_PANEL);
206 			appMessage.AddMessenger("target", target);
207 			appMessage.AddMessage("message", &result);
208 			appMessage.AddString("title", B_TRANSLATE("Open Playlist"));
209 			appMessage.AddString("label", B_TRANSLATE("Open"));
210 			be_app->PostMessage(&appMessage);
211 			break;
212 		}
213 
214 		case M_PLAYLIST_SAVE:
215 			if (fSavedPlaylistRef != entry_ref()) {
216 				_SavePlaylist(fSavedPlaylistRef);
217 				break;
218 			}
219 			// supposed to fall through
220 		case M_PLAYLIST_SAVE_AS:
221 		{
222 			BMessenger target(this);
223 			BMessage result(M_PLAYLIST_SAVE_RESULT);
224 			BMessage appMessage(M_SHOW_SAVE_PANEL);
225 			appMessage.AddMessenger("target", target);
226 			appMessage.AddMessage("message", &result);
227 			appMessage.AddString("title", B_TRANSLATE("Save Playlist"));
228 			appMessage.AddString("label", B_TRANSLATE("Save"));
229 			be_app->PostMessage(&appMessage);
230 			break;
231 		}
232 
233 		case M_PLAYLIST_SAVE_RESULT:
234 			_SavePlaylist(message);
235 			break;
236 
237 		case B_SELECT_ALL:
238 			fListView->SelectAll();
239 			break;
240 
241 		case M_PLAYLIST_RANDOMIZE:
242 			fListView->Randomize();
243 			break;
244 
245 		case M_PLAYLIST_REMOVE:
246 			fListView->RemoveSelected();
247 			break;
248 
249 		case M_PLAYLIST_MOVE_TO_TRASH:
250 		{
251 			int32 index;
252 			if (message->FindInt32("playlist index", &index) == B_OK)
253 				fListView->RemoveToTrash(index);
254 			else
255 				fListView->RemoveSelectionToTrash();
256 			break;
257 		}
258 
259 		default:
260 			BWindow::MessageReceived(message);
261 			break;
262 	}
263 }
264 
265 
266 // #pragma mark -
267 
268 
269 void
_CreateMenu(BRect & frame)270 PlaylistWindow::_CreateMenu(BRect& frame)
271 {
272 	frame.bottom = 15;
273 	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
274 	BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist"));
275 	menuBar->AddItem(fileMenu);
276 	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS),
277 		new BMessage(M_PLAYLIST_OPEN), 'O'));
278 	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS),
279 		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
280 //	fileMenu->AddItem(new BMenuItem("Save",
281 //		new BMessage(M_PLAYLIST_SAVE), 'S'));
282 
283 	fileMenu->AddSeparatorItem();
284 
285 	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
286 		new BMessage(B_QUIT_REQUESTED), 'W'));
287 
288 	BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));
289 	fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z');
290 	editMenu->AddItem(fUndoMI);
291 	fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z',
292 		B_SHIFT_KEY);
293 	editMenu->AddItem(fRedoMI);
294 	editMenu->AddSeparatorItem();
295 	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"),
296 		new BMessage(B_SELECT_ALL), 'A'));
297 	editMenu->AddSeparatorItem();
298 	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"),
299 		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
300 	editMenu->AddSeparatorItem();
301 	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"),
302 		new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
303 			// TODO: See if we can support the modifier-less B_DELETE
304 			// and draw it properly too. B_NO_MODIFIER?
305 	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"),
306 		new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T'));
307 
308 	menuBar->AddItem(editMenu);
309 
310 	AddChild(menuBar);
311 	fileMenu->SetTargetForItems(this);
312 	editMenu->SetTargetForItems(this);
313 
314 	menuBar->ResizeToPreferred();
315 	frame = Bounds();
316 	frame.top = menuBar->Frame().bottom + 1;
317 }
318 
319 
320 void
_ObjectChanged(const Notifier * object)321 PlaylistWindow::_ObjectChanged(const Notifier* object)
322 {
323 	if (object == fCommandStack) {
324 		// relable Undo item and update enabled status
325 		BString label(B_TRANSLATE("Undo"));
326 		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
327 		if (fUndoMI->IsEnabled())
328 			fUndoMI->SetLabel(label.String());
329 		else
330 			fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>"));
331 
332 		// relable Redo item and update enabled status
333 		label.SetTo(B_TRANSLATE("Redo"));
334 		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
335 		if (fRedoMI->IsEnabled())
336 			fRedoMI->SetLabel(label.String());
337 		else
338 			fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>"));
339 	}
340 }
341 
342 
343 void
_SavePlaylist(const BMessage * message)344 PlaylistWindow::_SavePlaylist(const BMessage* message)
345 {
346 	entry_ref ref;
347 	const char* name;
348 	if (message->FindRef("directory", &ref) != B_OK
349 		|| message->FindString("name", &name) != B_OK) {
350 		display_save_alert(B_TRANSLATE("Internal error (malformed message). "
351 			"Saving the playlist failed."));
352 		return;
353 	}
354 
355 	BString tempName(name);
356 	tempName << system_time();
357 
358 	BPath origPath(&ref);
359 	BPath tempPath(&ref);
360 	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
361 		|| origPath.Append(name) != B_OK
362 		|| tempPath.Append(tempName.String()) != B_OK) {
363 		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
364 			"Saving the playlist failed."));
365 		return;
366 	}
367 
368 	BEntry origEntry(origPath.Path());
369 	BEntry tempEntry(tempPath.Path());
370 	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
371 		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
372 			"Saving the playlist failed."));
373 		return;
374 	}
375 
376 	_SavePlaylist(origEntry, tempEntry, name);
377 }
378 
379 
380 void
_SavePlaylist(const entry_ref & ref)381 PlaylistWindow::_SavePlaylist(const entry_ref& ref)
382 {
383 	BString tempName(ref.name);
384 	tempName << system_time();
385 	entry_ref tempRef(ref);
386 	tempRef.set_name(tempName.String());
387 
388 	BEntry origEntry(&ref);
389 	BEntry tempEntry(&tempRef);
390 
391 	_SavePlaylist(origEntry, tempEntry, ref.name);
392 }
393 
394 
395 void
_SavePlaylist(BEntry & origEntry,BEntry & tempEntry,const char * finalName)396 PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
397 	const char* finalName)
398 {
399 	class TempEntryRemover {
400 	public:
401 		TempEntryRemover(BEntry* entry)
402 			: fEntry(entry)
403 		{
404 		}
405 		~TempEntryRemover()
406 		{
407 			if (fEntry)
408 				fEntry->Remove();
409 		}
410 		void Detach()
411 		{
412 			fEntry = NULL;
413 		}
414 	private:
415 		BEntry* fEntry;
416 	} remover(&tempEntry);
417 
418 	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
419 	if (file.InitCheck() != B_OK) {
420 		BString errorMessage(B_TRANSLATE(
421 			"Saving the playlist failed:\n\nError: "));
422 		errorMessage << strerror(file.InitCheck());
423 		display_save_alert(errorMessage.String());
424 		return;
425 	}
426 
427 	AutoLocker<Playlist> lock(fPlaylist);
428 	if (!lock.IsLocked()) {
429 		display_save_alert(B_TRANSLATE("Internal error (locking failed). "
430 			"Saving the playlist failed."));
431 		return;
432 	}
433 
434 	status_t ret = fPlaylist->Flatten(&file);
435 	if (ret != B_OK) {
436 		display_save_alert(ret);
437 		return;
438 	}
439 	lock.Unlock();
440 
441 	if (origEntry.Exists()) {
442 		// TODO: copy attributes
443 	}
444 
445 	// clobber original entry, if it exists
446 	tempEntry.Rename(finalName, true);
447 	remover.Detach();
448 
449 	BNodeInfo info(&file);
450 	info.SetType("application/x-vnd.haiku-playlist");
451 }
452 
453 
454 void
_QueryInitialDurations()455 PlaylistWindow::_QueryInitialDurations()
456 {
457 	BAutolock lock(fPlaylist);
458 
459 	BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED);
460 	for (int32 i = 0; i < fPlaylist->CountItems(); i++) {
461 		addMessage.AddPointer("item", fPlaylist->ItemAt(i));
462 		addMessage.AddInt32("index", i);
463 	}
464 
465 	BMessenger(fDurationListener).SendMessage(&addMessage);
466 }
467 
468 
469 void
_UpdateTotalDuration(bigtime_t duration)470 PlaylistWindow::_UpdateTotalDuration(bigtime_t duration)
471 {
472 	BAutolock lock(this);
473 
474 	char buffer[64];
475 	duration /= 1000000;
476 	duration_to_string(duration, buffer, sizeof(buffer));
477 
478 	BString text;
479 	text.SetToFormat(B_TRANSLATE("Total duration: %s"), buffer);
480 
481 	fTotalDuration->SetText(text.String());
482 }
483 
484 
485 // #pragma mark -
486 
487 
DurationListener(PlaylistWindow & parent)488 PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
489 	:
490 	PlaylistObserver(this),
491 	fKnown(20, true),
492 	fTotalDuration(0),
493 	fParent(parent)
494 {
495 	Run();
496 }
497 
498 
~DurationListener()499 PlaylistWindow::DurationListener::~DurationListener()
500 {
501 }
502 
503 
504 void
MessageReceived(BMessage * message)505 PlaylistWindow::DurationListener::MessageReceived(BMessage* message)
506 {
507 	switch (message->what) {
508 		case MSG_PLAYLIST_ITEM_ADDED:
509 		{
510 			void* item;
511 			int32 index;
512 
513 			int32 currentItem = 0;
514 			while (message->FindPointer("item", currentItem, &item) == B_OK
515 				&& message->FindInt32("index", currentItem, &index) == B_OK) {
516 				_HandleItemAdded(static_cast<PlaylistItem*>(item), index);
517 				++currentItem;
518 			}
519 
520 			break;
521 		}
522 
523 		case MSG_PLAYLIST_ITEM_REMOVED:
524 		{
525 			int32 index;
526 
527 			if (message->FindInt32("index", &index) == B_OK) {
528 				_HandleItemRemoved(index);
529 			}
530 
531 			break;
532 		}
533 
534 		default:
535 			BLooper::MessageReceived(message);
536 			break;
537 	}
538 }
539 
540 
541 bigtime_t
TotalDuration()542 PlaylistWindow::DurationListener::TotalDuration()
543 {
544 	return fTotalDuration;
545 }
546 
547 
548 void
_HandleItemAdded(PlaylistItem * item,int32 index)549 PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item,
550 	int32 index)
551 {
552 	bigtime_t duration = item->Duration();
553 	fTotalDuration += duration;
554 	fParent._UpdateTotalDuration(fTotalDuration);
555 	fKnown.AddItem(new bigtime_t(duration), index);
556 }
557 
558 
559 void
_HandleItemRemoved(int32 index)560 PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index)
561 {
562 	bigtime_t* deleted = fKnown.RemoveItemAt(index);
563 	if (deleted == NULL)
564 		return;
565 
566 	fTotalDuration -= *deleted;
567 	fParent._UpdateTotalDuration(fTotalDuration);
568 
569 	delete deleted;
570 }
571 
572