root/src/apps/mediaplayer/MainApp.cpp
/*
 * MainApp.cpp - Media Player for the Haiku Operating System
 *
 * Copyright (C) 2006 Marcus Overhagen <marcus@overhagen.de>
 * Copyright (C) 2008 Stephan Aßmus <superstippi@gmx.de> (MIT Ok)
 *
 * Released under the terms of the MIT license.
 */


#include "MainApp.h"

#include <Alert.h>
#include <Autolock.h>
#include <Catalog.h>
#include <Entry.h>
#include <FilePanel.h>
#include <Locale.h>
#include <MediaDefs.h>
#include <MediaRoster.h>
#include <MimeType.h>
#include <Path.h>
#include <Resources.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "EventQueue.h"
#include "Playlist.h"
#include "Settings.h"
#include "SettingsWindow.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MediaPlayer-Main"


static const char* kCurrentPlaylistFilename = "MediaPlayer Current Playlist";

const char* kAppSig = "application/x-vnd.Haiku-MediaPlayer";

MainApp* gMainApp;


MainApp::MainApp()
        :
        BApplication(kAppSig),
        fPlayerCount(0),
        fSettingsWindow(NULL),

        fOpenFilePanel(NULL),
        fSaveFilePanel(NULL),
        fLastFilePanelFolder(),

        fAudioWindowFrameSaved(false),
        fLastSavedAudioWindowCreationTime(0)
{
        fLastFilePanelFolder = Settings::Default()->FilePanelFolder();

        if (!BMediaRoster::IsRunning()) {
                BAlert* alert = new BAlert("start_media_server",
                        B_TRANSLATE("It appears the media server is not running.\n"
                        "Would you like to start it ?"), B_TRANSLATE("Quit"),
                        B_TRANSLATE("Start media server"), NULL,
                        B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                alert->SetShortcut(0, B_ESCAPE);

                if (alert->Go() == 0) {
                        PostMessage(B_QUIT_REQUESTED);
                        return;
                }

                launch_media_server();
        }
}


MainApp::~MainApp()
{
        delete fOpenFilePanel;
        delete fSaveFilePanel;
}


bool
MainApp::QuitRequested()
{
        // Make sure we store the current playlist, if applicable.
        for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
                MainWin* playerWindow = dynamic_cast<MainWin*>(window);
                if (playerWindow == NULL)
                        continue;

                BAutolock _(playerWindow);

                BMessage quitMessage;
                playerWindow->GetQuitMessage(&quitMessage);

                // Store the playlist if there is one. If the user has multiple
                // instances playing audio at the this time, the first instance wins.
                BMessage playlistArchive;
                if (quitMessage.FindMessage("playlist", &playlistArchive) == B_OK) {
                        _StoreCurrentPlaylist(&playlistArchive);
                        break;
                }
        }

        // Note: This needs to be done here, SettingsWindow::QuitRequested()
        // returns "false" always. (Standard BApplication quit procedure will
        // hang otherwise.)
        if (fSettingsWindow && fSettingsWindow->Lock())
                fSettingsWindow->Quit();
        fSettingsWindow = NULL;

        // store the current file panel ref in the global settings
        Settings::Default()->SetFilePanelFolder(fLastFilePanelFolder);

        return BApplication::QuitRequested();
}


MainWin*
MainApp::NewWindow(BMessage* message)
{
        BAutolock _(this);
        fPlayerCount++;
        return new(std::nothrow) MainWin(fPlayerCount == 1, message);
}


int32
MainApp::PlayerCount() const
{
        BAutolock _(const_cast<MainApp*>(this));
        return fPlayerCount;
}


// #pragma mark -


void
MainApp::ReadyToRun()
{
        // make sure we have at least one window open
        if (fPlayerCount == 0) {
                MainWin* window = NewWindow();
                if (window == NULL) {
                        PostMessage(B_QUIT_REQUESTED);
                        return;
                }
                BMessage lastPlaylistArchive;
                if (_RestoreCurrentPlaylist(&lastPlaylistArchive) == B_OK) {
                        lastPlaylistArchive.what = M_OPEN_PREVIOUS_PLAYLIST;
                        window->PostMessage(&lastPlaylistArchive);
                } else
                        window->Show();
        }

        // setup the settings window now, we need to have it
        fSettingsWindow = new SettingsWindow(BRect(150, 150, 450, 520));
        fSettingsWindow->Hide();
        fSettingsWindow->Show();

        _InstallPlaylistMimeType();
}


void
MainApp::RefsReceived(BMessage* message)
{
        // The user dropped a file (or files) on this app's icon,
        // or double clicked a file that's handled by this app.
        // Command line arguments are also redirected to here by
        // ArgvReceived() but without MIME type check.

        // If multiple refs are received in short succession we
        // combine them into a single window/playlist. Tracker
        // will send multiple messages when opening a multi-
        // selection for example and we don't want to spawn large
        // numbers of windows when someone just tries to open an
        // album. We use half a second time and prolong it for
        // each new ref received.
        static bigtime_t sLastRefsReceived = 0;
        static MainWin* sLastRefsWindow = NULL;

        if (system_time() - sLastRefsReceived < 500000) {
                // Find the last opened window
                for (int32 i = CountWindows() - 1; i >= 0; i--) {
                        MainWin* playerWindow = dynamic_cast<MainWin*>(WindowAt(i));
                        if (playerWindow == NULL)
                                continue;

                        if (playerWindow != sLastRefsWindow) {
                                // The window has changed since the last refs
                                sLastRefsReceived = 0;
                                sLastRefsWindow = NULL;
                                break;
                        }

                        message->AddBool("append to playlist", true);
                        playerWindow->PostMessage(message);
                        sLastRefsReceived = system_time();
                        return;
                }
        }

        sLastRefsWindow = NewWindow(message);
        sLastRefsReceived = system_time();
}


void
MainApp::ArgvReceived(int32 argc, char** argv)
{
        char cwd[B_PATH_NAME_LENGTH];
        getcwd(cwd, sizeof(cwd));

        for (int i = 1; i < argc; i++) {
                BUrl url(argv[i], true);
                if (url.IsValid()) {
                        BMessage archivedUrl;
                        url.Archive(&archivedUrl);

                        BMessage msg(M_URL_RECEIVED);
                        if (msg.AddMessage("mediaplayer:url", &archivedUrl) == B_OK)
                                RefsReceived(&msg);

                        continue;
                }

                BPath path;
                if (argv[i][0] != '/')
                        path.SetTo(cwd, argv[i]);
                else
                        path.SetTo(argv[i]);
                BEntry entry(path.Path(), true);
                if (!entry.Exists() || !entry.IsFile())
                        continue;

                BMessage message(B_REFS_RECEIVED);
                entry_ref ref;
                if (entry.GetRef(&ref) == B_OK && message.AddRef("refs", &ref) == B_OK)
                        RefsReceived(&message);
        }
}


void
MainApp::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case M_NEW_PLAYER:
                {
                        MainWin* window = NewWindow();
                        if (window != NULL)
                                window->Show();
                        break;
                }
                case M_PLAYER_QUIT:
                {
                        // store the window settings of this instance
                        MainWin* window = NULL;
                        bool audioOnly = false;
                        BRect windowFrame;
                        bigtime_t creationTime;
                        if (message->FindPointer("instance", (void**)&window) == B_OK
                                && message->FindBool("audio only", &audioOnly) == B_OK
                                && message->FindRect("window frame", &windowFrame) == B_OK
                                && message->FindInt64("creation time", &creationTime) == B_OK) {
                                if (audioOnly && (!fAudioWindowFrameSaved
                                                || creationTime < fLastSavedAudioWindowCreationTime)) {
                                        fAudioWindowFrameSaved = true;
                                        fLastSavedAudioWindowCreationTime = creationTime;

                                        Settings::Default()->SetAudioPlayerWindowFrame(windowFrame);
                                }
                        }

                        // Store the playlist if there is one. Since the app is doing
                        // this, it is "atomic". If the user has multiple instances
                        // playing audio at the same time, the last instance which is
                        // quit wins.
                        BMessage playlistArchive;
                        if (message->FindMessage("playlist", &playlistArchive) == B_OK)
                                _StoreCurrentPlaylist(&playlistArchive);

                        // quit if this was the last player window
                        fPlayerCount--;
                        if (fPlayerCount == 0)
                                PostMessage(B_QUIT_REQUESTED);
                        break;
                }

                case M_SETTINGS:
                        _ShowSettingsWindow();
                        break;

                case M_SHOW_OPEN_PANEL:
                        _ShowOpenFilePanel(message);
                        break;
                case M_SHOW_SAVE_PANEL:
                        _ShowSaveFilePanel(message);
                        break;

                case M_OPEN_PANEL_RESULT:
                        _HandleOpenPanelResult(message);
                        break;
                case M_SAVE_PANEL_RESULT:
                        _HandleSavePanelResult(message);
                        break;
                case B_CANCEL:
                {
                        // The user canceled a file panel, but store at least the current
                        // file panel folder.
                        uint32 oldWhat;
                        if (message->FindInt32("old_what", (int32*)&oldWhat) != B_OK)
                                break;
                        if (oldWhat == M_OPEN_PANEL_RESULT && fOpenFilePanel != NULL)
                                fOpenFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
                        else if (oldWhat == M_SAVE_PANEL_RESULT && fSaveFilePanel != NULL)
                                fSaveFilePanel->GetPanelDirectory(&fLastFilePanelFolder);
                        break;
                }

                default:
                        BApplication::MessageReceived(message);
                        break;
        }
}


// #pragma mark -


void
MainApp::_BroadcastMessage(const BMessage& _message)
{
        for (int32 i = 0; BWindow* window = WindowAt(i); i++) {
                BMessage message(_message);
                window->PostMessage(&message);
        }
}


void
MainApp::_ShowSettingsWindow()
{
        BAutolock lock(fSettingsWindow);
        if (!lock.IsLocked())
                return;

        // If the window is already showing, don't jerk the workspaces around,
        // just pull it to the current one.
        uint32 workspace = 1UL << (uint32)current_workspace();
        uint32 windowWorkspaces = fSettingsWindow->Workspaces();
        if ((windowWorkspaces & workspace) == 0) {
                // window in a different workspace, reopen in current
                fSettingsWindow->SetWorkspaces(workspace);
        }

        if (fSettingsWindow->IsHidden())
                fSettingsWindow->Show();
        else
                fSettingsWindow->Activate();
}


// #pragma mark - file panels


void
MainApp::_ShowOpenFilePanel(const BMessage* message)
{
        if (fOpenFilePanel == NULL) {
                BMessenger target(this);
                fOpenFilePanel = new BFilePanel(B_OPEN_PANEL, &target);
        }

        _ShowFilePanel(fOpenFilePanel, M_OPEN_PANEL_RESULT, message,
                B_TRANSLATE("Open"), B_TRANSLATE("Open"));
}


void
MainApp::_ShowSaveFilePanel(const BMessage* message)
{
        if (fSaveFilePanel == NULL) {
                BMessenger target(this);
                fSaveFilePanel = new BFilePanel(B_SAVE_PANEL, &target);
        }

        _ShowFilePanel(fSaveFilePanel, M_SAVE_PANEL_RESULT, message,
                B_TRANSLATE("Save"), B_TRANSLATE("Save"));
}


void
MainApp::_ShowFilePanel(BFilePanel* panel, uint32 command,
        const BMessage* message, const char* defaultTitle,
        const char* defaultLabel)
{
//      printf("_ShowFilePanel()\n");
//      message->PrintToStream();

        BMessage panelMessage(command);

        if (message != NULL) {
                BMessage targetMessage;
                if (message->FindMessage("message", &targetMessage) == B_OK)
                        panelMessage.AddMessage("message", &targetMessage);

                BMessenger target;
                if (message->FindMessenger("target", &target) == B_OK)
                        panelMessage.AddMessenger("target", target);

                const char* panelTitle;
                if (message->FindString("title", &panelTitle) != B_OK)
                        panelTitle = defaultTitle;
                {
                        BString finalPanelTitle(B_TRANSLATE("%appname%: %title%"));
                        finalPanelTitle.ReplaceFirst("%appname%", B_TRANSLATE_SYSTEM_NAME("MediaPlayer"));
                        finalPanelTitle.ReplaceFirst("%title%", panelTitle);
                        BAutolock lock(panel->Window());
                        panel->Window()->SetTitle(finalPanelTitle.String());
                }
                const char* buttonLabel;
                if (message->FindString("label", &buttonLabel) != B_OK)
                        buttonLabel = defaultLabel;
                panel->SetButtonLabel(B_DEFAULT_BUTTON, buttonLabel);
        }

//      panelMessage.PrintToStream();
        panel->SetMessage(&panelMessage);

        if (fLastFilePanelFolder != entry_ref()) {
                panel->SetPanelDirectory(&fLastFilePanelFolder);
        }

        panel->Show();
}


void
MainApp::_HandleOpenPanelResult(const BMessage* message)
{
        _HandleFilePanelResult(fOpenFilePanel, message);
}


void
MainApp::_HandleSavePanelResult(const BMessage* message)
{
        _HandleFilePanelResult(fSaveFilePanel, message);
}


void
MainApp::_HandleFilePanelResult(BFilePanel* panel, const BMessage* message)
{
//      printf("_HandleFilePanelResult()\n");
//      message->PrintToStream();

        panel->GetPanelDirectory(&fLastFilePanelFolder);

        BMessage targetMessage;
        if (message->FindMessage("message", &targetMessage) != B_OK)
                targetMessage.what = message->what;

        BMessenger target;
        if (message->FindMessenger("target", &target) != B_OK) {
                if (targetMessage.what == M_OPEN_PANEL_RESULT
                        || targetMessage.what == M_SAVE_PANEL_RESULT) {
                        // prevent endless message cycle
                        return;
                }
                // send result message to ourselves
                target = BMessenger(this);
        }

        // copy the important contents of the message
        // save panel
        entry_ref directory;
        if (message->FindRef("directory", &directory) == B_OK)
                targetMessage.AddRef("directory", &directory);
        const char* name;
        if (message->FindString("name", &name) == B_OK)
                targetMessage.AddString("name", name);
        // open panel
        entry_ref ref;
        for (int32 i = 0; message->FindRef("refs", i, &ref) == B_OK; i++)
                targetMessage.AddRef("refs", &ref);

        target.SendMessage(&targetMessage);
}


void
MainApp::_StoreCurrentPlaylist(const BMessage* message) const
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
                || path.Append(kCurrentPlaylistFilename) != B_OK) {
                return;
        }

        BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
        if (file.InitCheck() != B_OK)
                return;

        message->Flatten(&file);
}


status_t
MainApp::_RestoreCurrentPlaylist(BMessage* message) const
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK
                || path.Append(kCurrentPlaylistFilename) != B_OK) {
                return B_ERROR;
        }

        BFile file(path.Path(), B_READ_ONLY);
        if (file.InitCheck() != B_OK)
                return B_ERROR;

        return message->Unflatten(&file);
}


void
MainApp::_InstallPlaylistMimeType()
{
        // install mime type of documents
        BMimeType mime(kBinaryPlaylistMimeString);
        status_t ret = mime.InitCheck();
        if (ret != B_OK) {
                fprintf(stderr, "Could not init native document mime type (%s): %s.\n",
                        kBinaryPlaylistMimeString, strerror(ret));
                return;
        }

        if (mime.IsInstalled() && !(modifiers() & B_SHIFT_KEY)) {
                // mime is already installed, and the user is not
                // pressing the shift key to force a re-install
                return;
        }

        ret = mime.Install();
        if (ret != B_OK && ret != B_FILE_EXISTS) {
                fprintf(stderr, "Could not install native document mime type (%s): %s.\n",
                        kBinaryPlaylistMimeString, strerror(ret));
                return;
        }
        // set preferred app
        ret = mime.SetPreferredApp(kAppSig);
        if (ret != B_OK) {
                fprintf(stderr, "Could not set native document preferred app: %s\n",
                        strerror(ret));
        }

        // set descriptions
        ret = mime.SetShortDescription("MediaPlayer playlist");
        if (ret != B_OK) {
                fprintf(stderr, "Could not set short description of mime type: %s\n",
                        strerror(ret));
        }
        ret = mime.SetLongDescription("MediaPlayer binary playlist file");
        if (ret != B_OK) {
                fprintf(stderr, "Could not set long description of mime type: %s\n",
                        strerror(ret));
        }

        // set extensions
        BMessage message('extn');
        message.AddString("extensions", "playlist");
        ret = mime.SetFileExtensions(&message);
        if (ret != B_OK) {
                fprintf(stderr, "Could not set extensions of mime type: %s\n",
                        strerror(ret));
        }

        // set sniffer rule
        char snifferRule[32];
        uint32 bigEndianMagic = B_HOST_TO_BENDIAN_INT32(kPlaylistMagicBytes);
        sprintf(snifferRule, "0.9 ('%4s')", (const char*)&bigEndianMagic);
        ret = mime.SetSnifferRule(snifferRule);
        if (ret != B_OK) {
                BString parseError;
                BMimeType::CheckSnifferRule(snifferRule, &parseError);
                fprintf(stderr, "Could not set sniffer rule of mime type: %s\n",
                        parseError.String());
        }

        // set playlist icon
        BResources* resources = AppResources();
                // does not need to be freed (belongs to BApplication base)
        if (resources != NULL) {
                size_t size;
                const void* iconData = resources->LoadResource('VICN', "PlaylistIcon",
                        &size);
                if (iconData != NULL && size > 0) {
                        if (mime.SetIcon(reinterpret_cast<const uint8*>(iconData), size)
                                != B_OK) {
                                fprintf(stderr, "Could not set vector icon of mime type.\n");
                        }
                } else {
                        fprintf(stderr, "Could not find icon in app resources "
                                "(data: %p, size: %ld).\n", iconData, size);
                }
        } else
                fprintf(stderr, "Could not find app resources.\n");
}


// #pragma mark - main


int
main()
{
        EventQueue::CreateDefault();

        srand(system_time());

        gMainApp = new MainApp;
        gMainApp->Run();
        delete gMainApp;

        EventQueue::DeleteDefault();

        return 0;
}