root/src/bin/desklink/MediaReplicant.cpp
/*
 * Copyright 2003-2018, Haiku. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Jérôme Duval
 *              François Revol
 *              Marcus Overhagen
 *              Jonas Sundström
 *              Axel Dörfler, axeld@pinc-software.de.
 *              Stephan Aßmus <superstippi@gmx.de>
 *              Puck Meerburg, puck@puckipedia.nl
 */


//! Volume control, and media shortcuts in Deskbar


#include <new>
#include <stdio.h>

#include <Alert.h>
#include <Bitmap.h>
#include <Catalog.h>
#include <Entry.h>
#include <File.h>
#include <FindDirectory.h>
#include <IconUtils.h>
#include <MenuItem.h>
#include <Path.h>
#include <PopUpMenu.h>
#include <Resources.h>
#include <Roster.h>
#include <String.h>
#include <StringView.h>
#include <TextView.h>
#include <ToolTip.h>
#include <ToolTipManager.h>

#include "desklink.h"
#include "MixerControl.h"
#include "VolumeWindow.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MediaReplicant"


static const uint32 kMsgOpenMediaSettings = 'mese';
static const uint32 kMsgOpenSoundSettings = 'sose';
static const uint32 kMsgOpenMediaPlayer = 'omep';
static const uint32 kMsgToggleBeep = 'tdbp';
static const uint32 kMsgVolumeWhich = 'svwh';

static const char* kReplicantName = "MediaReplicant";
        // R5 name needed, Media prefs manel removes by name

static const char* kSettingsFile = "x-vnd.Haiku-desklink";


class VolumeToolTip : public BTextToolTip {
public:
        VolumeToolTip(int32 which = VOLUME_USE_MIXER)
                :
                BTextToolTip(""),
                fWhich(which)
        {
        }

        virtual ~VolumeToolTip()
        {
        }

        virtual void AttachedToWindow()
        {
                Update();
        }

        void SetWhich(int32 which)
        {
                fWhich = which;
        }

        void Update()
        {
                if (!Lock())
                        return;

                BTextView* view = (BTextView*)View();

                if (fMuteMessage.Length() != 0)
                        view->SetText(fMuteMessage.String());
                else {
                        MixerControl control;
                        control.Connect(fWhich);

                        BString text;
                        text.SetToFormat(B_TRANSLATE("%g dB"), control.Volume());
                        view->SetText(text.String());
                }
                Unlock();
        }

        void SetMuteMessage(const char* message)
        {
                fMuteMessage = message == NULL ? "" : message;
        }

private:
        int32                   fWhich;
        BString                 fMuteMessage;
};


class MediaReplicant : public BView {
public:
                                                        MediaReplicant(BRect frame, const char* name,
                                                                uint32 resizeMask = B_FOLLOW_ALL,
                                                                uint32 flags = B_WILL_DRAW | B_NAVIGABLE
                                                                        | B_PULSE_NEEDED);
                                                        MediaReplicant(BMessage* archive);

        virtual                                 ~MediaReplicant();

        // archiving overrides
        static  MediaReplicant* Instantiate(BMessage* data);
        virtual status_t                Archive(BMessage* data, bool deep = true) const;

        // BView overrides
        virtual void                    AttachedToWindow();
        virtual void                    MouseDown(BPoint point);
        virtual void                    Draw(BRect updateRect);
        virtual void                    MessageReceived(BMessage* message);

private:
                        status_t                _LaunchByPath(const char* path);
                        status_t                _LaunchBySignature(const char* signature);
                        void                    _Launch(const char* prettyName,
                                                                const char* signature, directory_which base,
                                                                const char* fileName);
                        void                    _LoadSettings();
                        void                    _SaveSettings();
                        void                    _Init();
                        BBitmap*                _LoadIcon(BResources& resources, const char* name);

                        void                    _DisconnectMixer();
                        status_t                _ConnectMixer();

                        MixerControl*   fMixerControl;

                        BBitmap*                fIcon;
                        BBitmap*                fMutedIcon;
                        VolumeWindow*   fVolumeSlider;
                        bool                    fDontBeep;
                                // don't beep on volume change
                        int32                   fVolumeWhich;
                                // which volume parameter to act on (Mixer/Phys.Output)
                        bool                            fMuted;
};


status_t
our_image(image_info& image)
{
        int32 cookie = 0;
        while (get_next_image_info(B_CURRENT_TEAM, &cookie, &image) == B_OK) {
                if ((char*)our_image >= (char*)image.text
                        && (char*)our_image <= (char*)image.text + image.text_size)
                        return B_OK;
        }

        return B_ERROR;
}


//      #pragma mark -


MediaReplicant::MediaReplicant(BRect frame, const char* name,
                uint32 resizeMask, uint32 flags)
        :
        BView(frame, name, resizeMask, flags),
        fMixerControl(NULL),
        fVolumeSlider(NULL),
        fMuted(false)
{
        _Init();
}


MediaReplicant::MediaReplicant(BMessage* message)
        :
        BView(message),
        fMixerControl(NULL),
        fVolumeSlider(NULL),
        fMuted(false)
{
        _Init();
}


MediaReplicant::~MediaReplicant()
{
        delete fIcon;
        _SaveSettings();
        _DisconnectMixer();
}


MediaReplicant*
MediaReplicant::Instantiate(BMessage* data)
{
        if (!validate_instantiation(data, kReplicantName))
                return NULL;

        return new(std::nothrow) MediaReplicant(data);
}


status_t
MediaReplicant::Archive(BMessage* data, bool deep) const
{
        status_t status = BView::Archive(data, deep);
        if (status < B_OK)
                return status;

        return data->AddString("add_on", kAppSignature);
}


void
MediaReplicant::AttachedToWindow()
{
        AdoptParentColors();

        _ConnectMixer();

        BView::AttachedToWindow();
}


void
MediaReplicant::Draw(BRect rect)
{
        SetDrawingMode(B_OP_OVER);
        DrawBitmap(fMuted ? fMutedIcon : fIcon);
}


void
MediaReplicant::MouseDown(BPoint point)
{
        int32 buttons = B_PRIMARY_MOUSE_BUTTON;
        if (Looper() != NULL && Looper()->CurrentMessage() != NULL)
                Looper()->CurrentMessage()->FindInt32("buttons", &buttons);

        BPoint where = ConvertToScreen(point);

        if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
                BPopUpMenu* menu = new BPopUpMenu("", false, false);
                menu->SetFont(be_plain_font);

                menu->AddItem(new BMenuItem(
                        B_TRANSLATE("Media preferences" B_UTF8_ELLIPSIS),
                        new BMessage(kMsgOpenMediaSettings)));
                menu->AddItem(new BMenuItem(
                        B_TRANSLATE("Sounds preferences" B_UTF8_ELLIPSIS),
                        new BMessage(kMsgOpenSoundSettings)));

                menu->AddSeparatorItem();

                menu->AddItem(new BMenuItem(B_TRANSLATE("Open MediaPlayer"),
                        new BMessage(kMsgOpenMediaPlayer)));

                menu->AddSeparatorItem();

                BMenu* subMenu = new BMenu(B_TRANSLATE("Options"));
                menu->AddItem(subMenu);

                BMenuItem* item = new BMenuItem(B_TRANSLATE("Control physical output"),
                        new BMessage(kMsgVolumeWhich));
                item->SetMarked(fVolumeWhich == VOLUME_USE_PHYS_OUTPUT);
                subMenu->AddItem(item);

                item = new BMenuItem(B_TRANSLATE("Beep"),
                        new BMessage(kMsgToggleBeep));
                item->SetMarked(!fDontBeep);
                subMenu->AddItem(item);

                menu->SetTargetForItems(this);
                subMenu->SetTargetForItems(this);

                menu->Go(where, true, true, BRect(where - BPoint(4, 4),
                        where + BPoint(4, 4)));

        } else if ((buttons & B_TERTIARY_MOUSE_BUTTON) != 0) {
                if (fMixerControl != NULL) {
                        fMixerControl->SetMute(!fMuted);
                        fMuted = fMixerControl->Mute();
                        VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
                        if (tip != NULL) {
                                tip->SetMuteMessage(fMuted ? B_TRANSLATE("Muted"): NULL);
                                tip->Update();
                                ShowToolTip(tip);
                        }
                        Invalidate();
                }

        } else {
                // Show VolumeWindow
                fVolumeSlider = new VolumeWindow(BRect(where.x, where.y,
                        where.x + 207, where.y + 19), fDontBeep, fVolumeWhich);
                fVolumeSlider->Show();
        }
}


void
MediaReplicant::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kMsgOpenMediaPlayer:
                        _Launch("MediaPlayer", "application/x-vnd.Haiku-MediaPlayer",
                                B_SYSTEM_APPS_DIRECTORY, "MediaPlayer");
                        break;

                case kMsgOpenMediaSettings:
                        _Launch("Media Preferences", "application/x-vnd.Haiku-Media",
                                B_SYSTEM_PREFERENCES_DIRECTORY, "Media");
                        break;

                case kMsgOpenSoundSettings:
                        _Launch("Sounds Preferences", "application/x-vnd.Haiku-Sounds",
                                B_SYSTEM_PREFERENCES_DIRECTORY, "Sounds");
                        break;

                case kMsgToggleBeep:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void**)&item) != B_OK)
                                return;

                        item->SetMarked(!item->IsMarked());
                        fDontBeep = !item->IsMarked();
                        break;
                }

                case kMsgVolumeWhich:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void**)&item) != B_OK)
                                return;

                        item->SetMarked(!item->IsMarked());
                        fVolumeWhich = item->IsMarked()
                                ? VOLUME_USE_PHYS_OUTPUT : VOLUME_USE_MIXER;

                        if (_ConnectMixer() != B_OK
                                && fVolumeWhich == VOLUME_USE_PHYS_OUTPUT) {
                                // unable to switch to physical output
                                item->SetMarked(false);
                                fVolumeWhich = VOLUME_USE_MIXER;
                                _ConnectMixer();
                        }

                        if (VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip())) {
                                tip->SetWhich(fVolumeWhich);
                                tip->Update();
                        }
                        break;
                }

                case B_MOUSE_WHEEL_CHANGED:
                {
                        float deltaY;
                        if (message->FindFloat("be:wheel_delta_y", &deltaY) == B_OK
                                && deltaY != 0.0 && fMixerControl != NULL) {
                                fMixerControl->ChangeVolumeBy(deltaY < 0 ? 6 : -6);

                                VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
                                if (tip != NULL) {
                                        tip->Update();
                                        ShowToolTip(tip);
                                }
                        }
                        break;
                }

                case B_MEDIA_NEW_PARAMETER_VALUE:
                {
                        if (fMixerControl != NULL && !fMixerControl->Connected())
                                return;

                        bool setMuted = fMixerControl->Mute();
                        if (setMuted != fMuted) {
                                fMuted = setMuted;
                                VolumeToolTip* tip = dynamic_cast<VolumeToolTip*>(ToolTip());
                                if (tip != NULL) {
                                        tip->SetMuteMessage(fMuted ? B_TRANSLATE("Muted") : NULL);
                                        tip->Update();
                                }
                                Invalidate();
                        }
                        break;
                }

                case B_MEDIA_SERVER_STARTED:
                        _ConnectMixer();
                        break;

                case B_MEDIA_SERVER_QUIT:
                        _DisconnectMixer();
                        break;

                case B_MEDIA_NODE_CREATED:
                {
                        // It's not enough to wait for B_MEDIA_SERVER_STARTED message, as
                        // the mixer will still be getting loaded by the media server
                        media_node mixerNode;
                        media_node_id mixerNodeID;
                        BMediaRoster* roster = BMediaRoster::CurrentRoster();
                        if (roster != NULL
                                && message->FindInt32("media_node_id", &mixerNodeID) == B_OK
                                && roster->GetNodeFor(mixerNodeID, &mixerNode) == B_OK) {
                                if (mixerNode.kind == B_SYSTEM_MIXER)
                                        _ConnectMixer();
                                roster->ReleaseNode(mixerNode);
                        }
                        break;
                }

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


status_t
MediaReplicant::_LaunchByPath(const char* path)
{
        entry_ref ref;
        status_t status = get_ref_for_path(path, &ref);
        if (status != B_OK)
                return status;

        status = be_roster->Launch(&ref);
        if (status != B_ALREADY_RUNNING)
                return status;

        // The application runs already, bring it to front

        app_info appInfo;
        status = be_roster->GetAppInfo(&ref, &appInfo);
        if (status != B_OK)
                return status;

        return be_roster->ActivateApp(appInfo.team);
}


status_t
MediaReplicant::_LaunchBySignature(const char* signature)
{
        status_t status = be_roster->Launch(signature);
        if (status != B_ALREADY_RUNNING)
                return status;

        // The application runs already, bring it to front

        app_info appInfo;
        status = be_roster->GetAppInfo(signature, &appInfo);
        if (status != B_OK)
                return status;

        return be_roster->ActivateApp(appInfo.team);
}


void
MediaReplicant::_Launch(const char* prettyName, const char* signature,
        directory_which base, const char* fileName)
{
        BPath path;
        status_t status = find_directory(base, &path);
        if (status == B_OK)
                path.Append(fileName);

        // launch the application
        if (_LaunchBySignature(signature) != B_OK
                && _LaunchByPath(path.Path()) != B_OK) {
                BString message = B_TRANSLATE("Couldn't launch ");
                message << prettyName;

                BAlert* alert = new BAlert(
                        B_TRANSLATE_COMMENT("desklink", "Title of an alert box"), 
                        message.String(), B_TRANSLATE("OK"));
                alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
                alert->Go();
        }
}


void
MediaReplicant::_LoadSettings()
{
        fDontBeep = false;
        fVolumeWhich = VOLUME_USE_MIXER;

        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, false) < B_OK)
                return;

        path.Append(kSettingsFile);

        BFile settings(path.Path(), B_READ_ONLY);
        if (settings.InitCheck() < B_OK)
                return;

        BMessage msg;
        if (msg.Unflatten(&settings) < B_OK)
                return;

        msg.FindInt32("volwhich", &fVolumeWhich);
        msg.FindBool("dontbeep", &fDontBeep);
}


void
MediaReplicant::_SaveSettings()
{
        BPath path;
        if (find_directory(B_USER_SETTINGS_DIRECTORY, &path, false) < B_OK)
                return;

        path.Append(kSettingsFile);

        BFile settings(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
        if (settings.InitCheck() < B_OK)
                return;

        BMessage msg('CNFG');
        msg.AddInt32("volwhich", fVolumeWhich);
        msg.AddBool("dontbeep", fDontBeep);

        ssize_t size = 0;
        msg.Flatten(&settings, &size);
}


void
MediaReplicant::_Init()
{
        image_info info;
        if (our_image(info) != B_OK)
                return;

        BFile file(info.name, B_READ_ONLY);
        if (file.InitCheck() != B_OK)
                return;

        BResources resources(&file);
        if (resources.InitCheck() != B_OK)
                return;

        fIcon = _LoadIcon(resources, "Speaker");
        fMutedIcon = _LoadIcon(resources, "SpeakerMuted");

        _LoadSettings();

        SetToolTip(new VolumeToolTip(fVolumeWhich));
}


BBitmap*
MediaReplicant::_LoadIcon(BResources& resources, const char* name)
{
        size_t size;
        const void* data = resources.LoadResource(B_VECTOR_ICON_TYPE, name, &size);
        if (data == NULL)
                return NULL;

        // Scale tray icon
        BBitmap* icon = new BBitmap(Bounds(), B_RGBA32);
        if (icon->InitCheck() != B_OK
                || BIconUtils::GetVectorIcon((const uint8*)data, size, icon) != B_OK) {
                delete icon;
                return NULL;
        }
        return icon;
}


void
MediaReplicant::_DisconnectMixer()
{
        BMediaRoster* roster = BMediaRoster::CurrentRoster();
        if (roster == NULL)
                return;

        roster->StopWatching(this, B_MEDIA_SERVER_STARTED);
        roster->StopWatching(this, B_MEDIA_SERVER_QUIT);
        roster->StopWatching(this, B_MEDIA_NODE_CREATED);

        if (fMixerControl == NULL)
                return;

        if (fMixerControl->MuteNode() != media_node::null) {
                roster->StopWatching(this, fMixerControl->MuteNode(),
                        B_MEDIA_NEW_PARAMETER_VALUE);
        }

        delete fMixerControl;
        fMixerControl = NULL;
}


status_t
MediaReplicant::_ConnectMixer()
{
        _DisconnectMixer();

        BMediaRoster* roster = BMediaRoster::Roster();
        if (roster == NULL)
                return B_ERROR;

        roster->StartWatching(this, B_MEDIA_SERVER_STARTED);
        roster->StartWatching(this, B_MEDIA_SERVER_QUIT);
        roster->StartWatching(this, B_MEDIA_NODE_CREATED);

        fMixerControl = new MixerControl(fVolumeWhich);

        const char* errorString = NULL;
        float volume = 0.0;
        fMixerControl->Connect(fVolumeWhich, &volume, &errorString);

        if (errorString != NULL) {
                SetToolTip(errorString);
                delete fMixerControl;
                fMixerControl = NULL;
                return B_ERROR;
        }

        if (fMixerControl->MuteNode() != media_node::null) {
                roster->StartWatching(this, fMixerControl->MuteNode(),
                        B_MEDIA_NEW_PARAMETER_VALUE);
                fMuted = fMixerControl->Mute();
        }

        return B_OK;
}


//      #pragma mark -


extern "C" BView*
instantiate_deskbar_item(float maxWidth, float maxHeight)
{
        return new MediaReplicant(BRect(0, 0, maxHeight - 1, maxHeight - 1),
                kReplicantName);
}