root/src/apps/mediaplayer/VideoView.cpp
/*
 * Copyright 2006-2010 Stephan Aßmus <superstippi@gmx.de>
 * All rights reserved. Distributed under the terms of the MIT license.
 */


#include "VideoView.h"

#include <stdio.h>

#include <Application.h>
#include <Bitmap.h>
#include <Region.h>
#include <Screen.h>
#include <WindowScreen.h>

#include "Settings.h"
#include "SubtitleBitmap.h"


enum {
        MSG_INVALIDATE = 'ivdt'
};


VideoView::VideoView(BRect frame, const char* name, uint32 resizeMask)
        :
        BView(frame, name, resizeMask, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE
                | B_PULSE_NEEDED),
        fVideoFrame(Bounds()),
        fOverlayMode(false),
        fIsPlaying(false),
        fIsFullscreen(false),
        fFullscreenControlsVisible(false),
        fFirstPulseAfterFullscreen(false),
        fSendHideCounter(0),
        fLastMouseMove(system_time()),

        fSubtitleBitmap(new SubtitleBitmap),
        fSubtitleFrame(),
        fSubtitleMaxButtom(Bounds().bottom),
        fHasSubtitle(false),
        fSubtitleChanged(false),

        fGlobalSettingsListener(this)
{
        SetViewColor(B_TRANSPARENT_COLOR);
        SetHighColor(0, 0, 0);

        // create some hopefully sensible default overlay restrictions
        // they will be adjusted when overlays are actually used
        fOverlayRestrictions.min_width_scale = 0.25;
        fOverlayRestrictions.max_width_scale = 8.0;
        fOverlayRestrictions.min_height_scale = 0.25;
        fOverlayRestrictions.max_height_scale = 8.0;

        Settings::Default()->AddListener(&fGlobalSettingsListener);
        _AdoptGlobalSettings();

//SetSubTitle("<b><i>This</i></b> is a <font color=\"#00ff00\">test</font>!"
//      "\nWith a <i>short</i> line and a <b>long</b> line.");
}


VideoView::~VideoView()
{
        Settings::Default()->RemoveListener(&fGlobalSettingsListener);
        delete fSubtitleBitmap;
}


void
VideoView::Draw(BRect updateRect)
{
        BRegion outSideVideoRegion(updateRect);

        if (LockBitmap()) {
                if (const BBitmap* bitmap = GetBitmap()) {
                        outSideVideoRegion.Exclude(fVideoFrame);
                        if (!fOverlayMode)
                                _DrawBitmap(bitmap);
                        else
                                FillRect(fVideoFrame & updateRect, B_SOLID_LOW);
                }
                UnlockBitmap();
        }

        if (outSideVideoRegion.CountRects() > 0)
                FillRegion(&outSideVideoRegion);

        if (fHasSubtitle)
                _DrawSubtitle();
}


void
VideoView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_OBJECT_CHANGED:
                        // received from fGlobalSettingsListener
                        // TODO: find out which object, if we ever watch more than
                        // the global settings instance...
                        _AdoptGlobalSettings();
                        break;
                case MSG_INVALIDATE:
                {
                        BRect dirty;
                        if (message->FindRect("dirty", &dirty) == B_OK)
                                Invalidate(dirty);
                        break;
                }
                default:
                        BView::MessageReceived(message);
        }
}


/*!
        Disables the screen saver, and hides the full screen controls.
*/
void
VideoView::Pulse()
{
        if (!fIsFullscreen || !fIsPlaying)
                return;

        bigtime_t now = system_time();
        if (now - fLastMouseMove > 1500000) {
                fLastMouseMove = now;
                BPoint where;
                uint32 buttons;
                GetMouse(&where, &buttons, false);
                if (buttons == 0) {
                        // Hide the full screen controls (and the mouse pointer)
                        // after a while
                        if (fFullscreenControlsVisible || fFirstPulseAfterFullscreen) {
                                if (fSendHideCounter == 0 || fSendHideCounter == 3) {
                                        // Send after 1.5s and after 4.5s
                                        BMessage message(M_HIDE_FULL_SCREEN_CONTROLS);
                                        message.AddPoint("where", where);
                                        if (fSendHideCounter > 0)
                                                message.AddBool("force", true);
                                        Window()->PostMessage(&message, Window());
                                }
                                fSendHideCounter++;
                                fFirstPulseAfterFullscreen = false;
                        }

                        // Take care of disabling the screen saver
                        ConvertToScreen(&where);
                        set_mouse_position((int32)where.x, (int32)where.y);
                }
        }
}


void
VideoView::MouseMoved(BPoint where, uint32 transit,
        const BMessage* dragMessage)
{
        fLastMouseMove = system_time();
}


// #pragma mark -


void
VideoView::SetBitmap(const BBitmap* bitmap)
{
        VideoTarget::SetBitmap(bitmap);
        // Attention: Don't lock the window, if the bitmap is NULL. Otherwise
        // we're going to deadlock when the window tells the node manager to
        // stop the nodes (Window -> NodeManager -> VideoConsumer -> VideoView
        // -> Window).
        if (!bitmap || LockLooperWithTimeout(10000) != B_OK)
                return;

        if (LockBitmap()) {
                if (fOverlayMode
                        || (bitmap->Flags() & B_BITMAP_WILL_OVERLAY) != 0) {
                        if (!fOverlayMode) {
                                // init overlay
                                rgb_color key;
                                status_t ret = SetViewOverlay(bitmap, bitmap->Bounds(),
                                        fVideoFrame, &key, B_FOLLOW_ALL,
                                        B_OVERLAY_FILTER_HORIZONTAL | B_OVERLAY_FILTER_VERTICAL);
                                if (ret == B_OK) {
                                        fOverlayKeyColor = key;
                                        SetLowColor(key);
                                        snooze(20000);
                                        FillRect(fVideoFrame, B_SOLID_LOW);
                                        Sync();
                                        // use overlay from here on
                                        _SetOverlayMode(true);

                                        // update restrictions
                                        overlay_restrictions restrictions;
                                        if (bitmap->GetOverlayRestrictions(&restrictions) == B_OK)
                                                fOverlayRestrictions = restrictions;
                                } else {
                                        // try again next time
                                        // synchronous draw
                                        FillRect(fVideoFrame);
                                        Sync();
                                }
                        } else {
                                // transfer overlay channel
                                rgb_color key;
                                SetViewOverlay(bitmap, bitmap->Bounds(), fVideoFrame,
                                        &key, B_FOLLOW_ALL, B_OVERLAY_FILTER_HORIZONTAL
                                                | B_OVERLAY_FILTER_VERTICAL
                                                | B_OVERLAY_TRANSFER_CHANNEL);
                        }
                } else if (fOverlayMode
                        && (bitmap->Flags() & B_BITMAP_WILL_OVERLAY) == 0) {
                        _SetOverlayMode(false);
                        ClearViewOverlay();
                        SetViewColor(B_TRANSPARENT_COLOR);
                }
                if (!fOverlayMode) {
                        if (fSubtitleChanged) {
                                _LayoutSubtitle();
                                Invalidate(fVideoFrame | fSubtitleFrame);
                        } else if (fHasSubtitle
                                && fVideoFrame.Intersects(fSubtitleFrame)) {
                                Invalidate(fVideoFrame);
                        } else
                                _DrawBitmap(bitmap);
                }

                UnlockBitmap();
        }
        UnlockLooper();
}


void
VideoView::GetOverlayScaleLimits(float* minScale, float* maxScale) const
{
        *minScale = max_c(fOverlayRestrictions.min_width_scale,
                fOverlayRestrictions.min_height_scale);
        *maxScale = max_c(fOverlayRestrictions.max_width_scale,
                fOverlayRestrictions.max_height_scale);
}


void
VideoView::OverlayScreenshotPrepare()
{
        // TODO: Do nothing if the current bitmap is in RGB color space
        // and no overlay. Otherwise, convert current bitmap to RGB color
        // space an draw it in place of the normal display.
}


void
VideoView::OverlayScreenshotCleanup()
{
        // TODO: Do nothing if the current bitmap is in RGB color space
        // and no overlay. Otherwise clean view area with overlay color.
}


bool
VideoView::UseOverlays() const
{
        return fUseOverlays;
}


bool
VideoView::IsOverlayActive()
{
        bool active = false;
        if (LockBitmap()) {
                active = fOverlayMode;
                UnlockBitmap();
        }
        return active;
}


void
VideoView::DisableOverlay()
{
        if (!fOverlayMode)
                return;

        FillRect(Bounds());
        Sync();

        ClearViewOverlay();
        snooze(20000);
        Sync();
        _SetOverlayMode(false);
}


void
VideoView::SetPlaying(bool playing)
{
        fIsPlaying = playing;
}


void
VideoView::SetFullscreen(bool fullScreen)
{
        fIsFullscreen = fullScreen;
        fSendHideCounter = 0;
        fFirstPulseAfterFullscreen = true;
}


void
VideoView::SetFullscreenControlsVisible(bool visible)
{
        fFullscreenControlsVisible = visible;
        fSendHideCounter = 0;
}


void
VideoView::SetVideoFrame(const BRect& frame)
{
        if (fVideoFrame == frame)
                return;

        BRegion invalid(fVideoFrame | frame);
        invalid.Exclude(frame);
        Invalidate(&invalid);

        fVideoFrame = frame;

        fSubtitleBitmap->SetVideoBounds(fVideoFrame.OffsetToCopy(B_ORIGIN));
        _LayoutSubtitle();
}


void
VideoView::SetSubTitle(const char* text)
{
        BRect oldSubtitleFrame = fSubtitleFrame;

        if (text == NULL || text[0] == '\0') {
                fHasSubtitle = false;
                fSubtitleChanged = true;
        } else {
                fHasSubtitle = true;
                // If the subtitle frame still needs to be invalidated during
                // normal playback, make sure we don't unset the fSubtitleChanged
                // flag. It will be reset after drawing the subtitle once.
                fSubtitleChanged = fSubtitleBitmap->SetText(text) || fSubtitleChanged;
                if (fSubtitleChanged)
                        _LayoutSubtitle();
        }

        if (!fIsPlaying && Window() != NULL) {
                // If we are playing, the new subtitle will be displayed,
                // or the old one removed from screen, as soon as the next
                // frame is shown. Otherwise we need to invalidate manually.
                // But we are not in the window thread and we shall not lock
                // it or we may dead-locks.
                BMessage message(MSG_INVALIDATE);
                message.AddRect("dirty", oldSubtitleFrame | fSubtitleFrame);
                Window()->PostMessage(&message);
        }
}


void
VideoView::SetSubTitleMaxBottom(float bottom)
{
        if (bottom == fSubtitleMaxButtom)
                return;

        fSubtitleMaxButtom = bottom;

        BRect oldSubtitleFrame = fSubtitleFrame;
        _LayoutSubtitle();
        Invalidate(fSubtitleFrame | oldSubtitleFrame);
}


// #pragma mark -


void
VideoView::_DrawBitmap(const BBitmap* bitmap)
{
        SetDrawingMode(B_OP_COPY);
        uint32 options = B_WAIT_FOR_RETRACE;
        if (fUseBilinearScaling)
                options |= B_FILTER_BITMAP_BILINEAR;

        DrawBitmapAsync(bitmap, bitmap->Bounds(), fVideoFrame, options);
}


void
VideoView::_DrawSubtitle()
{
        SetDrawingMode(B_OP_ALPHA);
        SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);

        DrawBitmapAsync(fSubtitleBitmap->Bitmap(), fSubtitleFrame.LeftTop());

        // Unless the subtitle frame intersects the video frame, we don't have
        // to draw the subtitle again.
        fSubtitleChanged = false;
}


void
VideoView::_AdoptGlobalSettings()
{
        mpSettings settings;
        Settings::Default()->Get(settings);

        fUseOverlays = settings.useOverlays;
        fUseBilinearScaling = settings.scaleBilinear;

        switch (settings.subtitleSize) {
                case mpSettings::SUBTITLE_SIZE_SMALL:
                        fSubtitleBitmap->SetCharsPerLine(45.0);
                        break;
                case mpSettings::SUBTITLE_SIZE_MEDIUM:
                        fSubtitleBitmap->SetCharsPerLine(36.0);
                        break;
                case mpSettings::SUBTITLE_SIZE_LARGE:
                        fSubtitleBitmap->SetCharsPerLine(32.0);
                        break;
        }

        fSubtitlePlacement = settings.subtitlePlacement;

        _LayoutSubtitle();
        Invalidate();
}


void
VideoView::_SetOverlayMode(bool overlayMode)
{
        fOverlayMode = overlayMode;
        fSubtitleBitmap->SetOverlayMode(overlayMode);
}


void
VideoView::_LayoutSubtitle()
{
        if (!fHasSubtitle)
                return;

        const BBitmap* subtitleBitmap = fSubtitleBitmap->Bitmap();
        if (subtitleBitmap == NULL)
                return;

        fSubtitleFrame = subtitleBitmap->Bounds();

        BPoint offset;
        offset.x = (fVideoFrame.left + fVideoFrame.right
                - fSubtitleFrame.Width()) / 2;
        switch (fSubtitlePlacement) {
                default:
                case mpSettings::SUBTITLE_PLACEMENT_BOTTOM_OF_VIDEO:
                        offset.y = min_c(fSubtitleMaxButtom, fVideoFrame.bottom)
                                - fSubtitleFrame.Height();
                        break;
                case mpSettings::SUBTITLE_PLACEMENT_BOTTOM_OF_SCREEN:
                {
                        // Center between video and screen bottom, if there is still
                        // enough room.
                        float centeredOffset = (fVideoFrame.bottom + fSubtitleMaxButtom
                                - fSubtitleFrame.Height()) / 2;
                        float maxOffset = fSubtitleMaxButtom - fSubtitleFrame.Height();
                        offset.y = min_c(centeredOffset, maxOffset);
                        break;
                }
        }

        fSubtitleFrame.OffsetTo(offset);
}