/*
* Copyright 2007-2010, Haiku. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Stephan Aßmus <superstippi@gmx.de>
* Fredrik Modéen <fredrik@modeen.se>
*/
#include "PlaylistWindow.h"
#include <stdio.h>
#include <Alert.h>
#include <Application.h>
#include <Autolock.h>
#include <Box.h>
#include <Button.h>
#include <Catalog.h>
#include <Entry.h>
#include <File.h>
#include <FilePanel.h>
#include <Locale.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Roster.h>
#include <ScrollBar.h>
#include <ScrollView.h>
#include <String.h>
#include <StringView.h>
#include "CommandStack.h"
#include "DurationToString.h"
#include "MainApp.h"
#include "PlaylistListView.h"
#include "RWLocker.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow"
// TODO:
// Maintaining a playlist file on disk is a bit tricky. The playlist ref should
// be discarded when the user
// * loads a new playlist via Open,
// * loads a new playlist via dropping it on the MainWindow,
// * loads a new playlist via dropping it into the ListView while replacing
// the contents,
// * replacing the contents by other stuff.
static void
display_save_alert(const char* message)
{
BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message,
B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
alert->Go(NULL);
}
static void
display_save_alert(status_t error)
{
BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: "));
errorMessage << strerror(error);
display_save_alert(errorMessage.String());
}
// #pragma mark -
PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist,
Controller* controller)
:
BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK,
B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS),
fPlaylist(playlist),
fLocker(new RWLocker("command stack lock")),
fCommandStack(new CommandStack(fLocker)),
fCommandStackListener(this),
fDurationListener(new DurationListener(*this))
{
frame = Bounds();
_CreateMenu(frame);
// will adjust frame to account for menubar
frame.right -= B_V_SCROLL_BAR_WIDTH;
frame.bottom -= B_H_SCROLL_BAR_HEIGHT;
fListView = new PlaylistListView(frame, playlist, controller,
fCommandStack);
BScrollView* scrollView = new BScrollView("playlist scrollview", fListView,
B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER);
fTopView = scrollView;
AddChild(fTopView);
// small visual tweak
if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) {
// make it so the frame of the menubar is also the frame of
// the scroll bar (appears to be)
scrollBar->MoveBy(0, -1);
scrollBar->ResizeBy(0, 2);
}
frame.top += frame.Height();
frame.bottom += B_H_SCROLL_BAR_HEIGHT;
fTotalDuration = new BStringView(frame, "fDuration", "",
B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT);
fTotalDuration->SetAlignment(B_ALIGN_RIGHT);
fTotalDuration->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
AddChild(fTotalDuration);
_UpdateTotalDuration(0);
{
BAutolock _(fPlaylist);
_QueryInitialDurations();
fPlaylist->AddListener(fDurationListener);
}
fCommandStack->AddListener(&fCommandStackListener);
_ObjectChanged(fCommandStack);
}
PlaylistWindow::~PlaylistWindow()
{
// give listeners a chance to detach themselves
fTopView->RemoveSelf();
delete fTopView;
fCommandStack->RemoveListener(&fCommandStackListener);
delete fCommandStack;
delete fLocker;
fPlaylist->RemoveListener(fDurationListener);
BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED);
}
bool
PlaylistWindow::QuitRequested()
{
Hide();
return false;
}
void
PlaylistWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case B_MODIFIERS_CHANGED:
if (LastMouseMovedView())
PostMessage(message, LastMouseMovedView());
break;
case B_UNDO:
fCommandStack->Undo();
break;
case B_REDO:
fCommandStack->Redo();
break;
case MSG_OBJECT_CHANGED: {
Notifier* notifier;
if (message->FindPointer("object", (void**)¬ifier) == B_OK)
_ObjectChanged(notifier);
break;
}
case M_URL_RECEIVED:
case B_REFS_RECEIVED:
// Used for when we open a playlist from playlist window
if (!message->HasInt32("append_index")) {
message->AddInt32("append_index",
APPEND_INDEX_REPLACE_PLAYLIST);
}
// supposed to fall through
case B_SIMPLE_DATA:
{
// only accept this message when it comes from the
// player window, _not_ when it is dropped in this window
// outside of the playlist!
int32 appendIndex;
if (message->FindInt32("append_index", &appendIndex) == B_OK)
fListView->ItemsReceived(message, appendIndex);
break;
}
case M_PLAYLIST_OPEN:
{
BMessenger target(this);
BMessage result(B_REFS_RECEIVED);
BMessage appMessage(M_SHOW_OPEN_PANEL);
appMessage.AddMessenger("target", target);
appMessage.AddMessage("message", &result);
appMessage.AddString("title", B_TRANSLATE("Open Playlist"));
appMessage.AddString("label", B_TRANSLATE("Open"));
be_app->PostMessage(&appMessage);
break;
}
case M_PLAYLIST_SAVE:
if (fSavedPlaylistRef != entry_ref()) {
_SavePlaylist(fSavedPlaylistRef);
break;
}
// supposed to fall through
case M_PLAYLIST_SAVE_AS:
{
BMessenger target(this);
BMessage result(M_PLAYLIST_SAVE_RESULT);
BMessage appMessage(M_SHOW_SAVE_PANEL);
appMessage.AddMessenger("target", target);
appMessage.AddMessage("message", &result);
appMessage.AddString("title", B_TRANSLATE("Save Playlist"));
appMessage.AddString("label", B_TRANSLATE("Save"));
be_app->PostMessage(&appMessage);
break;
}
case M_PLAYLIST_SAVE_RESULT:
_SavePlaylist(message);
break;
case B_SELECT_ALL:
fListView->SelectAll();
break;
case M_PLAYLIST_RANDOMIZE:
fListView->Randomize();
break;
case M_PLAYLIST_REMOVE:
fListView->RemoveSelected();
break;
case M_PLAYLIST_MOVE_TO_TRASH:
{
int32 index;
if (message->FindInt32("playlist index", &index) == B_OK)
fListView->RemoveToTrash(index);
else
fListView->RemoveSelectionToTrash();
break;
}
default:
BWindow::MessageReceived(message);
break;
}
}
// #pragma mark -
void
PlaylistWindow::_CreateMenu(BRect& frame)
{
frame.bottom = 15;
BMenuBar* menuBar = new BMenuBar(frame, "main menu");
BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist"));
menuBar->AddItem(fileMenu);
fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS),
new BMessage(M_PLAYLIST_OPEN), 'O'));
fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS),
new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
// fileMenu->AddItem(new BMenuItem("Save",
// new BMessage(M_PLAYLIST_SAVE), 'S'));
fileMenu->AddSeparatorItem();
fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
new BMessage(B_QUIT_REQUESTED), 'W'));
BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));
fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z');
editMenu->AddItem(fUndoMI);
fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z',
B_SHIFT_KEY);
editMenu->AddItem(fRedoMI);
editMenu->AddSeparatorItem();
editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"),
new BMessage(B_SELECT_ALL), 'A'));
editMenu->AddSeparatorItem();
editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"),
new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
editMenu->AddSeparatorItem();
editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"),
new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
// TODO: See if we can support the modifier-less B_DELETE
// and draw it properly too. B_NO_MODIFIER?
editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"),
new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T'));
menuBar->AddItem(editMenu);
AddChild(menuBar);
fileMenu->SetTargetForItems(this);
editMenu->SetTargetForItems(this);
menuBar->ResizeToPreferred();
frame = Bounds();
frame.top = menuBar->Frame().bottom + 1;
}
void
PlaylistWindow::_ObjectChanged(const Notifier* object)
{
if (object == fCommandStack) {
// relable Undo item and update enabled status
BString label(B_TRANSLATE("Undo"));
fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
if (fUndoMI->IsEnabled())
fUndoMI->SetLabel(label.String());
else
fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>"));
// relable Redo item and update enabled status
label.SetTo(B_TRANSLATE("Redo"));
fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
if (fRedoMI->IsEnabled())
fRedoMI->SetLabel(label.String());
else
fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>"));
}
}
void
PlaylistWindow::_SavePlaylist(const BMessage* message)
{
entry_ref ref;
const char* name;
if (message->FindRef("directory", &ref) != B_OK
|| message->FindString("name", &name) != B_OK) {
display_save_alert(B_TRANSLATE("Internal error (malformed message). "
"Saving the playlist failed."));
return;
}
BString tempName(name);
tempName << system_time();
BPath origPath(&ref);
BPath tempPath(&ref);
if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
|| origPath.Append(name) != B_OK
|| tempPath.Append(tempName.String()) != B_OK) {
display_save_alert(B_TRANSLATE("Internal error (out of memory). "
"Saving the playlist failed."));
return;
}
BEntry origEntry(origPath.Path());
BEntry tempEntry(tempPath.Path());
if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
display_save_alert(B_TRANSLATE("Internal error (out of memory). "
"Saving the playlist failed."));
return;
}
_SavePlaylist(origEntry, tempEntry, name);
}
void
PlaylistWindow::_SavePlaylist(const entry_ref& ref)
{
BString tempName(ref.name);
tempName << system_time();
entry_ref tempRef(ref);
tempRef.set_name(tempName.String());
BEntry origEntry(&ref);
BEntry tempEntry(&tempRef);
_SavePlaylist(origEntry, tempEntry, ref.name);
}
void
PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
const char* finalName)
{
class TempEntryRemover {
public:
TempEntryRemover(BEntry* entry)
: fEntry(entry)
{
}
~TempEntryRemover()
{
if (fEntry)
fEntry->Remove();
}
void Detach()
{
fEntry = NULL;
}
private:
BEntry* fEntry;
} remover(&tempEntry);
BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
if (file.InitCheck() != B_OK) {
BString errorMessage(B_TRANSLATE(
"Saving the playlist failed:\n\nError: "));
errorMessage << strerror(file.InitCheck());
display_save_alert(errorMessage.String());
return;
}
AutoLocker<Playlist> lock(fPlaylist);
if (!lock.IsLocked()) {
display_save_alert(B_TRANSLATE("Internal error (locking failed). "
"Saving the playlist failed."));
return;
}
status_t ret = fPlaylist->Flatten(&file);
if (ret != B_OK) {
display_save_alert(ret);
return;
}
lock.Unlock();
if (origEntry.Exists()) {
// TODO: copy attributes
}
// clobber original entry, if it exists
tempEntry.Rename(finalName, true);
remover.Detach();
BNodeInfo info(&file);
info.SetType("application/x-vnd.haiku-playlist");
}
void
PlaylistWindow::_QueryInitialDurations()
{
BAutolock lock(fPlaylist);
BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED);
for (int32 i = 0; i < fPlaylist->CountItems(); i++) {
addMessage.AddPointer("item", fPlaylist->ItemAt(i));
addMessage.AddInt32("index", i);
}
BMessenger(fDurationListener).SendMessage(&addMessage);
}
void
PlaylistWindow::_UpdateTotalDuration(bigtime_t duration)
{
BAutolock lock(this);
char buffer[64];
duration /= 1000000;
duration_to_string(duration, buffer, sizeof(buffer));
BString text;
text.SetToFormat(B_TRANSLATE("Total duration: %s"), buffer);
fTotalDuration->SetText(text.String());
}
// #pragma mark -
PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
:
PlaylistObserver(this),
fKnown(20, true),
fTotalDuration(0),
fParent(parent)
{
Run();
}
PlaylistWindow::DurationListener::~DurationListener()
{
}
void
PlaylistWindow::DurationListener::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_PLAYLIST_ITEM_ADDED:
{
void* item;
int32 index;
int32 currentItem = 0;
while (message->FindPointer("item", currentItem, &item) == B_OK
&& message->FindInt32("index", currentItem, &index) == B_OK) {
_HandleItemAdded(static_cast<PlaylistItem*>(item), index);
++currentItem;
}
break;
}
case MSG_PLAYLIST_ITEM_REMOVED:
{
int32 index;
if (message->FindInt32("index", &index) == B_OK) {
_HandleItemRemoved(index);
}
break;
}
default:
BLooper::MessageReceived(message);
break;
}
}
bigtime_t
PlaylistWindow::DurationListener::TotalDuration()
{
return fTotalDuration;
}
void
PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item,
int32 index)
{
bigtime_t duration = item->Duration();
fTotalDuration += duration;
fParent._UpdateTotalDuration(fTotalDuration);
fKnown.AddItem(new bigtime_t(duration), index);
}
void
PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index)
{
bigtime_t* deleted = fKnown.RemoveItemAt(index);
if (deleted == NULL)
return;
fTotalDuration -= *deleted;
fParent._UpdateTotalDuration(fTotalDuration);
delete deleted;
}
↑ V763 Parameter 'frame' is always rewritten in function body before being used.
↑ V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.