mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-19 06:28:24 -04:00
Compute the height of the potential text field before rendering the melody pattern so that we can use that information later. Transform the painter so that notes drawn into a [0,1] x [0,1] coordinate system are drawn at the correct position on the pattern widget. Add code that moves the pattern notes smoothly under the text label in case there is not much space, i.e. for narrow patterns. Notes are drawn as filled rectangles with a slightly darker border. Always draw at least an octave of notes onto the pattern, so that patterns with just a single note will not look strange, i.e. that they are not filled completely with the note. If there is additional space to fill it will be rather filled at the top of the pattern so that single notes have a high chance of being drawn below the pattern text. Simplify the drawing of the inner and outer border. Use numeric_limits to initialize the minimum and maximum key. Remove some redundant checks whether the pattern holds notes and decrease the indentation depth by two tabs for most of the code.
1136 lines
24 KiB
C++
1136 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"
|
|
|
|
#include <limits>
|
|
|
|
|
|
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 );
|
|
}
|
|
}
|
|
|
|
|
|
static int computeNoteRange(int minKey, int maxKey)
|
|
{
|
|
return (maxKey - minKey) + 1;
|
|
}
|
|
|
|
void PatternView::paintEvent( QPaintEvent * )
|
|
{
|
|
QPainter painter( this );
|
|
|
|
if( !needsUpdate() )
|
|
{
|
|
painter.drawPixmap( 0, 0, m_paintPixmap );
|
|
return;
|
|
}
|
|
|
|
setNeedsUpdate( false );
|
|
|
|
if (m_paintPixmap.isNull() || m_paintPixmap.size() != size())
|
|
{
|
|
m_paintPixmap = QPixmap(size());
|
|
}
|
|
|
|
QPainter p( &m_paintPixmap );
|
|
|
|
QLinearGradient lingrad( 0, 0, 0, height() );
|
|
QColor c;
|
|
bool const 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 );
|
|
}
|
|
|
|
// Check whether we will paint a text box and compute its potential height
|
|
// This is needed so we can paint the notes beneath it.
|
|
bool const isDefaultName = m_pat->name() == m_pat->instrumentTrack()->name();
|
|
bool const drawTextBox = !beatPattern && !isDefaultName;
|
|
|
|
int textBoxHeight = 0;
|
|
const int textTop = TCO_BORDER_WIDTH + 1;
|
|
const int textLeft = TCO_BORDER_WIDTH + 3;
|
|
if (drawTextBox)
|
|
{
|
|
QFont labelFont = this->font();
|
|
labelFont.setHintingPreference( QFont::PreferFullHinting );
|
|
|
|
QFontMetrics fontMetrics(labelFont);
|
|
textBoxHeight = fontMetrics.height() + 2 * textTop;
|
|
}
|
|
|
|
// Compute pixels per tact
|
|
const int baseWidth = fixedTCOs() ? parentWidget()->width() : width();
|
|
const float pixelsPerTact = ( baseWidth - 2 * TCO_BORDER_WIDTH ) / (float) m_pat->length().getTact();
|
|
|
|
// Length of one tact/beat in the [0,1] x [0,1] coordinate system
|
|
const float tactLength = 1. / m_pat->length().getTact();
|
|
const float tickLength = tactLength / MidiTime::ticksPerTact();
|
|
|
|
const int x_base = TCO_BORDER_WIDTH;
|
|
|
|
// melody pattern paint event
|
|
NoteVector const & noteCollection = m_pat->m_notes;
|
|
if( m_pat->m_patternType == Pattern::MelodyPattern && !noteCollection.empty() )
|
|
{
|
|
// Compute the minimum and maximum key in the pattern
|
|
// so that we know how much there is to draw.
|
|
int maxKey = std::numeric_limits<int>::min();
|
|
int minKey = std::numeric_limits<int>::max();
|
|
|
|
for (Note const * note : noteCollection)
|
|
{
|
|
int const key = note->key();
|
|
maxKey = qMax( maxKey, key );
|
|
minKey = qMin( minKey, key );
|
|
}
|
|
|
|
// set colour based on mute status
|
|
QColor const noteColor = muted ? mutedColor() : painter.pen().brush().color();
|
|
p.setPen( noteColor.darker() );
|
|
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
|
|
p.save();
|
|
|
|
// Transform such that [0, 1] x [0, 1] paints in the correct area
|
|
float distanceToTop = textBoxHeight;
|
|
|
|
// This moves the notes smoothly under the text
|
|
int widgetHeight = height();
|
|
int fullyAtTopAtLimit = MINIMAL_TRACK_HEIGHT;
|
|
int fullyBelowAtLimit = 4 * fullyAtTopAtLimit;
|
|
if (widgetHeight <= fullyBelowAtLimit)
|
|
{
|
|
if (widgetHeight <= fullyAtTopAtLimit)
|
|
{
|
|
distanceToTop = 0;
|
|
}
|
|
else
|
|
{
|
|
float const a = 1. / (fullyAtTopAtLimit - fullyBelowAtLimit);
|
|
float const b = - float(fullyBelowAtLimit) / (fullyAtTopAtLimit - fullyBelowAtLimit);
|
|
float const scale = a * widgetHeight + b;
|
|
distanceToTop = (1. - scale) * textBoxHeight;
|
|
}
|
|
}
|
|
|
|
p.translate(0., distanceToTop);
|
|
p.scale(width(), height() - distanceToTop);
|
|
|
|
int const minimalNoteRange = 12; // Always paint at least one octave
|
|
int const actualNoteRange = computeNoteRange(minKey, maxKey);
|
|
|
|
if (actualNoteRange < minimalNoteRange)
|
|
{
|
|
int missingNumberOfNotes = minimalNoteRange - actualNoteRange;
|
|
minKey = std::max(0, minKey - missingNumberOfNotes / 2);
|
|
maxKey = maxKey + missingNumberOfNotes / 2;
|
|
if (missingNumberOfNotes % 2 == 1)
|
|
{
|
|
// Put more range at the top to bias drawing towards the bottom
|
|
++maxKey;
|
|
}
|
|
}
|
|
|
|
int const adjustedNoteRange = computeNoteRange(minKey, maxKey);
|
|
float const noteHeight = 1. / adjustedNoteRange;
|
|
|
|
// scan through all the notes and draw them on the pattern
|
|
for (Note const * currentNote : noteCollection)
|
|
{
|
|
// Map to 0, 1, 2, ...
|
|
int mappedNoteKey = currentNote->key() - minKey;
|
|
int invertedMappedNoteKey = adjustedNoteRange - mappedNoteKey - 1;
|
|
|
|
float const noteStartX = currentNote->pos() * tickLength;
|
|
float const noteLength = currentNote->length() * tickLength;
|
|
|
|
float const noteStartY = invertedMappedNoteKey * noteHeight;
|
|
|
|
QRectF noteRectF( noteStartX, noteStartY, noteLength, noteHeight);
|
|
p.fillRect( noteRectF, noteColor );
|
|
p.drawRect( noteRectF );
|
|
}
|
|
|
|
p.restore();
|
|
}
|
|
|
|
// beat pattern paint event
|
|
else if( beatPattern && ( fixedTCOs() || pixelsPerTact >= 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>( pixelsPerTact * t ) - 1,
|
|
TCO_BORDER_WIDTH, x_base + static_cast<int>(
|
|
pixelsPerTact * t ) - 1, TCO_BORDER_WIDTH + lineSize );
|
|
p.drawLine( x_base + static_cast<int>( pixelsPerTact * t ) - 1,
|
|
rect().bottom() - ( lineSize + TCO_BORDER_WIDTH ),
|
|
x_base + static_cast<int>( pixelsPerTact * t ) - 1,
|
|
rect().bottom() - TCO_BORDER_WIDTH );
|
|
}
|
|
|
|
// pattern name
|
|
if (drawTextBox)
|
|
{
|
|
paintTextLabel(m_pat->name(), p);
|
|
}
|
|
|
|
if( !beatPattern )
|
|
{
|
|
// inner border
|
|
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 ? c.lighter( 130 ) : c.darker( 300 ) );
|
|
p.drawRect( rect() );
|
|
}
|
|
|
|
// 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 );
|
|
|
|
}
|