root/src/apps/soundrecorder/TransportButton.cpp
/*
 * Copyright 2005, Jérôme Duval. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Inspired by SoundCapture from Be newsletter (Media Kit Basics: Consumers 
 * and Producers)
 */

#include <Bitmap.h>
#include <Debug.h>
#include <MessageFilter.h>
#include <Window.h>

#include <map>

#include "TransportButton.h"
#include "DrawingTidbits.h"

using std::map;

class BitmapStash {
// Bitmap stash is a simple class to hold all the lazily-allocated
// bitmaps that the TransportButton needs when rendering itself.
// signature is a combination of the different enabled, pressed, playing, etc.
// flavors of a bitmap. If the stash does not have a particular bitmap,
// it turns around to ask the button to create one and stores it for next time.
public:
        BitmapStash(TransportButton *);
        ~BitmapStash();
        BBitmap *GetBitmap(uint32 signature);
        
private:
        TransportButton *owner;
        map<uint32, BBitmap *> stash;
};


BitmapStash::BitmapStash(TransportButton *owner)
        :       owner(owner)
{
}


BBitmap *
BitmapStash::GetBitmap(uint32 signature)
{
        if (stash.find(signature) == stash.end()) {
                BBitmap *newBits = owner->MakeBitmap(signature);
                ASSERT(newBits);
                stash[signature] = newBits;
        }
        
        return stash[signature];
}


BitmapStash::~BitmapStash()
{
        // delete all the bitmaps
        for (map<uint32, BBitmap *>::iterator i = stash.begin(); 
                i != stash.end(); i++)
                delete (*i).second;
}


class PeriodicMessageSender {
        // used to send a specified message repeatedly when holding down a button
public:
        static PeriodicMessageSender *Launch(BMessenger target,
                const BMessage *message, bigtime_t period);
        void Quit();

private:
        PeriodicMessageSender(BMessenger target, const BMessage *message,
                bigtime_t period);
        ~PeriodicMessageSender() {}
                // use quit

        static status_t TrackBinder(void *);
        void Run();
        
        BMessenger target;
        BMessage message;

        bigtime_t period;
        
        bool requestToQuit;
};


PeriodicMessageSender::PeriodicMessageSender(BMessenger target,
        const BMessage *message, bigtime_t period)
        :       target(target),
                message(*message),
                period(period),
                requestToQuit(false)
{
}


PeriodicMessageSender *
PeriodicMessageSender::Launch(BMessenger target, const BMessage *message,
        bigtime_t period)
{
        PeriodicMessageSender *result = new PeriodicMessageSender(target, 
                message, period);
        thread_id thread = spawn_thread(&PeriodicMessageSender::TrackBinder,
                "ButtonRepeatingThread", B_NORMAL_PRIORITY, result);
        
        if (thread <= 0 || resume_thread(thread) != B_OK) {
                // didn't start, don't leak self
                delete result;
                result = 0;
        }

        return result;
}


void 
PeriodicMessageSender::Quit()
{
        requestToQuit = true;
}


status_t 
PeriodicMessageSender::TrackBinder(void *castToThis)
{
        ((PeriodicMessageSender *)castToThis)->Run();
        return 0;
}


void 
PeriodicMessageSender::Run()
{
        for (;;) {
                snooze(period);
                if (requestToQuit)
                        break;
                target.SendMessage(&message);
        }
        delete this;
}


class SkipButtonKeypressFilter : public BMessageFilter {
public:
        SkipButtonKeypressFilter(uint32 shortcutKey, uint32 shortcutModifier,
                TransportButton *target);

protected:
        filter_result Filter(BMessage *message, BHandler **handler);

private:
        uint32 shortcutKey;
        uint32 shortcutModifier;
        TransportButton *target;
};


SkipButtonKeypressFilter::SkipButtonKeypressFilter(uint32 shortcutKey,
        uint32 shortcutModifier, TransportButton *target)
        :       BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE),
                shortcutKey(shortcutKey),
                shortcutModifier(shortcutModifier),
                target(target)
{
}


filter_result 
SkipButtonKeypressFilter::Filter(BMessage *message, BHandler **handler)
{
        if (target->IsEnabled()
                && (message->what == B_KEY_DOWN || message->what == B_KEY_UP)) {
                uint32 modifiers;
                uint32 rawKeyChar = 0;
                uint8 byte = 0;
                int32 key = 0;
                
                if (message->FindInt32("modifiers", (int32 *)&modifiers) != B_OK
                        || message->FindInt32("raw_char", (int32 *)&rawKeyChar) != B_OK
                        || message->FindInt8("byte", (int8 *)&byte) != B_OK
                        || message->FindInt32("key", &key) != B_OK)
                        return B_DISPATCH_MESSAGE;

                modifiers &= B_SHIFT_KEY | B_COMMAND_KEY | B_CONTROL_KEY
                        | B_OPTION_KEY | B_MENU_KEY;
                        // strip caps lock, etc.

                if (modifiers == shortcutModifier && rawKeyChar == shortcutKey) {
                        if (message->what == B_KEY_DOWN)
                                target->ShortcutKeyDown();
                        else
                                target->ShortcutKeyUp();
                        
                        return B_SKIP_MESSAGE;
                }
        }

        // let others deal with this
        return B_DISPATCH_MESSAGE;
}


TransportButton::TransportButton(BRect frame, const char *name,
        const unsigned char *normalBits,
        const unsigned char *pressedBits,
        const unsigned char *disabledBits,
        BMessage *invokeMessage, BMessage *startPressingMessage,
        BMessage *pressingMessage, BMessage *donePressingMessage, bigtime_t period,
        uint32 key, uint32 modifiers, uint32 resizeFlags)
        :       BControl(frame, name, "", invokeMessage, resizeFlags, 
                        B_WILL_DRAW | B_NAVIGABLE),
                bitmaps(new BitmapStash(this)),
                normalBits(normalBits),
                pressedBits(pressedBits),
                disabledBits(disabledBits),
                startPressingMessage(startPressingMessage),
                pressingMessage(pressingMessage),
                donePressingMessage(donePressingMessage),
                pressingPeriod(period),
                mouseDown(false),
                keyDown(false),
                messageSender(0),
                keyPressFilter(0)
{
        if (key)
                keyPressFilter = new SkipButtonKeypressFilter(key, modifiers, this);
}


void 
TransportButton::AttachedToWindow()
{
        _inherited::AttachedToWindow();
        if (keyPressFilter)
                Window()->AddCommonFilter(keyPressFilter);
        
        // transparent to reduce flicker
        SetViewColor(B_TRANSPARENT_COLOR);
}


void 
TransportButton::DetachedFromWindow()
{
        if (keyPressFilter) {
                Window()->RemoveCommonFilter(keyPressFilter);
                delete keyPressFilter;
        }
        _inherited::DetachedFromWindow();
}


TransportButton::~TransportButton()
{
        delete startPressingMessage;
        delete pressingMessage;
        delete donePressingMessage;
        delete bitmaps;
}


void 
TransportButton::WindowActivated(bool state)
{
        if (!state)
                ShortcutKeyUp();
        
        _inherited::WindowActivated(state);
}


void 
TransportButton::SetEnabled(bool on)
{
        _inherited::SetEnabled(on);
        if (!on)
                ShortcutKeyUp();        
}


const unsigned char *
TransportButton::BitsForMask(uint32 mask) const
{
        switch (mask) {
                case 0:
                        return normalBits;
                case kDisabledMask:
                        return disabledBits;
                case kPressedMask:
                        return pressedBits;
                default:
                        break;
        }       
        TRESPASS();
        return 0;
}


BBitmap *
TransportButton::MakeBitmap(uint32 mask)
{
        BBitmap *result = new BBitmap(Bounds(), B_CMAP8);
        result->SetBits(BitsForMask(mask), (Bounds().Width() + 1) 
                * (Bounds().Height() + 1), 0, B_CMAP8);

        ReplaceTransparentColor(result, Parent()->ViewColor());
        
        return result;
}


uint32 
TransportButton::ModeMask() const
{
        return (IsEnabled() ? 0 : kDisabledMask)
                | (Value() ? kPressedMask : 0);
}


void 
TransportButton::Draw(BRect)
{
        DrawBitmapAsync(bitmaps->GetBitmap(ModeMask()));
}


void 
TransportButton::StartPressing()
{
        SetValue(1);
        if (startPressingMessage)
                Invoke(startPressingMessage);
        
        if (pressingMessage) {
                ASSERT(pressingMessage);
                messageSender = PeriodicMessageSender::Launch(Messenger(),
                        pressingMessage, pressingPeriod);
        }
}


void 
TransportButton::MouseCancelPressing()
{
        if (!mouseDown || keyDown)
                return;

        mouseDown = false;

        if (pressingMessage) {
                ASSERT(messageSender);
                PeriodicMessageSender *sender = messageSender;
                messageSender = 0;
                sender->Quit();
        }

        if (donePressingMessage)
                Invoke(donePressingMessage);
        SetValue(0);
}


void 
TransportButton::DonePressing()
{       
        if (pressingMessage) {
                ASSERT(messageSender);
                PeriodicMessageSender *sender = messageSender;
                messageSender = 0;
                sender->Quit();
        }

        Invoke();
        SetValue(0);
}


void 
TransportButton::MouseStartPressing()
{
        if (mouseDown)
                return;
        
        mouseDown = true;
        if (!keyDown)
                StartPressing();
}


void 
TransportButton::MouseDonePressing()
{
        if (!mouseDown)
                return;
        
        mouseDown = false;
        if (!keyDown)
                DonePressing();
}


void 
TransportButton::ShortcutKeyDown()
{
        if (!IsEnabled())
                return;

        if (keyDown)
                return;
        
        keyDown = true;
        if (!mouseDown)
                StartPressing();
}


void 
TransportButton::ShortcutKeyUp()
{
        if (!keyDown)
                return;
        
        keyDown = false;
        if (!mouseDown)
                DonePressing();
}


void 
TransportButton::MouseDown(BPoint)
{
        if (!IsEnabled())
                return;

        ASSERT(Window()->Flags() & B_ASYNCHRONOUS_CONTROLS);
        SetTracking(true);
        SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
        MouseStartPressing();
}

void 
TransportButton::MouseMoved(BPoint point, uint32 code, const BMessage *)
{
        if (IsTracking() && Bounds().Contains(point) != Value()) {
                if (!Value())
                        MouseStartPressing();
                else
                        MouseCancelPressing();
        }
}

void 
TransportButton::MouseUp(BPoint point)
{
        if (IsTracking()) {
                if (Bounds().Contains(point))
                        MouseDonePressing();
                else
                        MouseCancelPressing();
                SetTracking(false);
        }
}

void 
TransportButton::SetStartPressingMessage(BMessage *message)
{
        delete startPressingMessage;
        startPressingMessage = message;
}

void 
TransportButton::SetPressingMessage(BMessage *message)
{
        delete pressingMessage;
        pressingMessage = message;
}

void 
TransportButton::SetDonePressingMessage(BMessage *message)
{
        delete donePressingMessage;
        donePressingMessage = message;
}

void 
TransportButton::SetPressingPeriod(bigtime_t newTime)
{
        pressingPeriod = newTime;
}


PlayPauseButton::PlayPauseButton(BRect frame, const char *name,
        BMessage *invokeMessage, BMessage *blinkMessage,
        uint32 key, uint32 modifiers, uint32 resizeFlags)
        :       TransportButton(frame, name, kPlayButtonBitmapBits, 
                        kPressedPlayButtonBitmapBits,
                        kDisabledPlayButtonBitmapBits, invokeMessage, NULL,
                        NULL, NULL, 0, key, modifiers, resizeFlags),
                fState(PlayPauseButton::kStopped),
                fLastModeMask(0),
                fRunner(NULL),
                fBlinkMessage(blinkMessage)
{
}

void 
PlayPauseButton::SetStopped()
{
        if (fState == kStopped || fState == kAboutToPlay)
                return;
        
        fState = kStopped;
        delete fRunner;
        fRunner = NULL;
        Invalidate();
}

void 
PlayPauseButton::SetPlaying()
{
        if (fState == kAboutToPause)
                return;
        
        // in playing state blink the LED on and off
        if (fState == kPlayingLedOn)
                fState = kPlayingLedOff;
        else
                fState = kPlayingLedOn;
        
        Invalidate();
}

const bigtime_t kPlayingBlinkPeriod = 600000;

void 
PlayPauseButton::SetPaused()
{
        if (fState == kAboutToPlay)
                return;

        // in paused state blink the LED on and off
        if (fState == kPausedLedOn)
                fState = kPausedLedOff;
        else
                fState = kPausedLedOn;
        
        Invalidate();
}

uint32 
PlayPauseButton::ModeMask() const
{
        if (!IsEnabled())
                return kDisabledMask;
        
        uint32 result = 0;

        if (Value())
                result = kPressedMask;

        if (fState == kPlayingLedOn || fState == kAboutToPlay)
                result |= kPlayingMask;
        else if (fState == kAboutToPause || fState == kPausedLedOn)     
                result |= kPausedMask;
        
        return result;
}

const unsigned char *
PlayPauseButton::BitsForMask(uint32 mask) const
{
        switch (mask) {
                case kPlayingMask:
                        return kPlayingPlayButtonBitmapBits;
                case kPlayingMask | kPressedMask:
                        return kPressedPlayingPlayButtonBitmapBits;
                case kPausedMask:
                        return kPausedPlayButtonBitmapBits;
                case kPausedMask | kPressedMask:
                        return kPressedPausedPlayButtonBitmapBits;
                default:
                        return _inherited::BitsForMask(mask);
        }       
        TRESPASS();
        return 0;
}


void 
PlayPauseButton::StartPressing()
{
        if (fState == kPlayingLedOn || fState == kPlayingLedOff)
                fState = kAboutToPause;
        else
                fState = kAboutToPlay;
        
        _inherited::StartPressing();
}

void 
PlayPauseButton::MouseCancelPressing()
{
        if (fState == kAboutToPause)
                fState = kPlayingLedOn;
        else
                fState = kStopped;
        
        _inherited::MouseCancelPressing();
}

void 
PlayPauseButton::DonePressing()
{
        if (fState == kAboutToPause) {
                fState = kPausedLedOn;
        } else if (fState == kAboutToPlay) {
                fState = kPlayingLedOn;
                if (!fRunner && fBlinkMessage)
                        fRunner = new BMessageRunner(Messenger(), fBlinkMessage, 
                                kPlayingBlinkPeriod);
        }
        
        _inherited::DonePressing();
}


RecordButton::RecordButton(BRect frame, const char *name,
        BMessage *invokeMessage, BMessage *blinkMessage,
        uint32 key, uint32 modifiers, uint32 resizeFlags)
        :       TransportButton(frame, name, kRecordButtonBitmapBits, 
                        kPressedRecordButtonBitmapBits,
                        kDisabledRecordButtonBitmapBits, invokeMessage, NULL, NULL,
                        NULL, 0, key, modifiers, resizeFlags),
                fState(RecordButton::kStopped),
                fLastModeMask(0),
                fRunner(NULL),
                fBlinkMessage(blinkMessage)
{
}

void 
RecordButton::SetStopped()
{
        if (fState == kStopped || fState == kAboutToRecord)
                return;
        
        fState = kStopped;
        delete fRunner;
        fRunner = NULL;
        Invalidate();
}

const bigtime_t kRecordingBlinkPeriod = 600000;

void 
RecordButton::SetRecording()
{
        if (fState == kAboutToStop)
                return;

        if (fState == kRecordingLedOff)
                fState = kRecordingLedOn;
        else
                fState = kRecordingLedOff;
        
        Invalidate();
}

uint32 
RecordButton::ModeMask() const
{
        if (!IsEnabled())
                return kDisabledMask;
        
        uint32 result = 0;

        if (Value())
                result = kPressedMask;

        if (fState == kAboutToStop || fState == kRecordingLedOn)
                result |= kRecordingMask;
        
        return result;
}

const unsigned char *
RecordButton::BitsForMask(uint32 mask) const
{
        switch (mask) {
                case kRecordingMask:
                        return kRecordingRecordButtonBitmapBits;
                case kRecordingMask | kPressedMask:
                        return kPressedRecordingRecordButtonBitmapBits;
                default:
                        return _inherited::BitsForMask(mask);
        }       
        TRESPASS();
        return 0;
}


void 
RecordButton::StartPressing()
{
        if (fState == kRecordingLedOn || fState == kRecordingLedOff)
                fState = kAboutToStop;
        else
                fState = kAboutToRecord;
        
        _inherited::StartPressing();
}

void 
RecordButton::MouseCancelPressing()
{
        if (fState == kAboutToStop)
                fState = kRecordingLedOn;
        else
                fState = kStopped;
        
        _inherited::MouseCancelPressing();
}

void 
RecordButton::DonePressing()
{
        if (fState == kAboutToStop) {
                fState = kStopped;
                delete fRunner;
                fRunner = NULL;
        } else if (fState == kAboutToRecord) {
                fState = kRecordingLedOn;
                if (!fRunner && fBlinkMessage)
                        fRunner = new BMessageRunner(Messenger(), fBlinkMessage, 
                                kRecordingBlinkPeriod);
        }
        
        _inherited::DonePressing();
}