/*
 * track.cpp - implementation of classes concerning tracks -> necessary for
 *             all track-like objects (beat/bassline, sample-track...)
 *
 * Copyright (c) 2004-2012 Tobias Doerffel <tobydox/at/users.sourceforge.net>
 *
 * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

/** \file track.cpp
 *  \brief All classes concerning tracks and track-like objects
 */

/*
 * \mainpage Track classes
 *
 * \section introduction Introduction
 *
 * \todo fill this out
 */

#include "track.h"

#include <assert.h>
#include <cstdio>

#include <QtGui/QLayout>
#include <QtGui/QMenu>
#include <QtGui/QMouseEvent>
#include <QtGui/QPainter>
#include <QtGui/QStyleOption>


#include "AutomationPattern.h"
#include "AutomationTrack.h"
#include "bb_editor.h"
#include "bb_track.h"
#include "bb_track_container.h"
#include "config_mgr.h"
#include "Clipboard.h"
#include "embed.h"
#include "engine.h"
#include "gui_templates.h"
#include "InstrumentTrack.h"
#include "MainWindow.h"
#include "mmp.h"
#include "pixmap_button.h"
#include "ProjectJournal.h"
#include "sample_track.h"
#include "song.h"
#include "string_pair_drag.h"
#include "templates.h"
#include "text_float.h"
#include "tooltip.h"
#include "track_container.h"


/*! The width of the resize grip in pixels
 */
const Sint16 RESIZE_GRIP_WIDTH = 4;

/*! The size of the track buttons in pixels
 */
const Uint16 TRACK_OP_BTN_WIDTH = 20;
const Uint16 TRACK_OP_BTN_HEIGHT = 14;


/*! A pointer for that text bubble used when moving segments, etc.
 *
 * In a number of situations, LMMS displays a floating text bubble
 * beside the cursor as you move or resize elements of a track about.
 * This pointer keeps track of it, as you only ever need one at a time.
 */
textFloat * trackContentObjectView::s_textFloat = NULL;


// ===========================================================================
// trackContentObject
// ===========================================================================
/*! \brief Create a new trackContentObject
 *
 *  Creates a new track content object for the given track.
 *
 * \param _track The track that will contain the new object
 */
trackContentObject::trackContentObject( track * _track ) :
	Model( _track ),
	m_track( _track ),
	m_name( QString::null ),
	m_startPosition(),
	m_length(),
	m_mutedModel( false, this, tr( "Muted" ) )
{
	if( getTrack() )
	{
		getTrack()->addTCO( this );
	}
	setJournalling( false );
	movePosition( 0 );
	changeLength( 0 );
	setJournalling( true );
}




/*! \brief Destroy a trackContentObject
 *
 *  Destroys the given track content object.
 *
 */
trackContentObject::~trackContentObject()
{
	emit destroyedTCO();

	if( getTrack() )
	{
		getTrack()->removeTCO( this );
	}
}




/*! \brief Move this trackContentObject's position in time
 *
 *  If the track content object has moved, update its position.  We
 *  also add a journal entry for undo and update the display.
 *
 * \param _pos The new position of the track content object.
 */
void trackContentObject::movePosition( const midiTime & _pos )
{
	if( m_startPosition != _pos )
	{
		addJournalEntry( JournalEntry( Move, m_startPosition - _pos ) );
		m_startPosition = _pos;
		engine::getSong()->updateLength();
	}
	emit positionChanged();
}




/*! \brief Change the length of this trackContentObject
 *
 *  If the track content object's length has chaanged, update it.  We
 *  also add a journal entry for undo and update the display.
 *
 * \param _length The new length of the track content object.
 */
void trackContentObject::changeLength( const midiTime & _length )
{
	if( m_length != _length )
	{
		addJournalEntry( JournalEntry( Resize, m_length - _length ) );
		m_length = _length;
		engine::getSong()->updateLength();
	}
	emit lengthChanged();
}




/*! \brief Undo one journal entry of this trackContentObject
 *
 *  Restore the previous state of this track content object.  This will
 *  restore the position or the length of the track content object
 *  depending on what was changed.
 *
 * \param _je The journal entry to undo
 */
void trackContentObject::undoStep( JournalEntry & _je )
{
	saveJournallingState( false );
	switch( _je.actionID() )
	{
		case Move:
			movePosition( startPosition() + _je.data().toInt() );
			break;
		case Resize:
			changeLength( length() + _je.data().toInt() );
			break;
	}
	restoreJournallingState();
}




/*! \brief Redo one journal entry of this trackContentObject
 *
 *  Undoes one 'undo' of this track content object.
 *
 * \param _je The journal entry to redo
 */
void trackContentObject::redoStep( JournalEntry & _je )
{
	JournalEntry je( _je.actionID(), -_je.data().toInt() );
	undoStep( je );
}




/*! \brief Copy this trackContentObject to the clipboard.
 *
 *  Copies this track content object to the clipboard.
 */
void trackContentObject::copy()
{
	Clipboard::copy( this );
}




/*! \brief Pastes this trackContentObject into a track.
 *
 *  Pastes this track content object into a track.
 *
 * \param _je The journal entry to undo
 */
void trackContentObject::paste()
{
	if( Clipboard::getContent( nodeName() ) != NULL )
	{
		const midiTime pos = startPosition();
		restoreState( *( Clipboard::getContent( nodeName() ) ) );
		movePosition( pos );
	}
}




/*! \brief Mutes this trackContentObject
 *
 *  Restore the previous state of this track content object.  This will
 *  restore the position or the length of the track content object
 *  depending on what was changed.
 *
 * \param _je The journal entry to undo
 */
void trackContentObject::toggleMute()
{
	m_mutedModel.setValue( !m_mutedModel.value() );
	emit dataChanged();
}







// ===========================================================================
// trackContentObjectView
// ===========================================================================
/*! \brief Create a new trackContentObjectView
 *
 *  Creates a new track content object view for the given
 *  track content object in the given track view.
 *
 * \param _tco The track content object to be displayed
 * \param _tv  The track view that will contain the new object
 */
trackContentObjectView::trackContentObjectView( trackContentObject * _tco,
							trackView * _tv ) :
	selectableObject( _tv->getTrackContentWidget() ),
	ModelView( NULL, this ),
	m_tco( _tco ),
	m_trackView( _tv ),
	m_action( NoAction ),
	m_autoResize( false ),
	m_initialMouseX( 0 ),
	m_hint( NULL )
{
	if( s_textFloat == NULL )
	{
		s_textFloat = new textFloat;
		s_textFloat->setPixmap( embed::getIconPixmap( "clock" ) );
	}

	setAttribute( Qt::WA_OpaquePaintEvent, true );
	setAttribute( Qt::WA_DeleteOnClose, true );
	setFocusPolicy( Qt::StrongFocus );
	setCursor( QCursor( embed::getIconPixmap( "hand" ), 0, 0 ) );
	move( 0, 1 );
	show();

	setFixedHeight( _tv->getTrackContentWidget()->height() - 2 );
	setAcceptDrops( true );
	setMouseTracking( true );

	connect( m_tco, SIGNAL( lengthChanged() ),
			this, SLOT( updateLength() ) );
	connect( m_tco, SIGNAL( positionChanged() ),
			this, SLOT( updatePosition() ) );
	connect( m_tco, SIGNAL( destroyedTCO() ), this, SLOT( close() ) );
	setModel( m_tco );

	m_trackView->getTrackContentWidget()->addTCOView( this );
}




/*! \brief Destroy a trackContentObjectView
 *
 *  Destroys the given track content object view.
 *
 */
trackContentObjectView::~trackContentObjectView()
{
	delete m_hint;
	// we have to give our track-container the focus because otherwise the
	// op-buttons of our track-widgets could become focus and when the user
	// presses space for playing song, just one of these buttons is pressed
	// which results in unwanted effects
	m_trackView->getTrackContainerView()->setFocus();
}




/*! \brief Does this trackContentObjectView have a fixed TCO?
 *
 *  Returns whether the containing trackView has fixed
 *  TCOs.
 *
 * \todo What the hell is a TCO here - track content object?  And in
 *  what circumstance are they fixed?
 */
bool trackContentObjectView::fixedTCOs()
{
	return m_trackView->getTrackContainerView()->fixedTCOs();
}




/*! \brief Close a trackContentObjectView
 *
 *  Closes a track content object view by asking the track
 *  view to remove us and then asking the QWidget to close us.
 *
 * \return Boolean state of whether the QWidget was able to close.
 */
bool trackContentObjectView::close()
{
	m_trackView->getTrackContentWidget()->removeTCOView( this );
	return QWidget::close();
}




/*! \brief Removes a trackContentObjectView from its track view.
 *
 *  Like the close() method, this asks the track view to remove this
 *  track content object view.  However, the track content object is
 *  scheduled for later deletion rather than closed immediately.
 *
 */
void trackContentObjectView::remove()
{
	// delete ourself
	close();
	m_tco->deleteLater();
}




/*! \brief Cut this trackContentObjectView from its track to the clipboard.
 *
 *  Perform the 'cut' action of the clipboard - copies the track content
 *  object to the clipboard and then removes it from the track.
 */
void trackContentObjectView::cut()
{
	m_tco->copy();
	remove();
}




/*! \brief Updates a trackContentObjectView's length
 *
 *  If this track content object view has a fixed TCO, then we must
 *  keep the width of our parent.  Otherwise, calculate our width from
 *  the track content object's length in pixels adding in the border.
 *
 */
void trackContentObjectView::updateLength()
{
	if( fixedTCOs() )
	{
		setFixedWidth( parentWidget()->width() );
	}
	else
	{
		setFixedWidth(
		static_cast<int>( m_tco->length() * pixelsPerTact() /
					midiTime::ticksPerTact() ) +
						TCO_BORDER_WIDTH * 2-1 );
	}
	m_trackView->getTrackContainerView()->update();
}




/*! \brief Updates a trackContentObjectView's position.
 *
 *  Ask our track view to change our position.  Then make sure that the
 *  track view is updated in case this position has changed the track
 *  view's length.
 *
 */
void trackContentObjectView::updatePosition()
{
	m_trackView->getTrackContentWidget()->changePosition();
	// moving a TCO can result in change of song-length etc.,
	// therefore we update the track-container
	m_trackView->getTrackContainerView()->update();
}



/*! \brief Change the trackContentObjectView's display when something
 *  being dragged enters it.
 *
 *  We need to notify Qt to change our display if something being
 *  dragged has entered our 'airspace'.
 *
 * \param _dee The QDragEnterEvent to watch.
 */
void trackContentObjectView::dragEnterEvent( QDragEnterEvent * _dee )
{
	stringPairDrag::processDragEnterEvent( _dee, "tco_" +
				QString::number( m_tco->getTrack()->type() ) );
}




/*! \brief Handle something being dropped on this trackContentObjectView.
 *
 *  When something has been dropped on this trackContentObjectView, and
 *  it's a track content object, then use an instance of our mmp reader
 *  to take the xml of the track content object and turn it into something
 *  we can write over our current state.
 *
 * \param _de The QDropEvent to handle.
 */
void trackContentObjectView::dropEvent( QDropEvent * _de )
{
	QString type = stringPairDrag::decodeKey( _de );
	QString value = stringPairDrag::decodeValue( _de );
	if( type == ( "tco_" + QString::number( m_tco->getTrack()->type() ) ) )
	{
		// value contains our XML-data so simply create a
		// multimediaProject which does the rest for us...
		multimediaProject mmp( value.toUtf8() );
		// at least save position before getting to moved to somewhere
		// the user doesn't expect...
		midiTime pos = m_tco->startPosition();
		m_tco->restoreState( mmp.content().firstChild().toElement() );
 		m_tco->movePosition( pos );
		AutomationPattern::resolveAllIDs();
		_de->accept();
	}
}




/*! \brief Handle a dragged selection leaving our 'airspace'.
 *
 * \param _e The QEvent to watch.
 */
void trackContentObjectView::leaveEvent( QEvent * _e )
{
	while( QApplication::overrideCursor() != NULL )
	{
		QApplication::restoreOverrideCursor();
	}
	if( _e != NULL )
	{
		QWidget::leaveEvent( _e );
	}
}




/*! \brief Handle a mouse press on this trackContentObjectView.
 *
 *  Handles the various ways in which a trackContentObjectView can be
 *  used with a click of a mouse button.
 *
 *  * If our container supports rubber band selection then handle
 *    selection events.
 *  * or if shift-left button, add this object to the selection
 *  * or if ctrl-left button, start a drag-copy event
 *  * or if just plain left button, resize if we're resizeable
 *  * or if ctrl-middle button, mute the track content object
 *  * or if middle button, maybe delete the track content object.
 *
 * \param _me The QMouseEvent to handle.
 */
void trackContentObjectView::mousePressEvent( QMouseEvent * _me )
{
	if( m_trackView->getTrackContainerView()->allowRubberband() == true &&
					_me->button() == Qt::LeftButton )
	{
		// if rubberband is active, we can be selected
		if( !m_trackView->getTrackContainerView()->rubberBandActive() )
		{
			if( _me->modifiers() & Qt::ControlModifier )
			{
				setSelected( !isSelected() );
			}
			else if( isSelected() == true )
			{
				m_action = MoveSelection;
				m_initialMouseX = _me->x();
			}
		}
		else
		{
			selectableObject::mousePressEvent( _me );
		}
		return;
	}
	else if( _me->modifiers() & Qt::ShiftModifier )
	{
		// add/remove object to/from selection
		selectableObject::mousePressEvent( _me );
	}
	else if( _me->button() == Qt::LeftButton &&
			_me->modifiers() & Qt::ControlModifier )
	{
		// start drag-action
		multimediaProject mmp( multimediaProject::DragNDropData );
		m_tco->saveState( mmp, mmp.content() );
		QPixmap thumbnail = QPixmap::grabWidget( this ).scaled(
						128, 128,
						Qt::KeepAspectRatio,
						Qt::SmoothTransformation );
		new stringPairDrag( QString( "tco_%1" ).arg(
						m_tco->getTrack()->type() ),
					mmp.toString(), thumbnail, this );
	}
	else if( _me->button() == Qt::LeftButton &&
		/*	engine::mainWindow()->isShiftPressed() == false &&*/
							fixedTCOs() == false )
	{
		// move or resize
		m_tco->setJournalling( false );

		m_initialMouseX = _me->x();

		if( _me->x() < width() - RESIZE_GRIP_WIDTH )
		{
			m_action = Move;
			m_oldTime = m_tco->startPosition();
			QCursor c( Qt::SizeAllCursor );
			QApplication::setOverrideCursor( c );
			s_textFloat->setTitle( tr( "Current position" ) );
			delete m_hint;
			m_hint = textFloat::displayMessage( tr( "Hint" ),
					tr( "Press <Ctrl> and drag to make "
							"a copy." ),
					embed::getIconPixmap( "hint" ), 0 );
		}
		else if( m_autoResize == false )
		{
			m_action = Resize;
			m_oldTime = m_tco->length();
			QCursor c( Qt::SizeHorCursor );
			QApplication::setOverrideCursor( c );
			s_textFloat->setTitle( tr( "Current length" ) );
			delete m_hint;
			m_hint = textFloat::displayMessage( tr( "Hint" ),
					tr( "Press <Ctrl> for free "
							"resizing." ),
					embed::getIconPixmap( "hint" ), 0 );
		}
//		s_textFloat->reparent( this );
		// setup text-float as if TCO was already moved/resized
		mouseMoveEvent( _me );
		s_textFloat->show();
	}
	else if( _me->button() == Qt::MidButton )
	{
		if( _me->modifiers() & Qt::ControlModifier )
		{
			m_tco->toggleMute();
		}
		else if( fixedTCOs() == false )
		{
			remove();
		}
	}
}




/*! \brief Handle a mouse movement (drag) on this trackContentObjectView.
 *
 *  Handles the various ways in which a trackContentObjectView can be
 *  used with a mouse drag.
 *
 *  * If in move mode, move ourselves in the track,
 *  * or if in move-selection mode, move the entire selection,
 *  * or if in resize mode, resize ourselves,
 *  * otherwise ???
 *
 * \param _me The QMouseEvent to handle.
 * \todo what does the final else case do here?
 */
void trackContentObjectView::mouseMoveEvent( QMouseEvent * _me )
{
	if( _me->modifiers() & Qt::ControlModifier )
	{
		delete m_hint;
		m_hint = NULL;
	}

	const float ppt = m_trackView->getTrackContainerView()->pixelsPerTact();
	if( m_action == Move )
	{
		const int x = mapToParent( _me->pos() ).x() - m_initialMouseX;
		midiTime t = qMax( 0, (int)
			m_trackView->getTrackContainerView()->currentPosition()+
				static_cast<int>( x * midiTime::ticksPerTact() /
									ppt ) );
		if( ! ( _me->modifiers() & Qt::ControlModifier )
		   && _me->button() == Qt::NoButton )
		{
			t = t.toNearestTact();
		}
		m_tco->movePosition( t );
		m_trackView->getTrackContentWidget()->changePosition();
		s_textFloat->setText( QString( "%1:%2" ).
				arg( m_tco->startPosition().getTact() + 1 ).
				arg( m_tco->startPosition().getTicks() %
						midiTime::ticksPerTact() ) );
		s_textFloat->moveGlobal( this, QPoint( width() + 2,
		                                        height() + 2 ) );
	}
	else if( m_action == MoveSelection )
	{
		const int dx = _me->x() - m_initialMouseX;
		QVector<selectableObject *> so =
			m_trackView->getTrackContainerView()->selectedObjects();
		QVector<trackContentObject *> tcos;
		midiTime smallest_pos, t;
		// find out smallest position of all selected objects for not
		// moving an object before zero
		for( QVector<selectableObject *>::iterator it = so.begin();
							it != so.end(); ++it )
		{
			trackContentObjectView * tcov =
				dynamic_cast<trackContentObjectView *>( *it );
			if( tcov == NULL )
			{
				continue;
			}
			trackContentObject * tco = tcov->m_tco;
			tcos.push_back( tco );
			smallest_pos = qMin<int>( smallest_pos,
					(int)tco->startPosition() +
				static_cast<int>( dx *
					midiTime::ticksPerTact() / ppt ) );
		}
		for( QVector<trackContentObject *>::iterator it = tcos.begin();
							it != tcos.end(); ++it )
		{
			t = ( *it )->startPosition() +
				static_cast<int>( dx *midiTime::ticksPerTact() /
					 ppt )-smallest_pos;
			if( ! ( _me->modifiers() & Qt::AltModifier )
					   && _me->button() == Qt::NoButton )
			{
				t = t.toNearestTact();
			}
			( *it )->movePosition( t );
		}
	}
	else if( m_action == Resize )
	{
		midiTime t = qMax( midiTime::ticksPerTact(),
				static_cast<int>( _me->x() *
					midiTime::ticksPerTact() / ppt ) );
		if( ! ( _me->modifiers() & Qt::ControlModifier )
		   && _me->button() == Qt::NoButton )
		{
			t = t.toNearestTact();
		}
		m_tco->changeLength( t );
		s_textFloat->setText( tr( "%1:%2 (%3:%4 to %5:%6)" ).
				arg( m_tco->length().getTact() ).
				arg( m_tco->length().getTicks() %
						midiTime::ticksPerTact() ).
				arg( m_tco->startPosition().getTact() + 1 ).
				arg( m_tco->startPosition().getTicks() %
						midiTime::ticksPerTact() ).
				arg( m_tco->endPosition().getTact() + 1 ).
				arg( m_tco->endPosition().getTicks() %
						midiTime::ticksPerTact() ) );
		s_textFloat->moveGlobal( this, QPoint( width() + 2,
					height() + 2) );
	}
	else
	{
		if( _me->x() > width() - RESIZE_GRIP_WIDTH )
		{
			if( QApplication::overrideCursor() != NULL &&
				QApplication::overrideCursor()->shape() !=
							Qt::SizeHorCursor )
			{
				while( QApplication::overrideCursor() != NULL )
				{
					QApplication::restoreOverrideCursor();
				}
			}
			QCursor c( Qt::SizeHorCursor );
			QApplication::setOverrideCursor( c );
		}
		else
		{
			leaveEvent( NULL );
		}
	}
}




/*! \brief Handle a mouse release on this trackContentObjectView.
 *
 *  If we're in move or resize mode, journal the change as appropriate.
 *  Then tidy up.
 *
 * \param _me The QMouseEvent to handle.
 */
void trackContentObjectView::mouseReleaseEvent( QMouseEvent * _me )
{
	if( m_action == Move || m_action == Resize )
	{
		m_tco->setJournalling( true );
		m_tco->addJournalEntry( JournalEntry( m_action, m_oldTime -
			( ( m_action == Move ) ?
				m_tco->startPosition() : m_tco->length() ) ) );
	}
	m_action = NoAction;
	delete m_hint;
	m_hint = NULL;
	s_textFloat->hide();
	leaveEvent( NULL );
	selectableObject::mouseReleaseEvent( _me );
}




/*! \brief Set up the context menu for this trackContentObjectView.
 *
 *  Set up the various context menu events that can apply to a
 *  track content object view.
 *
 * \param _cme The QContextMenuEvent to add the actions to.
 */
void trackContentObjectView::contextMenuEvent( QContextMenuEvent * _cme )
{
	QMenu contextMenu( this );
	if( fixedTCOs() == false )
	{
		contextMenu.addAction( embed::getIconPixmap( "cancel" ),
					tr( "Delete (middle mousebutton)" ),
						this, SLOT( remove() ) );
		contextMenu.addSeparator();
		contextMenu.addAction( embed::getIconPixmap( "edit_cut" ),
					tr( "Cut" ), this, SLOT( cut() ) );
	}
	contextMenu.addAction( embed::getIconPixmap( "edit_copy" ),
					tr( "Copy" ), m_tco, SLOT( copy() ) );
	contextMenu.addAction( embed::getIconPixmap( "edit_paste" ),
					tr( "Paste" ), m_tco, SLOT( paste() ) );
	contextMenu.addSeparator();
	contextMenu.addAction( embed::getIconPixmap( "muted" ),
				tr( "Mute/unmute (<Ctrl> + middle click)" ),
						m_tco, SLOT( toggleMute() ) );
	constructContextMenu( &contextMenu );

	contextMenu.exec( QCursor::pos() );
}





/*! \brief How many pixels a tact (bar) takes for this trackContentObjectView.
 *
 * \return the number of pixels per tact (bar).
 */
float trackContentObjectView::pixelsPerTact()
{
	return m_trackView->getTrackContainerView()->pixelsPerTact();
}




/*! \brief Set whether this trackContentObjectView can resize.
 *
 * \param _e The boolean state of whether this track content object view
 *  is allowed to resize.
 */
void trackContentObjectView::setAutoResizeEnabled( bool _e )
{
	m_autoResize = _e;
}




// ===========================================================================
// trackContentWidget
// ===========================================================================
/*! \brief Create a new trackContentWidget
 *
 *  Creates a new track content widget for the given track.
 *  The content widget comprises the 'grip bar' and the 'tools' button
 *  for the track's context menu.
 *
 * \param _track The parent track.
 */
trackContentWidget::trackContentWidget( trackView * _parent ) :
	QWidget( _parent ),
	m_trackView( _parent )
{
	setAcceptDrops( true );

	connect( _parent->getTrackContainerView(),
			SIGNAL( positionChanged( const midiTime & ) ),
			this, SLOT( changePosition( const midiTime & ) ) );

	updateBackground();
}




/*! \brief Destroy this trackContentWidget
 *
 *  Destroys the trackContentWidget.
 */
trackContentWidget::~trackContentWidget()
{
}




void trackContentWidget::updateBackground()
{
	const int tactsPerBar = 4;
	const trackContainerView * tcv = m_trackView->getTrackContainerView();

	// Assume even-pixels-per-tact. Makes sense, should be like this anyways
	int ppt = static_cast<int>( tcv->pixelsPerTact() );

    int w = ppt * tactsPerBar;
    int h = height();
    m_background = QPixmap( w * 2, height() );
    QPainter pmp( &m_background );

    QLinearGradient grad( 0, 1, 0, h-2 );
    pmp.fillRect( 0, 0, 1, h, QColor( 96, 96, 96 ) );
    pmp.fillRect( 1, 0, w+1, h, QColor( 128, 128, 128 ) );
    grad.setColorAt( 0.0, QColor( 64, 64, 64 ) );
    grad.setColorAt( 0.3, QColor( 128, 128, 128 ) );
    grad.setColorAt( 0.5, QColor( 128, 128, 128 ) );
    grad.setColorAt( 0.95, QColor( 160, 160, 160 ) );
    pmp.fillRect( 0, 1, w, h-2, grad );

    QLinearGradient grad2( 0,1, 0, h-2 );
    pmp.fillRect( w+1, 0, w , h, QColor( 96, 96, 96 ) );
    grad2.setColorAt( 0.0, QColor( 48, 48, 48 ) );
    grad2.setColorAt( 0.3, QColor( 96, 96, 96 ) );
    grad2.setColorAt( 0.5, QColor( 96, 96, 96 ) );
    grad2.setColorAt( 0.95, QColor( 120, 120, 120 ) );
    pmp.fillRect( w, 1, w , h-2, grad2 );

    // draw vertical lines
    pmp.setPen( QPen( QColor( 0, 0, 0, 112 ), 1 ) );
    for( float x = 0.5; x < w * 2; x += ppt )
    {
        pmp.drawLine( QLineF( x, 1.0, x, h-2.0 ) );
    }
    pmp.drawLine( 0, 1, w*2, 1 );

    pmp.setPen( QPen( QColor( 255, 255, 255, 32 ), 1 ) );
    for( float x = 1.5; x < w * 2; x += ppt )
    {
        pmp.drawLine( QLineF( x, 1.0, x, h-2.0 ) );
    }
    pmp.drawLine( 0, h-2, w*2, h-2 );

    pmp.end();

    // Force redraw
	update();
}




/*! \brief Adds a trackContentObjectView to this widget.
 *
 *  Adds a(nother) trackContentObjectView to our list of views.  We also
 *  check that our position is up-to-date.
 *
 * \param _tcov The trackContentObjectView to add.
 */
void trackContentWidget::addTCOView( trackContentObjectView * _tcov )
{
	trackContentObject * tco = _tcov->getTrackContentObject();
/*	QMap<QString, QVariant> map;
	map["id"] = tco->id();
	addJournalEntry( JournalEntry( AddTrackContentObject, map ) );*/

	m_tcoViews.push_back( _tcov );

	tco->saveJournallingState( false );
	changePosition();
	tco->restoreJournallingState();
}




/*! \brief Removes the given trackContentObjectView to this widget.
 *
 *  Removes the given trackContentObjectView from our list of views.
 *
 * \param _tcov The trackContentObjectView to add.
 */
void trackContentWidget::removeTCOView( trackContentObjectView * _tcov )
{
	tcoViewVector::iterator it = qFind( m_tcoViews.begin(),
						m_tcoViews.end(),
						_tcov );
	if( it != m_tcoViews.end() )
	{
/*		QMap<QString, QVariant> map;
		multimediaProject mmp( multimediaProject::JournalData );
		_tcov->getTrackContentObject()->saveState( mmp, mmp.content() );
		map["id"] = _tcov->getTrackContentObject()->id();
		map["state"] = mmp.toString();
		addJournalEntry( JournalEntry( RemoveTrackContentObject,
								map ) );*/

		m_tcoViews.erase( it );
		engine::getSong()->setModified();
	}
}




/*! \brief Update ourselves by updating all the tCOViews attached.
 *
 */
void trackContentWidget::update()
{
	for( tcoViewVector::iterator it = m_tcoViews.begin();
				it != m_tcoViews.end(); ++it )
	{
		( *it )->setFixedHeight( height() - 2 );
		( *it )->update();
	}
	QWidget::update();
}




// resposible for moving track-content-widgets to appropriate position after
// change of visible viewport
/*! \brief Move the trackContentWidget to a new place in time
 *
 * \param _new_pos The MIDI time to move to.
 */
void trackContentWidget::changePosition( const midiTime & _new_pos )
{
	if( m_trackView->getTrackContainerView() == engine::getBBEditor() )
	{
		const int cur_bb = engine::getBBTrackContainer()->currentBB();
		setUpdatesEnabled( false );

		// first show TCO for current BB...
		for( tcoViewVector::iterator it = m_tcoViews.begin();
						it != m_tcoViews.end(); ++it )
		{
		if( ( *it )->getTrackContentObject()->
                            startPosition().getTact() == cur_bb )
			{
				( *it )->move( 0, ( *it )->y() );
				( *it )->raise();
				( *it )->show();
			}
			else
			{
				( *it )->lower();
			}
		}
		// ...then hide others to avoid flickering
		for( tcoViewVector::iterator it = m_tcoViews.begin();
					it != m_tcoViews.end(); ++it )
		{
			if( ( *it )->getTrackContentObject()->
	                            startPosition().getTact() != cur_bb )
			{
				( *it )->hide();
			}
		}
		setUpdatesEnabled( true );
		return;
	}

	midiTime pos = _new_pos;
	if( pos < 0 )
	{
		pos = m_trackView->getTrackContainerView()->currentPosition();
	}

	const int begin = pos;
	const int end = endPosition( pos );
	const float ppt = m_trackView->getTrackContainerView()->pixelsPerTact();

	setUpdatesEnabled( false );
	for( tcoViewVector::iterator it = m_tcoViews.begin();
						it != m_tcoViews.end(); ++it )
	{
		trackContentObjectView * tcov = *it;
		trackContentObject * tco = tcov->getTrackContentObject();

		tco->changeLength( tco->length() );

		const int ts = tco->startPosition();
		const int te = tco->endPosition()-3;
		if( ( ts >= begin && ts <= end ) ||
			( te >= begin && te <= end ) ||
			( ts <= begin && te >= end ) )
		{
			tcov->move( static_cast<int>( ( ts - begin ) * ppt /
						midiTime::ticksPerTact() ),
								tcov->y() );
			if( !tcov->isVisible() )
			{
				tcov->show();
			}
		}
		else
		{
			tcov->move( -tcov->width()-10, tcov->y() );
		}
	}
	setUpdatesEnabled( true );

	// redraw background
//	update();
}




/*! \brief Respond to a drag enter event on the trackContentWidget
 *
 * \param _dee the Drag Enter Event to respond to
 */
void trackContentWidget::dragEnterEvent( QDragEnterEvent * _dee )
{
	stringPairDrag::processDragEnterEvent( _dee, "tco_" +
					QString::number( getTrack()->type() ) );
}




/*! \brief Respond to a drop event on the trackContentWidget
 *
 * \param _de the Drop Event to respond to
 */
void trackContentWidget::dropEvent( QDropEvent * _de )
{
	QString type = stringPairDrag::decodeKey( _de );
	QString value = stringPairDrag::decodeValue( _de );
	if( type == ( "tco_" + QString::number( getTrack()->type() ) ) &&
		m_trackView->getTrackContainerView()->fixedTCOs() == false )
	{
		const midiTime pos = getPosition( _de->pos().x()
							).toNearestTact();
		trackContentObject * tco = getTrack()->createTCO( pos );

		// value contains our XML-data so simply create a
		// multimediaProject which does the rest for us...
		multimediaProject mmp( value.toUtf8() );
		// at least save position before getting moved to somewhere
		// the user doesn't expect...
		tco->restoreState( mmp.content().firstChild().toElement() );
		tco->movePosition( pos );

		AutomationPattern::resolveAllIDs();

		_de->accept();
	}
}






/*! \brief Respond to a mouse press on the trackContentWidget
 *
 * \param _me the mouse press event to respond to
 */
void trackContentWidget::mousePressEvent( QMouseEvent * _me )
{
	if( m_trackView->getTrackContainerView()->allowRubberband() == true )
	{
		QWidget::mousePressEvent( _me );
	}
	else if( _me->modifiers() & Qt::ShiftModifier )
	{
		QWidget::mousePressEvent( _me );
	}
	else if( _me->button() == Qt::LeftButton &&
			!m_trackView->getTrackContainerView()->fixedTCOs() )
	{
		const midiTime pos = getPosition( _me->x() ).getTact() *
						midiTime::ticksPerTact();
		trackContentObject * tco = getTrack()->createTCO( pos );

		tco->saveJournallingState( false );
		tco->movePosition( pos );
		tco->restoreJournallingState();

	}
}




/*! \brief Repaint the trackContentWidget on command
 *
 * \param _pe the Paint Event to respond to
 */
void trackContentWidget::paintEvent( QPaintEvent * _pe )
{
	// Assume even-pixels-per-tact. Makes sense, should be like this anyways
	const trackContainerView * tcv = m_trackView->getTrackContainerView();
	int ppt = static_cast<int>( tcv->pixelsPerTact() );
	QPainter p( this );
	// Don't draw background on BB-Editor
	if( m_trackView->getTrackContainerView() != engine::getBBEditor() )
	{
		p.drawTiledPixmap( rect(), m_background, QPoint(
				tcv->currentPosition().getTact() * ppt, 0 ) );
	}
}




/*! \brief Updates the background tile pixmap on size changes.
 *
 * \param resizeEvent the resize event to pass to base class
 */
void trackContentWidget::resizeEvent( QResizeEvent * resizeEvent )
{
	updateBackground();
	QWidget::resizeEvent( resizeEvent );
}




/*! \brief Undo an action on the trackContentWidget
 *
 * \param _je the details of the edit journal
 */
void trackContentWidget::undoStep( JournalEntry & _je )
{
	saveJournallingState( false );
	switch( _je.actionID() )
	{
		case AddTrackContentObject:
		{
			QMap<QString, QVariant> map = _je.data().toMap();
			trackContentObject * tco =
				dynamic_cast<trackContentObject *>(
			engine::projectJournal()->journallingObject( map["id"].toInt() ) );
			multimediaProject mmp( multimediaProject::JournalData );
			tco->saveState( mmp, mmp.content() );
			map["state"] = mmp.toString();
			_je.data() = map;
			tco->deleteLater();
			break;
		}

		case RemoveTrackContentObject:
		{
			trackContentObject * tco = getTrack()->createTCO( midiTime( 0 ) );
			multimediaProject mmp(
				_je.data().toMap()["state"].
					toString().toUtf8() );
			tco->restoreState( mmp.content().firstChild().toElement() );
			break;
		}
	}
	restoreJournallingState();
}




/*! \brief Redo an action of the trackContentWidget
 *
 * \param _je the entry in the edit journal to redo.
 */
void trackContentWidget::redoStep( JournalEntry & _je )
{
	switch( _je.actionID() )
	{
		case AddTrackContentObject:
		case RemoveTrackContentObject:
			_je.actionID() = ( _je.actionID() ==
						AddTrackContentObject ) ?
				RemoveTrackContentObject :
						AddTrackContentObject;
			undoStep( _je );
			_je.actionID() = ( _je.actionID() ==
						AddTrackContentObject ) ?
				RemoveTrackContentObject :
						AddTrackContentObject;
			break;
	}
}




/*! \brief Return the track shown by the trackContentWidget
 *
 */
track * trackContentWidget::getTrack()
{
	return m_trackView->getTrack();
}




/*! \brief Return the position of the trackContentWidget in Tacts.
 *
 * \param _mouse_x the mouse's current X position in pixels.
 */
midiTime trackContentWidget::getPosition( int _mouse_x )
{
	return midiTime( m_trackView->getTrackContainerView()->
					currentPosition() + _mouse_x *
						midiTime::ticksPerTact() /
			static_cast<int>( m_trackView->
				getTrackContainerView()->pixelsPerTact() ) );
}



/*! \brief Return the end position of the trackContentWidget in Tacts.
 *
 * \param _pos_start the starting position of the Widget (from getPosition())
 */
midiTime trackContentWidget::endPosition( const midiTime & _pos_start )
{
	const float ppt = m_trackView->getTrackContainerView()->pixelsPerTact();
	const int w = width();
	return _pos_start + static_cast<int>( w * midiTime::ticksPerTact() / ppt );
}






// ===========================================================================
// trackOperationsWidget
// ===========================================================================


QPixmap * trackOperationsWidget::s_grip = NULL;     /*!< grip pixmap */


/*! \brief Create a new trackOperationsWidget
 *
 * The trackOperationsWidget is the grip and the mute button of a track.
 *
 * \param _parent the trackView to contain this widget
 */
trackOperationsWidget::trackOperationsWidget( trackView * _parent ) :
	QWidget( _parent ),             /*!< The parent widget */
	m_trackView( _parent )          /*!< The parent track view */
{
	if( s_grip == NULL )
	{
		s_grip = new QPixmap( embed::getIconPixmap(
							"track_op_grip" ) );
	}

	toolTip::add( this, tr( "Press <Ctrl> while clicking on move-grip "
				"to begin a new drag'n'drop-action." ) );

	QMenu * to_menu = new QMenu( this );
	to_menu->setFont( pointSize<9>( to_menu->font() ) );
	connect( to_menu, SIGNAL( aboutToShow() ), this, SLOT( updateMenu() ) );


	setObjectName( "automationEnabled" );


	m_trackOps = new QPushButton( this );
	m_trackOps->move( 12, 1 );
	m_trackOps->setFocusPolicy( Qt::NoFocus );
	m_trackOps->setMenu( to_menu );
	toolTip::add( m_trackOps, tr( "Actions for this track" ) );


	m_muteBtn = new pixmapButton( this, tr( "Mute" ) );
	m_muteBtn->setActiveGraphic( embed::getIconPixmap( "led_off" ) );
	m_muteBtn->setInactiveGraphic( embed::getIconPixmap( "led_green" ) );
	m_muteBtn->setCheckable( true );

	m_soloBtn = new pixmapButton( this, tr( "Solo" ) );
	m_soloBtn->setActiveGraphic( embed::getIconPixmap( "led_red" ) );
	m_soloBtn->setInactiveGraphic( embed::getIconPixmap( "led_off" ) );
	m_soloBtn->setCheckable( true );

	if( configManager::inst()->value( "ui",
					  "compacttrackbuttons" ).toInt() )
	{
		m_muteBtn->move( 46, 0 );
		m_soloBtn->move( 46, 16 );
	}
	else
	{
		m_muteBtn->move( 46, 8 );
		m_soloBtn->move( 62, 8 );
	}

	m_muteBtn->show();
	toolTip::add( m_muteBtn, tr( "Mute this track" ) );

	m_soloBtn->show();
	toolTip::add( m_soloBtn, tr( "Solo" ) );

	connect( this, SIGNAL( trackRemovalScheduled( trackView * ) ),
			m_trackView->getTrackContainerView(),
				SLOT( deleteTrackView( trackView * ) ),
							Qt::QueuedConnection );
}




/*! \brief Destroy an existing trackOperationsWidget
 *
 */
trackOperationsWidget::~trackOperationsWidget()
{
}




/*! \brief Respond to trackOperationsWidget mouse events
 *
 *  If it's the left mouse button, and Ctrl is held down, and we're
 *  not a Beat+Bassline Editor track, then start a new drag event to
 *  copy this track.
 *
 *  Otherwise, ignore all other events.
 *
 *  \param _me The mouse event to respond to.
 */
void trackOperationsWidget::mousePressEvent( QMouseEvent * _me )
{
	if( _me->button() == Qt::LeftButton &&
		_me->modifiers() & Qt::ControlModifier &&
			m_trackView->getTrack()->type() != track::BBTrack )
	{
		multimediaProject mmp( multimediaProject::DragNDropData );
		m_trackView->getTrack()->saveState( mmp, mmp.content() );
		new stringPairDrag( QString( "track_%1" ).arg(
					m_trackView->getTrack()->type() ),
			mmp.toString(), QPixmap::grabWidget(
				m_trackView->getTrackSettingsWidget() ),
									this );
	}
	else if( _me->button() == Qt::LeftButton )
	{
		// track-widget (parent-widget) initiates track-move
		_me->ignore();
	}
}





/*! \brief Repaint the trackOperationsWidget
 *
 *  If we're not moving, and in the Beat+Bassline Editor, then turn
 *  automation on or off depending on its previous state and show
 *  ourselves.
 *
 *  Otherwise, hide ourselves.
 *
 *  \todo Flesh this out a bit - is it correct?
 *  \param _pe The paint event to respond to
 */
void trackOperationsWidget::paintEvent( QPaintEvent * _pe )
{
	QPainter p( this );
	p.fillRect( rect(), palette().brush(QPalette::Background) );

	if( m_trackView->isMovingTrack() == false )
	{
		p.drawPixmap( 2, 2, *s_grip );
		m_trackOps->show();
		m_muteBtn->show();
	}
	else
	{
		m_trackOps->hide();
		m_muteBtn->hide();
	}
}





/*! \brief Clone this track
 *
 */
void trackOperationsWidget::cloneTrack()
{
	engine::getMixer()->lock();
	m_trackView->getTrack()->clone();
	engine::getMixer()->unlock();
}




/*! \brief Remove this track from the track list
 *
 */
void trackOperationsWidget::removeTrack()
{
	emit trackRemovalScheduled( m_trackView );
}




/*! \brief Update the trackOperationsWidget context menu
 *
 *  For all track types, we have the Clone and Remove options.
 *  For instrument-tracks we also offer the MIDI-control-menu
 */
void trackOperationsWidget::updateMenu()
{
	QMenu * to_menu = m_trackOps->menu();
	to_menu->clear();
	to_menu->addAction( embed::getIconPixmap( "edit_copy", 16, 16 ),
						tr( "Clone this track" ),
						this, SLOT( cloneTrack() ) );
	to_menu->addAction( embed::getIconPixmap( "cancel", 16, 16 ),
						tr( "Remove this track" ),
						this, SLOT( removeTrack() ) );

	if( dynamic_cast<InstrumentTrackView *>( m_trackView ) )
	{
		to_menu->addSeparator();
		to_menu->addMenu( dynamic_cast<InstrumentTrackView *>(
						m_trackView )->midiMenu() );
	}
}





// ===========================================================================
// track
// ===========================================================================

/*! \brief Create a new (empty) track object
 *
 *  The track object is the whole track, linking its contents, its
 *  automation, name, type, and so forth.
 *
 * \param _type The type of track (Song Editor or Beat+Bassline Editor)
 * \param _tc The track Container object to encapsulate in this track.
 *
 * \todo check the definitions of all the properties - are they OK?
 */
track::track( TrackTypes _type, trackContainer * _tc ) :
	Model( _tc ),                   /*!< The track Model */
	m_trackContainer( _tc ),        /*!< The track container object */
	m_type( _type ),                /*!< The track type */
	m_name(),                       /*!< The track's name */
	m_mutedModel( false, this, tr( "Muted" ) ),
					 /*!< For controlling track muting */
	m_soloModel( false, this, tr( "Solo" ) ),
					/*!< For controlling track soloing */
	m_simpleSerializingMode( false ),
	m_trackContentObjects()         /*!< The track content objects (segments) */
{
	m_trackContainer->addTrack( this );
	m_height = -1;
}




/*! \brief Destroy this track
 *
 *  If the track container is a Beat+Bassline container, step through
 *  its list of tracks and remove us.
 *
 *  Then delete the trackContentObject's contents, remove this track from
 *  the track container.
 *
 *  Finally step through this track's automation and forget all of them.
 */
track::~track()
{
	emit destroyedTrack();

	while( !m_trackContentObjects.isEmpty() )
	{
		delete m_trackContentObjects.last();
	}

	m_trackContainer->removeTrack( this );
}




/*! \brief Create a track based on the given track type and container.
 *
 *  \param _tt The type of track to create
 *  \param _tc The track container to attach to
 */
track * track::create( TrackTypes _tt, trackContainer * _tc )
{
	track * t = NULL;

	switch( _tt )
	{
		case InstrumentTrack: t = new ::InstrumentTrack( _tc ); break;
		case BBTrack: t = new bbTrack( _tc ); break;
		case SampleTrack: t = new sampleTrack( _tc ); break;
//		case EVENT_TRACK:
//		case VIDEO_TRACK:
		case AutomationTrack: t = new ::AutomationTrack( _tc ); break;
		case HiddenAutomationTrack:
						t = new ::AutomationTrack( _tc, true ); break;
		default: break;
	}

	_tc->updateAfterTrackAdd();

	return t;
}




/*! \brief Create a track inside trackContainer from track type in a QDomElement and restore state from XML
 *
 *  \param _this The QDomElement containing the type of track to create
 *  \param _tc The track container to attach to
 */
track * track::create( const QDomElement & _this, trackContainer * _tc )
{
	track * t = create(
		static_cast<TrackTypes>( _this.attribute( "type" ).toInt() ),
									_tc );
	if( t != NULL )
	{
		t->restoreState( _this );
	}
	return t;
}




/*! \brief Clone a track from this track
 *
 */
void track::clone()
{
	QDomDocument doc;
	QDomElement parent = doc.createElement( "clone" );
	saveState( doc, parent );
	create( parent.firstChild().toElement(), m_trackContainer );
}






/*! \brief Save this track's settings to file
 *
 *  We save the track type and its muted state, then append the track-
 *  specific settings.  Then we iterate through the trackContentObjects
 *  and save all their states in turn.
 *
 *  \param _doc The QDomDocument to use to save
 *  \param _this The The QDomElement to save into
 *  \todo Does this accurately describe the parameters?  I think not!?
 *  \todo Save the track height
 */
void track::saveSettings( QDomDocument & _doc, QDomElement & _this )
{
	if( !m_simpleSerializingMode )
	{
		_this.setTagName( "track" );
	}
	_this.setAttribute( "type", type() );
	_this.setAttribute( "name", name() );
	_this.setAttribute( "muted", isMuted() );
	if( m_height >= MINIMAL_TRACK_HEIGHT )
	{
		_this.setAttribute( "height", m_height );
	}

	QDomElement ts_de = _doc.createElement( nodeName() );
	// let actual track (InstrumentTrack, bbTrack, sampleTrack etc.) save
	// its settings
	_this.appendChild( ts_de );
	saveTrackSpecificSettings( _doc, ts_de );

	if( m_simpleSerializingMode )
	{
		m_simpleSerializingMode = false;
		return;
	}

	// now save settings of all TCO's
	for( tcoVector::const_iterator it = m_trackContentObjects.begin();
				it != m_trackContentObjects.end(); ++it )
	{
		( *it )->saveState( _doc, _this );
	}
}




/*! \brief Load the settings from a file
 *
 *  We load the track's type and muted state, then clear out our
 *  current trackContentObject.
 *
 *  Then we step through the QDomElement's children and load the
 *  track-specific settings and trackContentObjects states from it
 *  one at a time.
 *
 *  \param _this the QDomElement to load track settings from
 *  \todo Load the track height.
 */
void track::loadSettings( const QDomElement & _this )
{
	if( _this.attribute( "type" ).toInt() != type() )
	{
		qWarning( "Current track-type does not match track-type of "
							"settings-node!\n" );
	}

	setName( _this.hasAttribute( "name" ) ? _this.attribute( "name" ) :
			_this.firstChild().toElement().attribute( "name" ) );

	setMuted( _this.attribute( "muted" ).toInt() );

	if( m_simpleSerializingMode )
	{
		QDomNode node = _this.firstChild();
		while( !node.isNull() )
		{
			if( node.isElement() && node.nodeName() == nodeName() )
			{
				loadTrackSpecificSettings( node.toElement() );
				break;
			}
			node = node.nextSibling();
		}
		m_simpleSerializingMode = false;
		return;
	}

	while( !m_trackContentObjects.empty() )
	{
		delete m_trackContentObjects.front();
//		m_trackContentObjects.erase( m_trackContentObjects.begin() );
	}

	QDomNode node = _this.firstChild();
	while( !node.isNull() )
	{
		if( node.isElement() )
		{
			if( node.nodeName() == nodeName() )
			{
				loadTrackSpecificSettings( node.toElement() );
			}
			else if(
			!node.toElement().attribute( "metadata" ).toInt() )
			{
				trackContentObject * tco = createTCO(
								midiTime( 0 ) );
				tco->restoreState( node.toElement() );
				saveJournallingState( false );
				restoreJournallingState();
			}
		}
		node = node.nextSibling();
	}

	if( _this.attribute( "height" ).toInt() >= MINIMAL_TRACK_HEIGHT &&
		_this.attribute( "height" ).toInt() <= DEFAULT_TRACK_HEIGHT )	// workaround for #3585927, tobydox/2012-11-11
	{
		m_height = _this.attribute( "height" ).toInt();
	}
}




/*! \brief Add another trackContentObject into this track
 *
 *  \param _tco The trackContentObject to attach to this track.
 */
trackContentObject * track::addTCO( trackContentObject * _tco )
{
	m_trackContentObjects.push_back( _tco );

	emit trackContentObjectAdded( _tco );

	return _tco;		// just for convenience
}




/*! \brief Remove a given trackContentObject from this track
 *
 *  \param _tco The trackContentObject to remove from this track.
 */
void track::removeTCO( trackContentObject * _tco )
{
	tcoVector::iterator it = qFind( m_trackContentObjects.begin(),
					m_trackContentObjects.end(),
					_tco );
	if( it != m_trackContentObjects.end() )
	{
		m_trackContentObjects.erase( it );
		if( engine::getSong() )
		{
			engine::getSong()->updateLength();
			engine::getSong()->setModified();
		}
	}
}




/*! \brief Return the number of trackContentObjects we contain
 *
 *  \return the number of trackContentObjects we currently contain.
 */
int track::numOfTCOs()
{
	return m_trackContentObjects.size();
}




/*! \brief Get a trackContentObject by number
 *
 *  If the TCO number is less than our TCO array size then fetch that
 *  numbered object from the array.  Otherwise we warn the user that
 *  we've somehow requested a TCO that is too large, and create a new
 *  TCO for them.
 *  \param _tco_number The number of the trackContentObject to fetch.
 *  \return the given trackContentObject or a new one if out of range.
 *  \todo reject TCO numbers less than zero.
 *  \todo if we create a TCO here, should we somehow attach it to the
 *     track?
 */
trackContentObject * track::getTCO( int _tco_num )
{
	if( _tco_num < m_trackContentObjects.size() )
	{
		return m_trackContentObjects[_tco_num];
	}
	printf( "called track::getTCO( %d ), "
			"but TCO %d doesn't exist\n", _tco_num, _tco_num );
	return createTCO( _tco_num * midiTime::ticksPerTact() );
	
}




/*! \brief Determine the given trackContentObject's number in our array.
 *
 *  \param _tco The trackContentObject to search for.
 *  \return its number in our array.
 */
int track::getTCONum( trackContentObject * _tco )
{
//	for( int i = 0; i < getTrackContentWidget()->numOfTCOs(); ++i )
	tcoVector::iterator it = qFind( m_trackContentObjects.begin(),
					m_trackContentObjects.end(),
					_tco );
	if( it != m_trackContentObjects.end() )
	{
/*		if( getTCO( i ) == _tco )
		{
			return i;
		}*/
		return it - m_trackContentObjects.begin();
	}
	qWarning( "track::getTCONum(...) -> _tco not found!\n" );
	return 0;
}




/*! \brief Retrieve a list of trackContentObjects that fall within a period.
 *
 *  Here we're interested in a range of trackContentObjects that fall
 *  completely within a given time period - their start must be no earlier
 *  than the given start time and their end must be no later than the given
 *  end time.
 *
 *  We return the TCOs we find in order by time, earliest TCOs first.
 *
 *  \param _tco_c The list to contain the found trackContentObjects.
 *  \param _start The MIDI start time of the range.
 *  \param _end   The MIDI endi time of the range.
 */
void track::getTCOsInRange( tcoVector & _tco_v, const midiTime & _start,
							const midiTime & _end )
{
	for( tcoVector::iterator it_o = m_trackContentObjects.begin();
				it_o != m_trackContentObjects.end(); ++it_o )
	{
		trackContentObject * tco = ( *it_o );
		int s = tco->startPosition();
		int e = tco->endPosition();
		if( ( s <= _end ) && ( e >= _start ) )
		{
			// ok, TCO is posated within given range
			// now let's search according position for TCO in list
			// 	-> list is ordered by TCO's position afterwards
			bool inserted = false;
			for( tcoVector::iterator it = _tco_v.begin();
						it != _tco_v.end(); ++it )
			{
				if( ( *it )->startPosition() >= s )
				{
					_tco_v.insert( it, tco );
					inserted = true;
					break;
				}
			}
			if( inserted == false )
			{
				// no TCOs found posated behind current TCO...
				_tco_v.push_back( tco );
			}
		}
	}
}




/*! \brief Swap the position of two trackContentObjects.
 *
 *  First, we arrange to swap the positions of the two TCOs in the
 *  trackContentObjects list.  Then we swap their start times as well.
 *
 *  \param _tco_num1 The first trackContentObject to swap.
 *  \param _tco_num2 The second trackContentObject to swap.
 */
void track::swapPositionOfTCOs( int _tco_num1, int _tco_num2 )
{
	qSwap( m_trackContentObjects[_tco_num1],
					m_trackContentObjects[_tco_num2] );

	const midiTime pos = m_trackContentObjects[_tco_num1]->startPosition();

	m_trackContentObjects[_tco_num1]->movePosition(
			m_trackContentObjects[_tco_num2]->startPosition() );
	m_trackContentObjects[_tco_num2]->movePosition( pos );
}




/*! \brief Move all the trackContentObjects after a certain time later by one bar.
 *
 *  \param _pos The time at which we want to insert the bar.
 *  \todo if we stepped through this list last to first, and the list was
 *    in ascending order by TCO time, once we hit a TCO that was earlier
 *    than the insert time, we could fall out of the loop early.
 */
void track::insertTact( const midiTime & _pos )
{
	// we'll increase the position of every TCO, positioned behind _pos, by
	// one tact
	for( tcoVector::iterator it = m_trackContentObjects.begin();
				it != m_trackContentObjects.end(); ++it )
	{
		if( ( *it )->startPosition() >= _pos )
		{
			( *it )->movePosition( (*it)->startPosition() +
						midiTime::ticksPerTact() );
		}
	}
}




/*! \brief Move all the trackContentObjects after a certain time earlier by one bar.
 *
 *  \param _pos The time at which we want to remove the bar.
 */
void track::removeTact( const midiTime & _pos )
{
	// we'll decrease the position of every TCO, positioned behind _pos, by
	// one tact
	for( tcoVector::iterator it = m_trackContentObjects.begin();
				it != m_trackContentObjects.end(); ++it )
	{
		if( ( *it )->startPosition() >= _pos )
		{
			( *it )->movePosition( qMax( ( *it )->startPosition() -
						midiTime::ticksPerTact(), 0 ) );
		}
	}
}




/*! \brief Return the length of the entire track in bars
 *
 *  We step through our list of TCOs and determine their end position,
 *  keeping track of the latest time found in ticks.  Then we return
 *  that in bars by dividing by the number of ticks per bar.
 */
tact_t track::length() const
{
	// find last end-position
	tick_t last = 0;
	for( tcoVector::const_iterator it = m_trackContentObjects.begin();
				it != m_trackContentObjects.end(); ++it )
	{
		const tick_t cur = ( *it )->endPosition();
		if( cur > last )
		{
			last = cur;
		}
	}

	return last / midiTime::ticksPerTact();
}



/*! \brief Invert the track's solo state.
 *
 *  We have to go through all the tracks determining if any other track
 *  is already soloed.  Then we have to save the mute state of all tracks,
 *  and set our mute state to on and all the others to off.
 */
void track::toggleSolo()
{
	const trackContainer::trackList & tl = m_trackContainer->tracks();

	bool solo_before = false;
	for( trackContainer::trackList::const_iterator it = tl.begin();
							it != tl.end(); ++it )
	{
		if( *it != this )
		{
			if( ( *it )->m_soloModel.value() )
			{
				solo_before = true;
				break;
			}
		}
	}

	const bool solo = m_soloModel.value();
	for( trackContainer::trackList::const_iterator it = tl.begin();
							it != tl.end(); ++it )
	{
		if( solo )
		{
			// save mute-state in case no track was solo before
			if( !solo_before )
			{
				( *it )->m_mutedBeforeSolo = ( *it )->isMuted();
			}
			( *it )->setMuted( *it == this ? false : true );
			if( *it != this )
			{
				( *it )->m_soloModel.setValue( false );
			}
		}
		else if( !solo_before )
		{
			( *it )->setMuted( ( *it )->m_mutedBeforeSolo );
		}
	}
}






// ===========================================================================
// trackView
// ===========================================================================

/*! \brief Create a new track View.
 *
 *  The track View is handles the actual display of the track, including
 *  displaying its various widgets and the track segments.
 *
 *  \param _track The track to display.
 *  \param _tcv The track Container View for us to be displayed in.
 *  \todo Is my description of these properties correct?
 */
trackView::trackView( track * _track, trackContainerView * _tcv ) :
	QWidget( _tcv->contentWidget() ),   /*!< The Track Container View's content widget. */
	ModelView( NULL, this ),            /*!< The model view of this track */
	m_track( _track ),                  /*!< The track we're displaying */
	m_trackContainerView( _tcv ),       /*!< The track Container View we're displayed in */
	m_trackOperationsWidget( this ),    /*!< Our trackOperationsWidget */
	m_trackSettingsWidget( this ),      /*!< Our trackSettingsWidget */
	m_trackContentWidget( this ),       /*!< Our trackContentWidget */
	m_action( NoAction )                /*!< The action we're currently performing */
{
	setAutoFillBackground( true );
	QPalette pal;
	pal.setColor( backgroundRole(), QColor( 32, 36, 40 ) );
	setPalette( pal );

	m_trackSettingsWidget.setAutoFillBackground( true );

	QHBoxLayout * layout = new QHBoxLayout( this );
	layout->setMargin( 0 );
	layout->setSpacing( 0 );
	layout->addWidget( &m_trackOperationsWidget );
	layout->addWidget( &m_trackSettingsWidget );
	layout->addWidget( &m_trackContentWidget, 1 );
	setFixedHeight( m_track->getHeight() );

	resizeEvent( NULL );

	setAcceptDrops( true );
	setAttribute( Qt::WA_DeleteOnClose, true );


	connect( m_track, SIGNAL( destroyedTrack() ), this, SLOT( close() ) );
	connect( m_track,
		SIGNAL( trackContentObjectAdded( trackContentObject * ) ),
			this, SLOT( createTCOView( trackContentObject * ) ),
			Qt::QueuedConnection );

	connect( &m_track->m_mutedModel, SIGNAL( dataChanged() ),
			&m_trackContentWidget, SLOT( update() ) );

	connect( &m_track->m_soloModel, SIGNAL( dataChanged() ),
			m_track, SLOT( toggleSolo() ) );
	// create views for already existing TCOs
	for( track::tcoVector::iterator it =
					m_track->m_trackContentObjects.begin();
			it != m_track->m_trackContentObjects.end(); ++it )
	{
		createTCOView( *it );
	}

	m_trackContainerView->addTrackView( this );
}




/*! \brief Destroy this track View.
 *
 */
trackView::~trackView()
{
}




/*! \brief Resize this track View.
 *
 *  \param _re the Resize Event to handle.
 */
void trackView::resizeEvent( QResizeEvent * _re )
{
	if( configManager::inst()->value( "ui",
					  "compacttrackbuttons" ).toInt() )
	{
		m_trackOperationsWidget.setFixedSize( TRACK_OP_WIDTH_COMPACT, height() - 1 );
		m_trackSettingsWidget.setFixedSize( DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT, height() - 1 );
	}
	else
	{
		m_trackOperationsWidget.setFixedSize( TRACK_OP_WIDTH, height() - 1 );
		m_trackSettingsWidget.setFixedSize( DEFAULT_SETTINGS_WIDGET_WIDTH, height() - 1 );
	}
	m_trackContentWidget.setFixedHeight( height() );
}




/*! \brief Update this track View and all its content objects.
 *
 */
void trackView::update()
{
	m_trackContentWidget.update();
	if( !m_trackContainerView->fixedTCOs() )
	{
		m_trackContentWidget.changePosition();
	}
	QWidget::update();
}




/*! \brief Close this track View.
 *
 */
bool trackView::close()
{
	m_trackContainerView->removeTrackView( this );
	return QWidget::close();
}




/*! \brief Register that the model of this track View has changed.
 *
 */
void trackView::modelChanged()
{
	m_track = castModel<track>();
	assert( m_track != NULL );
	connect( m_track, SIGNAL( destroyedTrack() ), this, SLOT( close() ) );
	m_trackOperationsWidget.m_muteBtn->setModel( &m_track->m_mutedModel );
	m_trackOperationsWidget.m_soloBtn->setModel( &m_track->m_soloModel );
	ModelView::modelChanged();	
	setFixedHeight( m_track->getHeight() );
}




/*! \brief Undo a change to this track View.
 *
 *  \param _je the Journal Entry to undo.
 */
void trackView::undoStep( JournalEntry & _je )
{
	saveJournallingState( false );
	switch( _je.actionID() )
	{
		case MoveTrack:
			if( _je.data().toInt() > 0 )
			{
				m_trackContainerView->moveTrackViewUp( this );
			}
			else
			{
				m_trackContainerView->moveTrackViewDown( this );
			}
			break;
		case ResizeTrack:
			setFixedHeight( qMax<int>( height() +
						_je.data().toInt(),
						MINIMAL_TRACK_HEIGHT ) );
			m_trackContainerView->realignTracks();
			break;
			/*case RestoreTrack:
			setFixedHeight( DEFAULT_TRACK_HEIGHT );
			m_trackContainerView->realignTracks();
			break; */
	}
	restoreJournallingState();
}




/*! \brief Redo a change to this track View.
 *
 *  \param _je the Journal Event to redo.
 */
void trackView::redoStep( JournalEntry & _je )
{
	JournalEntry je( _je.actionID(), -_je.data().toInt() );
	undoStep( je );
}




/*! \brief Start a drag event on this track View.
 *
 *  \param _dee the DragEnterEvent to start.
 */
void trackView::dragEnterEvent( QDragEnterEvent * _dee )
{
	stringPairDrag::processDragEnterEvent( _dee, "track_" +
					QString::number( m_track->type() ) );
}




/*! \brief Accept a drop event on this track View.
 *
 *  We only accept drop events that are of the same type as this track.
 *  If so, we decode the data from the drop event by just feeding it
 *  back into the engine as a state.
 *
 *  \param _de the DropEvent to handle.
 */
void trackView::dropEvent( QDropEvent * _de )
{
	QString type = stringPairDrag::decodeKey( _de );
	QString value = stringPairDrag::decodeValue( _de );
	if( type == ( "track_" + QString::number( m_track->type() ) ) )
	{
		// value contains our XML-data so simply create a
		// multimediaProject which does the rest for us...
		multimediaProject mmp( value.toUtf8() );
		engine::getMixer()->lock();
		m_track->restoreState( mmp.content().firstChild().toElement() );
		engine::getMixer()->unlock();
		_de->accept();
	}
}




/*! \brief Handle a mouse press event on this track View.
 *
 *  If this track container supports rubber band selection, let the
 *  widget handle that and don't bother with any other handling.
 *
 *  If the left mouse button is pressed, we handle two things.  If
 *  SHIFT is pressed, then we resize vertically.  Otherwise we start
 *  the process of moving this track to a new position.
 *
 *  Otherwise we let the widget handle the mouse event as normal.
 *
 *  \param _me the MouseEvent to handle.
 */
void trackView::mousePressEvent( QMouseEvent * _me )
{
	// If previously dragged too small, restore on shift-leftclick
	if( height() < DEFAULT_TRACK_HEIGHT &&
		_me->modifiers() & Qt::ShiftModifier &&
		_me->button() == Qt::LeftButton )
	{
		setFixedHeight( DEFAULT_TRACK_HEIGHT );
		m_track->setHeight( DEFAULT_TRACK_HEIGHT );
	}


	if( m_trackContainerView->allowRubberband() == true )
	{
		QWidget::mousePressEvent( _me );
	}
	else if( _me->button() == Qt::LeftButton )
	{
		if( _me->modifiers() & Qt::ShiftModifier )
		{
			m_action = ResizeTrack;
			QCursor::setPos( mapToGlobal( QPoint( _me->x(),
								height() ) ) );
			QCursor c( Qt::SizeVerCursor);
			QApplication::setOverrideCursor( c );
		}
		else
		{
			m_action = MoveTrack;

			QCursor c( Qt::SizeAllCursor );
			QApplication::setOverrideCursor( c );
			// update because in move-mode, all elements in
			// track-op-widgets are hidden as a visual feedback
			m_trackOperationsWidget.update();
		}

		_me->accept();
	}
	else
	{
		QWidget::mousePressEvent( _me );
	}
}




/*! \brief Handle a mouse move event on this track View.
 *
 *  If this track container supports rubber band selection, let the
 *  widget handle that and don't bother with any other handling.
 *
 *  Otherwise if we've started the move process (from mousePressEvent())
 *  then move ourselves into that position, reordering the track list
 *  with moveTrackViewUp() and moveTrackViewDown() to suit.  We make a
 *  note of this in the undo journal in case the user wants to undo this
 *  move.
 *
 *  Likewise if we've started a resize process, handle this too, making
 *  sure that we never go below the minimum track height.
 *
 *  \param _me the MouseEvent to handle.
 */
void trackView::mouseMoveEvent( QMouseEvent * _me )
{

	if( m_trackContainerView->allowRubberband() == true )
	{
		QWidget::mouseMoveEvent( _me );
	}
	else if( m_action == MoveTrack )
	{
		// look which track-widget the mouse-cursor is over
		const trackView * track_at_y =
			m_trackContainerView->trackViewAt(
				mapTo( m_trackContainerView->contentWidget(),
							_me->pos() ).y() );
		// a track-widget not equal to ourself?
		if( track_at_y != NULL && track_at_y != this )
		{
			// then move us up/down there!
			if( _me->y() < 0 )
			{
				m_trackContainerView->moveTrackViewUp( this );
			}
			else
			{
				m_trackContainerView->moveTrackViewDown( this );
			}
			addJournalEntry( JournalEntry( MoveTrack, _me->y() ) );
		}
	}
	else if( m_action == ResizeTrack )
	{
		setFixedHeight( qMax<int>( _me->y(), MINIMAL_TRACK_HEIGHT ) );
		m_trackContainerView->realignTracks();
		m_track->setHeight( height() );
	}

	if( height() < DEFAULT_TRACK_HEIGHT )
	{
		toolTip::add( this, m_track->m_name );
	}
}



/*! \brief Handle a mouse release event on this track View.
 *
 *  \param _me the MouseEvent to handle.
 */
void trackView::mouseReleaseEvent( QMouseEvent * _me )
{
	m_action = NoAction;
	while( QApplication::overrideCursor() != NULL )
	{
		QApplication::restoreOverrideCursor();
	}
	m_trackOperationsWidget.update();

	QWidget::mouseReleaseEvent( _me );
}




/*! \brief Repaint this track View.
 *
 *  \param _pe the PaintEvent to start.
 */
void trackView::paintEvent( QPaintEvent * _pe )
{
	QStyleOption opt;
	opt.initFrom( this );
	QPainter p( this );
	style()->drawPrimitive( QStyle::PE_Widget, &opt, &p, this );
}




/*! \brief Create a trackContentObject View in this track View.
 *
 *  \param _tco the trackContentObject to create the view for.
 *  \todo is this a good description for what this method does?
 */
void trackView::createTCOView( trackContentObject * _tco )
{
	_tco->createView( this );
}





#include "moc_track.cxx"


