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


// NOTE: Based on my code in the BeOS interface for the VLC media player
// that I did during the VLC 0.4.3 - 0.4.6 times. Code not written by me
// removed. -Stephan Aßmus


#include "TransportControlGroup.h"

#include <stdio.h>
#include <string.h>

#include <Shape.h>
#include <SpaceLayoutItem.h>
#include <String.h>
#include <ToolTipManager.h>
#include <Window.h>

#include "DurationView.h"
#include "PeakView.h"
#include "PlaybackState.h"
#include "PlayPauseButton.h"
#include "PositionToolTip.h"
#include "SeekSlider.h"
#include "SymbolButton.h"
#include "VolumeSlider.h"

enum {
        MSG_SEEK                                = 'seek',
        MSG_SEEK_HOVER                  = 'hovr',
        MSG_PLAY                                = 'play',
        MSG_STOP                                = 'stop',
        MSG_REWIND                              = 'rwnd',
        MSG_FORWARD                             = 'frwd',
        MSG_SKIP_BACKWARDS              = 'skpb',
        MSG_SKIP_FORWARD                = 'skpf',
        MSG_SET_VOLUME                  = 'stvl',
        MSG_SET_MUTE                    = 'stmt',
        MSG_DURATION_TOOLTIP    = 'msdt',
};

// the range of the volume sliders (in dB)
#define kVolumeDbMax    6.0
#define kVolumeDbMin    -60.0
// a power function for non linear sliders
#define kVolumeDbExpPositive 1.4        // for dB values > 0
#define kVolumeDbExpNegative 1.9        // for dB values < 0

#define kVolumeFactor   100
#define kPositionFactor 3000


TransportControlGroup::TransportControlGroup(BRect frame, bool useSkipButtons,
                bool usePeakView, bool useWindButtons)
        :
        BGroupView(B_VERTICAL, 0),
        fSeekSlider(NULL),
        fDurationView(NULL),
        fPositionToolTip(NULL),
        fPeakView(NULL),
        fVolumeSlider(NULL),
        fSkipBack(NULL),
        fSkipForward(NULL),
        fRewind(NULL),
        fForward(NULL),
        fPlayPause(NULL),
        fStop(NULL),
        fMute(NULL),
        fSymbolScale(1.0f),
        fLastEnabledButtons(0)
{
        // Pick a symbol size based on the current system font size, but make
        // sure the size is uneven, so the pointy shapes have their middle on
        // a pixel instead of between two pixels.
        float symbolHeight = int(be_plain_font->Size() / 1.33) | 1;

        BGroupView* seekGroup = new BGroupView(B_HORIZONTAL, 0);
        fSeekLayout = seekGroup->GroupLayout();
        GroupLayout()->AddView(seekGroup);

        // Seek slider
        fSeekSlider = new SeekSlider("seek slider", new BMessage(MSG_SEEK),
                new BMessage(MSG_SEEK_HOVER), 0, kPositionFactor);
        fSeekLayout->AddView(fSeekSlider);

        fPositionToolTip = new PositionToolTip();
        fPositionToolTip->SetAlignment(BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP));
        fPositionToolTip->SetMouseRelativeLocation(BPoint(0, 0));
        fSeekSlider->SetToolTip(fPositionToolTip);

        // Duration view
        fDurationView = new DurationView("duration view");
        fSeekLayout->AddView(fDurationView);

        // Buttons

        uint32 topBottomBorder = BControlLook::B_TOP_BORDER
                | BControlLook::B_BOTTOM_BORDER;

        if (useSkipButtons) {
                // Skip Back
                fSkipBack = new SymbolButton(B_EMPTY_STRING,
                        _CreateSkipBackwardsShape(symbolHeight),
                        new BMessage(MSG_SKIP_BACKWARDS),
                        BControlLook::B_LEFT_BORDER | topBottomBorder);
                // Skip Foward
                fSkipForward = new SymbolButton(B_EMPTY_STRING,
                        _CreateSkipForwardShape(symbolHeight),
                        new BMessage(MSG_SKIP_FORWARD),
                        BControlLook::B_RIGHT_BORDER | topBottomBorder);
        }

        if (useWindButtons) {
                // Rewind
                fRewind = new SymbolButton(B_EMPTY_STRING,
                        _CreateRewindShape(symbolHeight), new BMessage(MSG_REWIND),
                        useSkipButtons ? topBottomBorder
                                : BControlLook::B_LEFT_BORDER | topBottomBorder);
                // Forward
                fForward = new SymbolButton(B_EMPTY_STRING,
                        _CreateForwardShape(symbolHeight), new BMessage(MSG_FORWARD),
                        useSkipButtons ? topBottomBorder
                                : BControlLook::B_RIGHT_BORDER | topBottomBorder);
        }

        // Play Pause
        fPlayPause = new PlayPauseButton(B_EMPTY_STRING,
                _CreatePlayShape(symbolHeight), _CreatePauseShape(symbolHeight),
                new BMessage(MSG_PLAY), useWindButtons || useSkipButtons
                        ? topBottomBorder
                        : topBottomBorder | BControlLook::B_LEFT_BORDER);

        // Stop
        fStop = new SymbolButton(B_EMPTY_STRING,
                _CreateStopShape(symbolHeight), new BMessage(MSG_STOP),
                useWindButtons || useSkipButtons ? topBottomBorder
                        : topBottomBorder | BControlLook::B_RIGHT_BORDER);

        // Mute
        fMute = new SymbolButton(B_EMPTY_STRING,
                _CreateSpeakerShape(floorf(symbolHeight * 0.9)),
                new BMessage(MSG_SET_MUTE), 0);

        // Volume Slider
        fVolumeSlider = new VolumeSlider("volume slider",
                _DbToGain(_ExponentialToLinear(kVolumeDbMin)) * kVolumeFactor,
                _DbToGain(_ExponentialToLinear(kVolumeDbMax)) * kVolumeFactor,
                kVolumeFactor, new BMessage(MSG_SET_VOLUME));
        fVolumeSlider->SetValue(_DbToGain(_ExponentialToLinear(0.0))
                * kVolumeFactor);

        // Peak view
        if (usePeakView)
                fPeakView = new PeakView("peak view", false, false);

        // Layout the controls

        BGroupView* buttonGroup = new BGroupView(B_HORIZONTAL, 0);
        BGroupLayout* buttonLayout = buttonGroup->GroupLayout();

        if (fSkipBack != NULL)
                buttonLayout->AddView(fSkipBack);
        if (fRewind != NULL)
                buttonLayout->AddView(fRewind);
        buttonLayout->AddView(fPlayPause);
        buttonLayout->AddView(fStop);
        if (fForward != NULL)
                buttonLayout->AddView(fForward);
        if (fSkipForward != NULL)
                buttonLayout->AddView(fSkipForward);

        BGroupView* controlGroup = new BGroupView(B_HORIZONTAL, 0);
        GroupLayout()->AddView(controlGroup);
        fControlLayout = controlGroup->GroupLayout();
        fControlLayout->AddView(buttonGroup, 0.6f);
        fControlLayout->AddItem(BSpaceLayoutItem::CreateHorizontalStrut(5));
        fControlLayout->AddView(fMute);
        fControlLayout->AddView(fVolumeSlider);
        if (fPeakView != NULL)
                fControlLayout->AddView(fPeakView, 0.6f);

        // Figure out the visual insets of the slider bounds towards the slider
        // bar, and use that as insets for the rest of the layout.
        float inset = fSeekSlider->BarFrame().left;
        float hInset = inset - fSeekSlider->BarFrame().top;
        if (hInset < 0.0f)
                hInset = 0.0f;

        fSeekLayout->SetInsets(0, hInset, 5, 0);
        fControlLayout->SetInsets(inset, hInset, inset, inset);

        BSize size = fControlLayout->MinSize();
        size.width *= 3;
        size.height = B_SIZE_UNSET;
        fControlLayout->SetExplicitMaxSize(size);
        fControlLayout->SetExplicitAlignment(BAlignment(B_ALIGN_CENTER,
                B_ALIGN_TOP));
}


TransportControlGroup::~TransportControlGroup()
{
        if (!fSeekSlider->IsEnabled())
                fPositionToolTip->ReleaseReference();
}


void
TransportControlGroup::AttachedToWindow()
{
        SetEnabled(EnabledButtons());

        // we are now a valid BHandler
        fSeekSlider->SetTarget(this);
        fVolumeSlider->SetTarget(this);
        if (fSkipBack)
                fSkipBack->SetTarget(this);
        if (fSkipForward)
                fSkipForward->SetTarget(this);
        if (fRewind)
                fRewind->SetTarget(this);
        if (fForward)
                fForward->SetTarget(this);
        fPlayPause->SetTarget(this);
        fStop->SetTarget(this);
        fMute->SetTarget(this);
}


void
TransportControlGroup::GetPreferredSize(float* _width, float* _height)
{
        BSize size = GroupLayout()->MinSize();
        if (_width != NULL)
                *_width = size.width;
        if (_height != NULL)
                *_height = size.height;
}


void
TransportControlGroup::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_PLAY:
                        _TogglePlaying();
                        break;
                case MSG_STOP:
                        _Stop();
                        break;

                case MSG_REWIND:
                        _Rewind();
                        break;
                case MSG_FORWARD:
                        _Forward();
                        break;

                case MSG_SKIP_BACKWARDS:
                        _SkipBackward();
                        break;
                case MSG_SKIP_FORWARD:
                        _SkipForward();
                        break;

                case MSG_SET_VOLUME:
                        _UpdateVolume();
                        break;
                case MSG_SET_MUTE:
                        _ToggleMute();
                        break;

                case MSG_SEEK:
                        _UpdatePosition();
                        break;

                case MSG_DURATION_TOOLTIP:
                {
                        BPoint tipPoint;
                        fSeekSlider->GetMouse(&tipPoint, NULL, false);
                        tipPoint.y = 0;
                        fSeekSlider->ConvertToScreen(&tipPoint);
                        BToolTipManager::Manager()->ShowTip(fPositionToolTip, tipPoint, this);
                        break;
                }

                case MSG_SEEK_HOVER:
                {
                        int32 value;
                        if (message->FindInt32("value", &value) == B_OK) {
                                bigtime_t position = TimePositionFor(value / (float)kPositionFactor);
                                fPositionToolTip->Update(position, fDurationView->TimeDuration());
                                Looper()->PostMessage(MSG_DURATION_TOOLTIP, this);
                        }
                        break;
                }

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


// #pragma mark - default implementation for the virtuals


uint32
TransportControlGroup::EnabledButtons()
{
        return fLastEnabledButtons;
}


void TransportControlGroup::TogglePlaying() {}
void TransportControlGroup::Stop() {}
void TransportControlGroup::Rewind() {}
void TransportControlGroup::Forward() {}
void TransportControlGroup::SkipBackward() {}
void TransportControlGroup::SkipForward() {}
void TransportControlGroup::VolumeChanged(float value) {}
void TransportControlGroup::ToggleMute() {}
void TransportControlGroup::PositionChanged(float value) {}
bigtime_t TransportControlGroup::TimePositionFor(float value) { return 0; }

// #pragma mark -


float
TransportControlGroup::_LinearToExponential(float dbIn)
{
        float db = dbIn;
        if (db >= 0) {
                db = db * (pow(fabs(kVolumeDbMax), (1.0 / kVolumeDbExpPositive))
                        / fabs(kVolumeDbMax));
                db = pow(db, kVolumeDbExpPositive);
        } else {
                db = -db;
                db = db * (pow(fabs(kVolumeDbMin), (1.0 / kVolumeDbExpNegative))
                        / fabs(kVolumeDbMin));
                db = pow(db, kVolumeDbExpNegative);
                db = -db;
        }
        return db;
}


float
TransportControlGroup::_ExponentialToLinear(float dbIn)
{
        float db = dbIn;
        if (db >= 0) {
                db = pow(db, (1.0 / kVolumeDbExpPositive));
                db = db * (fabs(kVolumeDbMax) / pow(fabs(kVolumeDbMax),
                        (1.0 / kVolumeDbExpPositive)));
        } else {
                db = -db;
                db = pow(db, (1.0 / kVolumeDbExpNegative));
                db = db * (fabs(kVolumeDbMin) / pow(fabs(kVolumeDbMin),
                        (1.0 / kVolumeDbExpNegative)));
                db = -db;
        }
        return db;
}


float
TransportControlGroup::_DbToGain(float db)
{
        return pow(10.0, db / 20.0);
}


float
TransportControlGroup::_GainToDb(float gain)
{
        return 20.0 * log10(gain);
}


// #pragma mark -


void
TransportControlGroup::SetEnabled(uint32 buttons)
{
        if (!LockLooper())
                return;

        fLastEnabledButtons = buttons;

        fSeekSlider->SetEnabled(buttons & SEEK_ENABLED);
        fSeekSlider->SetToolTip((buttons & SEEK_ENABLED) != 0
                ? fPositionToolTip : NULL);

        fVolumeSlider->SetEnabled(buttons & VOLUME_ENABLED);
        fMute->SetEnabled(buttons & VOLUME_ENABLED);

        if (fSkipBack)
                fSkipBack->SetEnabled(buttons & SKIP_BACK_ENABLED);
        if (fSkipForward)
                fSkipForward->SetEnabled(buttons & SKIP_FORWARD_ENABLED);
        if (fRewind)
                fRewind->SetEnabled(buttons & SEEK_BACK_ENABLED);
        if (fForward)
                fForward->SetEnabled(buttons & SEEK_FORWARD_ENABLED);

        fPlayPause->SetEnabled(buttons & PLAYBACK_ENABLED);
        fStop->SetEnabled(buttons & PLAYBACK_ENABLED);

        UnlockLooper();
}


// #pragma mark -


void
TransportControlGroup::SetPlaybackState(uint32 state)
{
        if (!LockLooper())
                return;

        switch (state) {
                case PLAYBACK_STATE_PLAYING:
                        fPlayPause->SetPlaying();
                        break;
                case PLAYBACK_STATE_PAUSED:
                        fPlayPause->SetPaused();
                        break;
                case PLAYBACK_STATE_STOPPED:
                        fPlayPause->SetStopped();
                        break;
        }

        UnlockLooper();
}


void
TransportControlGroup::SetSkippable(bool backward, bool forward)
{
        if (!LockLooper())
                return;

        if (fSkipBack)
                fSkipBack->SetEnabled(backward);
        if (fSkipForward)
                fSkipForward->SetEnabled(forward);

        UnlockLooper();
}


// #pragma mark -


void
TransportControlGroup::SetAudioEnabled(bool enabled)
{
        if (!LockLooper())
                return;

        fMute->SetEnabled(enabled);
        fVolumeSlider->SetEnabled(enabled);

        UnlockLooper();
}


void
TransportControlGroup::SetMuted(bool mute)
{
        if (!LockLooper())
                return;

        fVolumeSlider->SetMuted(mute);

        UnlockLooper();
}


void
TransportControlGroup::SetVolume(float value)
{
        float db = _GainToDb(value);
        float exponential = _LinearToExponential(db);
        float gain = _DbToGain(exponential);
        int32 pos = (int32)(floorf(gain * kVolumeFactor + 0.5));

        fVolumeSlider->SetValue(pos);
}


void
TransportControlGroup::SetAudioChannelCount(int32 count)
{
        fPeakView->SetChannelCount(count);
}


void
TransportControlGroup::SetPosition(float value, bigtime_t position,
        bigtime_t duration)
{
        fDurationView->Update(position, duration);

        if (fSeekSlider->IsTracking())
                return;
        fSeekSlider->SetPosition(value);
}


float
TransportControlGroup::Position() const
{
        return fSeekSlider->Position();
}


void
TransportControlGroup::SetDisabledString(const char* string)
{
        fSeekSlider->SetDisabledString(string);
}


void
TransportControlGroup::SetSymbolScale(float scale)
{
        if (scale == fSymbolScale)
                return;

        fSymbolScale = scale;

        if (fSeekSlider != NULL)
                fSeekSlider->SetSymbolScale(scale);
        if (fVolumeSlider != NULL) {
                fVolumeSlider->SetBarThickness(fVolumeSlider->PreferredBarThickness()
                        * scale);
        }
        if (fDurationView != NULL)
                fDurationView->SetSymbolScale(scale);

        float symbolHeight = int(scale * be_plain_font->Size() / 1.33) | 1;

        if (fSkipBack != NULL)
                fSkipBack->SetSymbol(_CreateSkipBackwardsShape(symbolHeight));
        if (fSkipForward != NULL)
                fSkipForward->SetSymbol(_CreateSkipForwardShape(symbolHeight));
        if (fRewind != NULL)
                fRewind->SetSymbol(_CreateRewindShape(symbolHeight));
        if (fForward != NULL)
                fForward->SetSymbol(_CreateForwardShape(symbolHeight));
        if (fPlayPause != NULL) {
                fPlayPause->SetSymbols(_CreatePlayShape(symbolHeight),
                        _CreatePauseShape(symbolHeight));
        }
        if (fStop != NULL)
                fStop->SetSymbol(_CreateStopShape(symbolHeight));
        if (fMute != NULL)
                fMute->SetSymbol(_CreateSpeakerShape(floorf(symbolHeight * 0.9)));

        // Figure out the visual insets of the slider bounds towards the slider
        // bar, and use that as insets for the rest of the layout.
        float barInset = fSeekSlider->BarFrame().left;
        float inset = barInset * scale;
        float hInset = inset - fSeekSlider->BarFrame().top;
        if (hInset < 0.0f)
                hInset = 0.0f;

        fSeekLayout->SetInsets(inset - barInset, hInset, inset, 0);
        fSeekLayout->SetSpacing(inset - barInset);
        fControlLayout->SetInsets(inset, hInset, inset, inset);
        fControlLayout->SetSpacing(inset - barInset);

        ResizeTo(Bounds().Width(), GroupLayout()->MinSize().height);
}

// #pragma mark -


void
TransportControlGroup::_TogglePlaying()
{
        TogglePlaying();
}


void
TransportControlGroup::_Stop()
{
        fPlayPause->SetStopped();
        Stop();
}


void
TransportControlGroup::_Rewind()
{
        Rewind();
}


void
TransportControlGroup::_Forward()
{
        Forward();
}


void
TransportControlGroup::_SkipBackward()
{
        SkipBackward();
}


void
TransportControlGroup::_SkipForward()
{
        SkipForward();
}


void
TransportControlGroup::_UpdateVolume()
{
        float pos = fVolumeSlider->Value() / (float)kVolumeFactor;
        float db = _ExponentialToLinear(_GainToDb(pos));
        float gain = _DbToGain(db);
        VolumeChanged(gain);
}


void
TransportControlGroup::_ToggleMute()
{
        fVolumeSlider->SetMuted(!fVolumeSlider->IsMuted());
        ToggleMute();
}


void
TransportControlGroup::_UpdatePosition()
{
        PositionChanged(fSeekSlider->Value() / (float)kPositionFactor);

        BMessage msg(MSG_DURATION_TOOLTIP);
        Window()->PostMessage(&msg, this);
}


// #pragma mark -


BShape*
TransportControlGroup::_CreateSkipBackwardsShape(float height) const
{
        BShape* shape = new BShape();

        float stopWidth = ceilf(height / 6);

        shape->MoveTo(BPoint(-stopWidth, height));
        shape->LineTo(BPoint(0, height));
        shape->LineTo(BPoint(0, 0));
        shape->LineTo(BPoint(-stopWidth, 0));
        shape->Close();

        shape->MoveTo(BPoint(0, height / 2));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->Close();

        shape->MoveTo(BPoint(height, height / 2));
        shape->LineTo(BPoint(height * 2, height));
        shape->LineTo(BPoint(height * 2, 0));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreateSkipForwardShape(float height) const
{
        BShape* shape = new BShape();

        shape->MoveTo(BPoint(height, height / 2));
        shape->LineTo(BPoint(0, height));
        shape->LineTo(BPoint(0, 0));
        shape->Close();

        shape->MoveTo(BPoint(height * 2, height / 2));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->Close();

        float stopWidth = ceilf(height / 6);

        shape->MoveTo(BPoint(height * 2, height));
        shape->LineTo(BPoint(height * 2 + stopWidth, height));
        shape->LineTo(BPoint(height * 2 + stopWidth, 0));
        shape->LineTo(BPoint(height * 2, 0));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreateRewindShape(float height) const
{
        BShape* shape = new BShape();

        shape->MoveTo(BPoint(0, height / 2));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->Close();

        shape->MoveTo(BPoint(height, height / 2));
        shape->LineTo(BPoint(height * 2, height));
        shape->LineTo(BPoint(height * 2, 0));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreateForwardShape(float height) const
{
        BShape* shape = new BShape();

        shape->MoveTo(BPoint(height, height / 2));
        shape->LineTo(BPoint(0, height));
        shape->LineTo(BPoint(0, 0));
        shape->Close();

        shape->MoveTo(BPoint(height * 2, height / 2));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreatePlayShape(float height) const
{
        BShape* shape = new BShape();

        float step = floorf(height / 8);

        shape->MoveTo(BPoint(height + step, height / 2));
        shape->LineTo(BPoint(-step, height + step));
        shape->LineTo(BPoint(-step, 0 - step));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreatePauseShape(float height) const
{
        BShape* shape = new BShape();

        float stemWidth = floorf(height / 3);

        shape->MoveTo(BPoint(0, height));
        shape->LineTo(BPoint(stemWidth, height));
        shape->LineTo(BPoint(stemWidth, 0));
        shape->LineTo(BPoint(0, 0));
        shape->Close();

        shape->MoveTo(BPoint(height - stemWidth, height));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->LineTo(BPoint(height - stemWidth, 0));
        shape->Close();

        return shape;
}


BShape*
TransportControlGroup::_CreateStopShape(float height) const
{
        BShape* shape = new BShape();

        shape->MoveTo(BPoint(0, height));
        shape->LineTo(BPoint(height, height));
        shape->LineTo(BPoint(height, 0));
        shape->LineTo(BPoint(0, 0));
        shape->Close();

        return shape;
}


static void
add_bow(BShape* shape, float offset, float size, float height, float step)
{
        float width = floorf(size * 2 / 3);
        float outerControlHeight = size * 2 / 3;
        float outerControlWidth = size / 4;
        float innerControlHeight = size / 2;
        float innerControlWidth = size / 5;
        // left/bottom
        shape->MoveTo(BPoint(offset, height / 2 + size));
        // outer bow, to middle
        shape->BezierTo(
                BPoint(offset + outerControlWidth, height / 2 + size),
                BPoint(offset + width, height / 2 + outerControlHeight),
                BPoint(offset + width, height / 2)
        );
        // outer bow, to left/top
        shape->BezierTo(
                BPoint(offset + width, height / 2 - outerControlHeight),
                BPoint(offset + outerControlWidth, height / 2 - size),
                BPoint(offset, height / 2 - size)
        );
        // inner bow, to middle
        shape->BezierTo(
                BPoint(offset + innerControlWidth, height / 2 - size),
                BPoint(offset + width - step, height / 2 - innerControlHeight),
                BPoint(offset + width - step, height / 2)
        );
        // inner bow, back to left/bottom
        shape->BezierTo(
                BPoint(offset + width - step, height / 2 + innerControlHeight),
                BPoint(offset + innerControlWidth, height / 2 + size),
                BPoint(offset, height / 2 + size)
        );
        shape->Close();
}


BShape*
TransportControlGroup::_CreateSpeakerShape(float height) const
{
        BShape* shape = new BShape();

        float step = floorf(height / 8);
        float magnetWidth = floorf(height / 5);
        float chassieWidth = floorf(height / 1.5);
        float chassieHeight = floorf(height / 4);

        shape->MoveTo(BPoint(0, height - step));
        shape->LineTo(BPoint(magnetWidth, height - step));
        shape->LineTo(BPoint(magnetWidth, height / 2 + chassieHeight));
        shape->LineTo(BPoint(magnetWidth + chassieWidth - step, height + step));
        shape->LineTo(BPoint(magnetWidth + chassieWidth, height + step));
        shape->LineTo(BPoint(magnetWidth + chassieWidth, -step));
        shape->LineTo(BPoint(magnetWidth + chassieWidth - step, -step));
        shape->LineTo(BPoint(magnetWidth, height / 2 - chassieHeight));
        shape->LineTo(BPoint(magnetWidth, step));
        shape->LineTo(BPoint(0, step));
        shape->Close();

        float offset = magnetWidth + chassieWidth + step * 2;
        add_bow(shape, offset, 3 * step, height, step * 2);
        offset += step * 2;
        add_bow(shape, offset, 5 * step, height, step * 2);
        offset += step * 2;
        add_bow(shape, offset, 7 * step, height, step * 2);

        return shape;
}