mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-19 14:38:22 -04:00
1124 lines
24 KiB
C++
1124 lines
24 KiB
C++
/*
|
|
* Pattern.cpp - implementation of class pattern which holds notes
|
|
*
|
|
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
|
|
* Copyright (c) 2005-2007 Danny McRae <khjklujn/at/yahoo.com>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* 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.
|
|
*
|
|
*/
|
|
#include "Pattern.h"
|
|
|
|
#include <QTimer>
|
|
#include <QMenu>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QPushButton>
|
|
|
|
#include "InstrumentTrack.h"
|
|
#include "gui_templates.h"
|
|
#include "embed.h"
|
|
#include "GuiApplication.h"
|
|
#include "PianoRoll.h"
|
|
#include "RenameDialog.h"
|
|
#include "SampleBuffer.h"
|
|
#include "AudioSampleRecorder.h"
|
|
#include "BBTrackContainer.h"
|
|
#include "StringPairDrag.h"
|
|
#include "MainWindow.h"
|
|
|
|
|
|
QPixmap * PatternView::s_stepBtnOn0 = NULL;
|
|
QPixmap * PatternView::s_stepBtnOn200 = NULL;
|
|
QPixmap * PatternView::s_stepBtnOff = NULL;
|
|
QPixmap * PatternView::s_stepBtnOffLight = NULL;
|
|
|
|
|
|
|
|
Pattern::Pattern( InstrumentTrack * _instrument_track ) :
|
|
TrackContentObject( _instrument_track ),
|
|
m_instrumentTrack( _instrument_track ),
|
|
m_patternType( BeatPattern ),
|
|
m_steps( MidiTime::stepsPerTact() )
|
|
{
|
|
setName( _instrument_track->name() );
|
|
if( _instrument_track->trackContainer()
|
|
== Engine::getBBTrackContainer() )
|
|
{
|
|
resizeToFirstTrack();
|
|
}
|
|
init();
|
|
setAutoResize( true );
|
|
}
|
|
|
|
|
|
|
|
|
|
Pattern::Pattern( const Pattern& other ) :
|
|
TrackContentObject( other.m_instrumentTrack ),
|
|
m_instrumentTrack( other.m_instrumentTrack ),
|
|
m_patternType( other.m_patternType ),
|
|
m_steps( other.m_steps )
|
|
{
|
|
for( NoteVector::ConstIterator it = other.m_notes.begin(); it != other.m_notes.end(); ++it )
|
|
{
|
|
m_notes.push_back( new Note( **it ) );
|
|
}
|
|
|
|
init();
|
|
switch( getTrack()->trackContainer()->type() )
|
|
{
|
|
case TrackContainer::BBContainer:
|
|
setAutoResize( true );
|
|
break;
|
|
|
|
case TrackContainer::SongContainer:
|
|
// move down
|
|
default:
|
|
setAutoResize( false );
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
Pattern::~Pattern()
|
|
{
|
|
emit destroyedPattern( this );
|
|
|
|
for( NoteVector::Iterator it = m_notes.begin();
|
|
it != m_notes.end(); ++it )
|
|
{
|
|
delete *it;
|
|
}
|
|
|
|
m_notes.clear();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::resizeToFirstTrack()
|
|
{
|
|
// Resize this track to be the same as existing tracks in the BB
|
|
const TrackContainer::TrackList & tracks =
|
|
m_instrumentTrack->trackContainer()->tracks();
|
|
for(unsigned int trackID = 0; trackID < tracks.size(); ++trackID)
|
|
{
|
|
if(tracks.at(trackID)->type() == Track::InstrumentTrack)
|
|
{
|
|
if(tracks.at(trackID) != m_instrumentTrack)
|
|
{
|
|
unsigned int currentTCO = m_instrumentTrack->
|
|
getTCOs().indexOf(this);
|
|
m_steps = static_cast<Pattern *>
|
|
(tracks.at(trackID)->getTCO(currentTCO))
|
|
->m_steps;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::init()
|
|
{
|
|
connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int, int ) ),
|
|
this, SLOT( changeTimeSignature() ) );
|
|
saveJournallingState( false );
|
|
|
|
updateLength();
|
|
restoreJournallingState();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::updateLength()
|
|
{
|
|
if( m_patternType == BeatPattern )
|
|
{
|
|
changeLength( beatPatternLength() );
|
|
updateBBTrack();
|
|
return;
|
|
}
|
|
|
|
tick_t max_length = MidiTime::ticksPerTact();
|
|
|
|
for( NoteVector::ConstIterator it = m_notes.begin();
|
|
it != m_notes.end(); ++it )
|
|
{
|
|
if( ( *it )->length() > 0 )
|
|
{
|
|
max_length = qMax<tick_t>( max_length,
|
|
( *it )->endPos() );
|
|
}
|
|
}
|
|
changeLength( MidiTime( max_length ).nextFullTact() *
|
|
MidiTime::ticksPerTact() );
|
|
updateBBTrack();
|
|
}
|
|
|
|
|
|
|
|
|
|
MidiTime Pattern::beatPatternLength() const
|
|
{
|
|
tick_t max_length = MidiTime::ticksPerTact();
|
|
|
|
for( NoteVector::ConstIterator it = m_notes.begin();
|
|
it != m_notes.end(); ++it )
|
|
{
|
|
if( ( *it )->length() < 0 )
|
|
{
|
|
max_length = qMax<tick_t>( max_length,
|
|
( *it )->pos() + 1 );
|
|
}
|
|
}
|
|
|
|
if( m_steps != MidiTime::stepsPerTact() )
|
|
{
|
|
max_length = m_steps * MidiTime::ticksPerTact() /
|
|
MidiTime::stepsPerTact();
|
|
}
|
|
|
|
return MidiTime( max_length ).nextFullTact() * MidiTime::ticksPerTact();
|
|
}
|
|
|
|
|
|
|
|
|
|
Note * Pattern::addNote( const Note & _new_note, const bool _quant_pos )
|
|
{
|
|
Note * new_note = new Note( _new_note );
|
|
if( _quant_pos && gui->pianoRoll() )
|
|
{
|
|
new_note->quantizePos( gui->pianoRoll()->quantization() );
|
|
}
|
|
|
|
instrumentTrack()->lock();
|
|
if( m_notes.size() == 0 || m_notes.back()->pos() <= new_note->pos() )
|
|
{
|
|
m_notes.push_back( new_note );
|
|
}
|
|
else
|
|
{
|
|
// simple algorithm for inserting the note between two
|
|
// notes with smaller and greater position
|
|
// maybe it could be optimized by starting in the middle and
|
|
// going forward or backward but note-inserting isn't that
|
|
// time-critical since it is usually not done while playing...
|
|
long new_note_abs_time = new_note->pos();
|
|
NoteVector::Iterator it = m_notes.begin();
|
|
|
|
while( it != m_notes.end() &&
|
|
( *it )->pos() < new_note_abs_time )
|
|
{
|
|
++it;
|
|
}
|
|
|
|
m_notes.insert( it, new_note );
|
|
}
|
|
instrumentTrack()->unlock();
|
|
|
|
checkType();
|
|
updateLength();
|
|
|
|
emit dataChanged();
|
|
|
|
return new_note;
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::removeNote( Note * _note_to_del )
|
|
{
|
|
instrumentTrack()->lock();
|
|
NoteVector::Iterator it = m_notes.begin();
|
|
while( it != m_notes.end() )
|
|
{
|
|
if( *it == _note_to_del )
|
|
{
|
|
delete *it;
|
|
m_notes.erase( it );
|
|
break;
|
|
}
|
|
++it;
|
|
}
|
|
instrumentTrack()->unlock();
|
|
|
|
checkType();
|
|
updateLength();
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
|
|
// returns a pointer to the note at specified step, or NULL if note doesn't exist
|
|
|
|
Note * Pattern::noteAtStep( int _step )
|
|
{
|
|
for( NoteVector::Iterator it = m_notes.begin(); it != m_notes.end();
|
|
++it )
|
|
{
|
|
if( ( *it )->pos() == MidiTime::stepPosition( _step )
|
|
&& ( *it )->length() < 0 )
|
|
{
|
|
return *it;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
|
|
void Pattern::rearrangeAllNotes()
|
|
{
|
|
// sort notes by start time
|
|
qSort(m_notes.begin(), m_notes.end(), Note::lessThan );
|
|
}
|
|
|
|
|
|
|
|
void Pattern::clearNotes()
|
|
{
|
|
instrumentTrack()->lock();
|
|
for( NoteVector::Iterator it = m_notes.begin(); it != m_notes.end();
|
|
++it )
|
|
{
|
|
delete *it;
|
|
}
|
|
m_notes.clear();
|
|
instrumentTrack()->unlock();
|
|
|
|
checkType();
|
|
emit dataChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
Note * Pattern::addStepNote( int step )
|
|
{
|
|
return addNote( Note( MidiTime( -DefaultTicksPerTact ),
|
|
MidiTime::stepPosition( step ) ), false );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::setStep( int step, bool enabled )
|
|
{
|
|
if( enabled )
|
|
{
|
|
if ( !noteAtStep( step ) )
|
|
{
|
|
addStepNote( step );
|
|
}
|
|
return;
|
|
}
|
|
|
|
while( Note * note = noteAtStep( step ) )
|
|
{
|
|
removeNote( note );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::setType( PatternTypes _new_pattern_type )
|
|
{
|
|
if( _new_pattern_type == BeatPattern ||
|
|
_new_pattern_type == MelodyPattern )
|
|
{
|
|
m_patternType = _new_pattern_type;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::checkType()
|
|
{
|
|
NoteVector::Iterator it = m_notes.begin();
|
|
while( it != m_notes.end() )
|
|
{
|
|
if( ( *it )->length() > 0 )
|
|
{
|
|
setType( MelodyPattern );
|
|
return;
|
|
}
|
|
++it;
|
|
}
|
|
setType( BeatPattern );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::saveSettings( QDomDocument & _doc, QDomElement & _this )
|
|
{
|
|
_this.setAttribute( "type", m_patternType );
|
|
_this.setAttribute( "name", name() );
|
|
// as the target of copied/dragged pattern is always an existing
|
|
// pattern, we must not store actual position, instead we store -1
|
|
// which tells loadSettings() not to mess around with position
|
|
if( _this.parentNode().nodeName() == "clipboard" ||
|
|
_this.parentNode().nodeName() == "dnddata" )
|
|
{
|
|
_this.setAttribute( "pos", -1 );
|
|
}
|
|
else
|
|
{
|
|
_this.setAttribute( "pos", startPosition() );
|
|
}
|
|
_this.setAttribute( "muted", isMuted() );
|
|
_this.setAttribute( "steps", m_steps );
|
|
|
|
// now save settings of all notes
|
|
for( NoteVector::Iterator it = m_notes.begin();
|
|
it != m_notes.end(); ++it )
|
|
{
|
|
( *it )->saveState( _doc, _this );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::loadSettings( const QDomElement & _this )
|
|
{
|
|
m_patternType = static_cast<PatternTypes>( _this.attribute( "type"
|
|
).toInt() );
|
|
setName( _this.attribute( "name" ) );
|
|
if( _this.attribute( "pos" ).toInt() >= 0 )
|
|
{
|
|
movePosition( _this.attribute( "pos" ).toInt() );
|
|
}
|
|
if( _this.attribute( "muted" ).toInt() != isMuted() )
|
|
{
|
|
toggleMute();
|
|
}
|
|
|
|
clearNotes();
|
|
|
|
QDomNode node = _this.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
if( node.isElement() &&
|
|
!node.toElement().attribute( "metadata" ).toInt() )
|
|
{
|
|
Note * n = new Note;
|
|
n->restoreState( node.toElement() );
|
|
m_notes.push_back( n );
|
|
}
|
|
node = node.nextSibling();
|
|
}
|
|
|
|
m_steps = _this.attribute( "steps" ).toInt();
|
|
if( m_steps == 0 )
|
|
{
|
|
m_steps = MidiTime::stepsPerTact();
|
|
}
|
|
|
|
checkType();
|
|
updateLength();
|
|
|
|
emit dataChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
Pattern * Pattern::previousPattern() const
|
|
{
|
|
return adjacentPatternByOffset(-1);
|
|
}
|
|
|
|
|
|
|
|
|
|
Pattern * Pattern::nextPattern() const
|
|
{
|
|
return adjacentPatternByOffset(1);
|
|
}
|
|
|
|
|
|
|
|
|
|
Pattern * Pattern::adjacentPatternByOffset(int offset) const
|
|
{
|
|
QVector<TrackContentObject *> tcos = m_instrumentTrack->getTCOs();
|
|
int tcoNum = m_instrumentTrack->getTCONum(this);
|
|
return dynamic_cast<Pattern*>(tcos.value(tcoNum + offset, NULL));
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::clear()
|
|
{
|
|
addJournalCheckPoint();
|
|
clearNotes();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::addSteps()
|
|
{
|
|
m_steps += MidiTime::stepsPerTact();
|
|
updateLength();
|
|
emit dataChanged();
|
|
}
|
|
|
|
void Pattern::cloneSteps()
|
|
{
|
|
int oldLength = m_steps;
|
|
m_steps *= 2; // cloning doubles the track
|
|
for(int i = 0; i < oldLength; ++i )
|
|
{
|
|
Note *toCopy = noteAtStep( i );
|
|
if( toCopy )
|
|
{
|
|
setStep( oldLength + i, true );
|
|
Note *newNote = noteAtStep( oldLength + i );
|
|
newNote->setKey( toCopy->key() );
|
|
newNote->setLength( toCopy->length() );
|
|
newNote->setPanning( toCopy->getPanning() );
|
|
newNote->setVolume( toCopy->getVolume() );
|
|
}
|
|
}
|
|
updateLength();
|
|
emit dataChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::removeSteps()
|
|
{
|
|
int n = MidiTime::stepsPerTact();
|
|
if( n < m_steps )
|
|
{
|
|
for( int i = m_steps - n; i < m_steps; ++i )
|
|
{
|
|
setStep( i, false );
|
|
}
|
|
m_steps -= n;
|
|
updateLength();
|
|
emit dataChanged();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
TrackContentObjectView * Pattern::createView( TrackView * _tv )
|
|
{
|
|
return new PatternView( this, _tv );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::updateBBTrack()
|
|
{
|
|
if( getTrack()->trackContainer() == Engine::getBBTrackContainer() )
|
|
{
|
|
Engine::getBBTrackContainer()->updateBBTrack( this );
|
|
}
|
|
|
|
if( gui && gui->pianoRoll() && gui->pianoRoll()->currentPattern() == this )
|
|
{
|
|
gui->pianoRoll()->update();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Pattern::empty()
|
|
{
|
|
for( NoteVector::ConstIterator it = m_notes.begin();
|
|
it != m_notes.end(); ++it )
|
|
{
|
|
if( ( *it )->length() != 0 )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
void Pattern::changeTimeSignature()
|
|
{
|
|
MidiTime last_pos = MidiTime::ticksPerTact() - 1;
|
|
for( NoteVector::ConstIterator cit = m_notes.begin();
|
|
cit != m_notes.end(); ++cit )
|
|
{
|
|
if( ( *cit )->length() < 0 && ( *cit )->pos() > last_pos )
|
|
{
|
|
last_pos = ( *cit )->pos()+MidiTime::ticksPerTact() /
|
|
MidiTime::stepsPerTact();
|
|
}
|
|
}
|
|
last_pos = last_pos.nextFullTact() * MidiTime::ticksPerTact();
|
|
m_steps = qMax<tick_t>( MidiTime::stepsPerTact(),
|
|
last_pos.getTact() * MidiTime::stepsPerTact() );
|
|
updateLength();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PatternView::PatternView( Pattern* pattern, TrackView* parent ) :
|
|
TrackContentObjectView( pattern, parent ),
|
|
m_pat( pattern ),
|
|
m_paintPixmap()
|
|
{
|
|
connect( gui->pianoRoll(), SIGNAL( currentPatternChanged() ),
|
|
this, SLOT( update() ) );
|
|
|
|
if( s_stepBtnOn0 == NULL )
|
|
{
|
|
s_stepBtnOn0 = new QPixmap( embed::getIconPixmap(
|
|
"step_btn_on_0" ) );
|
|
}
|
|
|
|
if( s_stepBtnOn200 == NULL )
|
|
{
|
|
s_stepBtnOn200 = new QPixmap( embed::getIconPixmap(
|
|
"step_btn_on_200" ) );
|
|
}
|
|
|
|
if( s_stepBtnOff == NULL )
|
|
{
|
|
s_stepBtnOff = new QPixmap( embed::getIconPixmap(
|
|
"step_btn_off" ) );
|
|
}
|
|
|
|
if( s_stepBtnOffLight == NULL )
|
|
{
|
|
s_stepBtnOffLight = new QPixmap( embed::getIconPixmap(
|
|
"step_btn_off_light" ) );
|
|
}
|
|
|
|
update();
|
|
|
|
setStyle( QApplication::style() );
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PatternView::~PatternView()
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PatternView::update()
|
|
{
|
|
if ( m_pat->m_patternType == Pattern::BeatPattern )
|
|
{
|
|
ToolTip::add( this,
|
|
tr( "use mouse wheel to set velocity of a step" ) );
|
|
}
|
|
else
|
|
{
|
|
ToolTip::add( this,
|
|
tr( "double-click to open in Piano Roll" ) );
|
|
}
|
|
|
|
TrackContentObjectView::update();
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::openInPianoRoll()
|
|
{
|
|
gui->pianoRoll()->setCurrentPattern( m_pat );
|
|
gui->pianoRoll()->parentWidget()->show();
|
|
gui->pianoRoll()->show();
|
|
gui->pianoRoll()->setFocus();
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::resetName()
|
|
{
|
|
m_pat->setName( m_pat->m_instrumentTrack->name() );
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::changeName()
|
|
{
|
|
QString s = m_pat->name();
|
|
RenameDialog rename_dlg( s );
|
|
rename_dlg.exec();
|
|
m_pat->setName( s );
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::constructContextMenu( QMenu * _cm )
|
|
{
|
|
QAction * a = new QAction( embed::getIconPixmap( "piano" ),
|
|
tr( "Open in piano-roll" ), _cm );
|
|
_cm->insertAction( _cm->actions()[0], a );
|
|
connect( a, SIGNAL( triggered( bool ) ),
|
|
this, SLOT( openInPianoRoll() ) );
|
|
_cm->insertSeparator( _cm->actions()[1] );
|
|
|
|
_cm->addSeparator();
|
|
|
|
_cm->addAction( embed::getIconPixmap( "edit_erase" ),
|
|
tr( "Clear all notes" ), m_pat, SLOT( clear() ) );
|
|
_cm->addSeparator();
|
|
|
|
_cm->addAction( embed::getIconPixmap( "reload" ), tr( "Reset name" ),
|
|
this, SLOT( resetName() ) );
|
|
_cm->addAction( embed::getIconPixmap( "edit_rename" ),
|
|
tr( "Change name" ),
|
|
this, SLOT( changeName() ) );
|
|
|
|
if ( m_pat->type() == Pattern::BeatPattern )
|
|
{
|
|
_cm->addSeparator();
|
|
|
|
_cm->addAction( embed::getIconPixmap( "step_btn_add" ),
|
|
tr( "Add steps" ), m_pat, SLOT( addSteps() ) );
|
|
_cm->addAction( embed::getIconPixmap( "step_btn_remove" ),
|
|
tr( "Remove steps" ), m_pat, SLOT( removeSteps() ) );
|
|
_cm->addAction( embed::getIconPixmap( "step_btn_duplicate" ),
|
|
tr( "Clone Steps" ), m_pat, SLOT( cloneSteps() ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::mousePressEvent( QMouseEvent * _me )
|
|
{
|
|
if( _me->button() == Qt::LeftButton &&
|
|
m_pat->m_patternType == Pattern::BeatPattern &&
|
|
( fixedTCOs() || pixelsPerTact() >= 96 ||
|
|
m_pat->m_steps != MidiTime::stepsPerTact() ) &&
|
|
_me->y() > height() - s_stepBtnOff->height() )
|
|
|
|
// when mouse button is pressed in beat/bassline -mode
|
|
|
|
{
|
|
// get the step number that was clicked on and
|
|
// do calculations in floats to prevent rounding errors...
|
|
float tmp = ( ( float(_me->x()) - TCO_BORDER_WIDTH ) *
|
|
float( m_pat -> m_steps ) ) / float(width() - TCO_BORDER_WIDTH*2);
|
|
|
|
int step = int( tmp );
|
|
|
|
// debugging to ensure we get the correct step...
|
|
// qDebug( "Step (%f) %d", tmp, step );
|
|
|
|
if( step >= m_pat->m_steps )
|
|
{
|
|
qDebug( "Something went wrong in pattern.cpp: step %d doesn't exist in pattern!", step );
|
|
return;
|
|
}
|
|
|
|
Note * n = m_pat->noteAtStep( step );
|
|
|
|
if( n == NULL )
|
|
{
|
|
m_pat->addStepNote( step );
|
|
}
|
|
else // note at step found
|
|
{
|
|
m_pat->addJournalCheckPoint();
|
|
m_pat->setStep( step, false );
|
|
}
|
|
|
|
Engine::getSong()->setModified();
|
|
update();
|
|
|
|
if( gui->pianoRoll()->currentPattern() == m_pat )
|
|
{
|
|
gui->pianoRoll()->update();
|
|
}
|
|
}
|
|
else
|
|
|
|
// if not in beat/bassline -mode, let parent class handle the event
|
|
|
|
{
|
|
TrackContentObjectView::mousePressEvent( _me );
|
|
}
|
|
}
|
|
|
|
void PatternView::mouseDoubleClickEvent(QMouseEvent *_me)
|
|
{
|
|
if( _me->button() != Qt::LeftButton )
|
|
{
|
|
_me->ignore();
|
|
return;
|
|
}
|
|
if( m_pat->m_patternType == Pattern::MelodyPattern || !fixedTCOs() )
|
|
{
|
|
openInPianoRoll();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::wheelEvent( QWheelEvent * _we )
|
|
{
|
|
if( m_pat->m_patternType == Pattern::BeatPattern &&
|
|
( fixedTCOs() || pixelsPerTact() >= 96 ||
|
|
m_pat->m_steps != MidiTime::stepsPerTact() ) &&
|
|
_we->y() > height() - s_stepBtnOff->height() )
|
|
{
|
|
// get the step number that was wheeled on and
|
|
// do calculations in floats to prevent rounding errors...
|
|
float tmp = ( ( float(_we->x()) - TCO_BORDER_WIDTH ) *
|
|
float( m_pat -> m_steps ) ) / float(width() - TCO_BORDER_WIDTH*2);
|
|
|
|
int step = int( tmp );
|
|
|
|
if( step >= m_pat->m_steps )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Note * n = m_pat->noteAtStep( step );
|
|
if( !n && _we->delta() > 0 )
|
|
{
|
|
n = m_pat->addStepNote( step );
|
|
n->setVolume( 0 );
|
|
}
|
|
if( n != NULL )
|
|
{
|
|
int vol = n->getVolume();
|
|
|
|
if( _we->delta() > 0 )
|
|
{
|
|
n->setVolume( qMin( 100, vol + 5 ) );
|
|
}
|
|
else
|
|
{
|
|
n->setVolume( qMax( 0, vol - 5 ) );
|
|
}
|
|
|
|
Engine::getSong()->setModified();
|
|
update();
|
|
if( gui->pianoRoll()->currentPattern() == m_pat )
|
|
{
|
|
gui->pianoRoll()->update();
|
|
}
|
|
}
|
|
_we->accept();
|
|
}
|
|
else
|
|
{
|
|
TrackContentObjectView::wheelEvent( _we );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void PatternView::paintEvent( QPaintEvent * )
|
|
{
|
|
QPainter painter( this );
|
|
|
|
if( !needsUpdate() )
|
|
{
|
|
painter.drawPixmap( 0, 0, m_paintPixmap );
|
|
return;
|
|
}
|
|
|
|
setNeedsUpdate( false );
|
|
|
|
m_paintPixmap = m_paintPixmap.isNull() == true || m_paintPixmap.size() != size()
|
|
? QPixmap( size() ) : m_paintPixmap;
|
|
|
|
QPainter p( &m_paintPixmap );
|
|
|
|
QLinearGradient lingrad( 0, 0, 0, height() );
|
|
QColor c;
|
|
bool muted = m_pat->getTrack()->isMuted() || m_pat->isMuted();
|
|
bool current = gui->pianoRoll()->currentPattern() == m_pat;
|
|
bool beatPattern = m_pat->m_patternType == Pattern::BeatPattern;
|
|
|
|
// state: selected, normal, beat pattern, muted
|
|
c = isSelected() ? selectedColor() : ( ( !muted && !beatPattern )
|
|
? painter.background().color() : ( beatPattern
|
|
? BBPatternBackground() : mutedBackgroundColor() ) );
|
|
|
|
// invert the gradient for the background in the B&B editor
|
|
lingrad.setColorAt( beatPattern ? 0 : 1, c.darker( 300 ) );
|
|
lingrad.setColorAt( beatPattern ? 1 : 0, c );
|
|
|
|
if( gradient() )
|
|
{
|
|
p.fillRect( rect(), lingrad );
|
|
}
|
|
else
|
|
{
|
|
p.fillRect( rect(), c );
|
|
}
|
|
|
|
const float ppt = fixedTCOs() ?
|
|
( parentWidget()->width() - 2 * TCO_BORDER_WIDTH )
|
|
/ (float) m_pat->length().getTact() :
|
|
( width() - 2 * TCO_BORDER_WIDTH )
|
|
/ (float) m_pat->length().getTact();
|
|
|
|
const int x_base = TCO_BORDER_WIDTH;
|
|
|
|
// melody pattern paint event
|
|
if( m_pat->m_patternType == Pattern::MelodyPattern )
|
|
{
|
|
if( m_pat->m_notes.size() > 0 )
|
|
{
|
|
// first determine the central tone so that we can
|
|
// display the area where most of the m_notes are
|
|
// also calculate min/max tones so the tonal range can be
|
|
// properly stretched accross the pattern vertically
|
|
|
|
int central_key = 0;
|
|
int max_key = 0;
|
|
int min_key = 9999999;
|
|
int total_notes = 0;
|
|
|
|
for( NoteVector::Iterator it = m_pat->m_notes.begin();
|
|
it != m_pat->m_notes.end(); ++it )
|
|
{
|
|
max_key = qMax( max_key, ( *it )->key() );
|
|
min_key = qMin( min_key, ( *it )->key() );
|
|
central_key += ( *it )->key();
|
|
++total_notes;
|
|
}
|
|
|
|
if( total_notes > 0 )
|
|
{
|
|
central_key = central_key / total_notes;
|
|
const int keyrange = qMax( qMax( max_key - central_key, central_key - min_key ), 1 );
|
|
|
|
// debug code
|
|
// qDebug( "keyrange: %d", keyrange );
|
|
|
|
// determine height of the pattern view, sans borders
|
|
const int ht = (height() - 1 - TCO_BORDER_WIDTH * 2) -1;
|
|
|
|
// determine maximum height value for drawing bounds checking
|
|
const int max_ht = height() - 1 - TCO_BORDER_WIDTH;
|
|
|
|
// set colour based on mute status
|
|
p.setPen( muted ? mutedColor() : painter.pen().brush().color() );
|
|
|
|
// scan through all the notes and draw them on the pattern
|
|
for( NoteVector::Iterator it =
|
|
m_pat->m_notes.begin();
|
|
it != m_pat->m_notes.end(); ++it )
|
|
{
|
|
// calculate relative y-position
|
|
const float y_key =
|
|
( float( central_key - ( *it )->key() ) / keyrange + 1.0f ) / 2;
|
|
// multiply that by pattern height
|
|
const int y_pos = static_cast<int>( TCO_BORDER_WIDTH + y_key * ht ) + 1;
|
|
|
|
// debug code
|
|
// if( ( *it )->length() > 0 ) qDebug( "key %d, central_key %d, y_key %f, y_pos %d", ( *it )->key(), central_key, y_key, y_pos );
|
|
|
|
// check that note isn't out of bounds, and has a length
|
|
if( y_pos >= TCO_BORDER_WIDTH &&
|
|
y_pos <= max_ht )
|
|
{
|
|
// calculate start and end x-coords of the line to be drawn
|
|
int length = ( *it )->length();
|
|
length = length > 0 ? length : 4;
|
|
const int x1 = x_base +
|
|
static_cast<int>
|
|
( ( *it )->pos() * ( ppt / MidiTime::ticksPerTact() ) );
|
|
const int x2 = x_base +
|
|
static_cast<int>
|
|
( ( ( *it )->pos() + length ) * ( ppt / MidiTime::ticksPerTact() ) );
|
|
|
|
// check bounds, draw line
|
|
if( x1 < width() - TCO_BORDER_WIDTH )
|
|
p.drawLine( x1, y_pos,
|
|
qMin( x2, width() - TCO_BORDER_WIDTH ), y_pos );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// beat pattern paint event
|
|
else if( beatPattern && ( fixedTCOs() || ppt >= 96
|
|
|| m_pat->m_steps != MidiTime::stepsPerTact() ) )
|
|
{
|
|
QPixmap stepon0;
|
|
QPixmap stepon200;
|
|
QPixmap stepoff;
|
|
QPixmap stepoffl;
|
|
const int steps = qMax( 1,
|
|
m_pat->m_steps );
|
|
const int w = width() - 2 * TCO_BORDER_WIDTH;
|
|
|
|
// scale step graphics to fit the beat pattern length
|
|
stepon0 = s_stepBtnOn0->scaled( w / steps,
|
|
s_stepBtnOn0->height(),
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation );
|
|
stepon200 = s_stepBtnOn200->scaled( w / steps,
|
|
s_stepBtnOn200->height(),
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation );
|
|
stepoff = s_stepBtnOff->scaled( w / steps,
|
|
s_stepBtnOff->height(),
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation );
|
|
stepoffl = s_stepBtnOffLight->scaled( w / steps,
|
|
s_stepBtnOffLight->height(),
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation );
|
|
|
|
for( int it = 0; it < steps; it++ ) // go through all the steps in the beat pattern
|
|
{
|
|
Note * n = m_pat->noteAtStep( it );
|
|
|
|
// figure out x and y coordinates for step graphic
|
|
const int x = TCO_BORDER_WIDTH + static_cast<int>( it * w / steps );
|
|
const int y = height() - s_stepBtnOff->height() - 1;
|
|
|
|
if( n )
|
|
{
|
|
const int vol = n->getVolume();
|
|
p.drawPixmap( x, y, stepoffl );
|
|
p.drawPixmap( x, y, stepon0 );
|
|
p.setOpacity( sqrt( vol / 200.0 ) );
|
|
p.drawPixmap( x, y, stepon200 );
|
|
p.setOpacity( 1 );
|
|
}
|
|
else if( ( it / 4 ) % 2 )
|
|
{
|
|
p.drawPixmap( x, y, stepoffl );
|
|
}
|
|
else
|
|
{
|
|
p.drawPixmap( x, y, stepoff );
|
|
}
|
|
} // end for loop
|
|
|
|
// draw a transparent rectangle over muted patterns
|
|
if ( muted )
|
|
{
|
|
p.setBrush( mutedBackgroundColor() );
|
|
p.setOpacity( 0.5 );
|
|
p.drawRect( 0, 0, width(), height() );
|
|
}
|
|
}
|
|
|
|
// bar lines
|
|
const int lineSize = 3;
|
|
p.setPen( c.darker( 200 ) );
|
|
|
|
for( tact_t t = 1; t < m_pat->length().getTact(); ++t )
|
|
{
|
|
p.drawLine( x_base + static_cast<int>( ppt * t ) - 1,
|
|
TCO_BORDER_WIDTH, x_base + static_cast<int>(
|
|
ppt * t ) - 1, TCO_BORDER_WIDTH + lineSize );
|
|
p.drawLine( x_base + static_cast<int>( ppt * t ) - 1,
|
|
rect().bottom() - ( lineSize + TCO_BORDER_WIDTH ),
|
|
x_base + static_cast<int>( ppt * t ) - 1,
|
|
rect().bottom() - TCO_BORDER_WIDTH );
|
|
}
|
|
|
|
// pattern name
|
|
bool isDefaultName = m_pat->name() == m_pat->instrumentTrack()->name();
|
|
|
|
if (!beatPattern && !isDefaultName)
|
|
{
|
|
p.setRenderHint( QPainter::TextAntialiasing );
|
|
|
|
QFont labelFont = this->font();
|
|
labelFont.setHintingPreference( QFont::PreferFullHinting );
|
|
p.setFont( labelFont );
|
|
|
|
const int textTop = TCO_BORDER_WIDTH + 1;
|
|
const int textLeft = TCO_BORDER_WIDTH + 3;
|
|
|
|
QFontMetrics fontMetrics(labelFont);
|
|
QString elidedPatternName = fontMetrics.elidedText(m_pat->name(), Qt::ElideMiddle, width() - 2 * textLeft);
|
|
|
|
QColor transparentBlack(0, 0, 0, 75);
|
|
p.fillRect(QRect(0, 0, width(), fontMetrics.height() + 2 * textTop), transparentBlack);
|
|
|
|
if( m_staticTextName.text() != elidedPatternName )
|
|
{
|
|
m_staticTextName.setText( elidedPatternName );
|
|
}
|
|
|
|
p.setPen( textColor() );
|
|
p.drawStaticText( textLeft, textTop, m_staticTextName );
|
|
}
|
|
|
|
// inner border
|
|
if( !beatPattern )
|
|
{
|
|
p.setPen( c.lighter( current ? 160 : 130 ) );
|
|
p.drawRect( 1, 1, rect().right() - TCO_BORDER_WIDTH,
|
|
rect().bottom() - TCO_BORDER_WIDTH );
|
|
|
|
// outer border
|
|
p.setPen( ( current && !beatPattern ) ? c.lighter( 130 ) : c.darker( 300 ) );
|
|
p.drawRect( 0, 0, rect().right(), rect().bottom() );
|
|
}
|
|
// draw the 'muted' pixmap only if the pattern was manually muted
|
|
if( m_pat->isMuted() )
|
|
{
|
|
const int spacing = TCO_BORDER_WIDTH;
|
|
const int size = 14;
|
|
p.drawPixmap( spacing, height() - ( size + spacing ),
|
|
embed::getIconPixmap( "muted", size, size ) );
|
|
}
|
|
|
|
p.end();
|
|
|
|
painter.drawPixmap( 0, 0, m_paintPixmap );
|
|
|
|
}
|