Files
lmms/src/gui/editors/PianoRoll.cpp
T
Oskar Wallgren ca09b29087 Select the correct piano key for marking semitones (#5478)
When selecting a Piano Key to mark semitones in the Piano Roll we
select key from the y position of the pop-up menu and not the mouse.
Incidentally these two are most often the same as the menu builds
from the mouse y positon and down. If there is room for it. If there
is no room downward it will create the menu so the lower part of the
frame aligns with the mouse y position.
Fixed by creating a variable to hold the pressed key before creating
the menu.
2020-05-02 16:41:44 +09:00

4508 lines
111 KiB
C++

/*
* PianoRoll.cpp - implementation of piano roll which is used for actual
* writing of melodies
*
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
* Copyright (c) 2008 Andrew Kelley <superjoe30/at/gmail/dot/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 <QApplication>
#include <QClipboard>
#include <QKeyEvent>
#include <QLabel>
#include <QLayout>
#include <QMdiArea>
#include <QPainter>
#include <QPointer>
#include <QScrollBar>
#include <QStyleOption>
#include <QSignalMapper>
#ifndef __USE_XOPEN
#define __USE_XOPEN
#endif
#include <math.h>
#include "AutomationEditor.h"
#include "ActionGroup.h"
#include "ConfigManager.h"
#include "PianoRoll.h"
#include "BBTrackContainer.h"
#include "Clipboard.h"
#include "ComboBox.h"
#include "debug.h"
#include "DetuningHelper.h"
#include "embed.h"
#include "GuiApplication.h"
#include "gui_templates.h"
#include "InstrumentTrack.h"
#include "MainWindow.h"
#include "Pattern.h"
#include "SongEditor.h"
#include "TextFloat.h"
#include "TimeLineWidget.h"
#if QT_VERSION < 0x040800
#define MiddleButton MidButton
#endif
typedef AutomationPattern::timeMap timeMap;
// some constants...
const int INITIAL_PIANOROLL_WIDTH = 860;
const int INITIAL_PIANOROLL_HEIGHT = 480;
const int SCROLLBAR_SIZE = 12;
const int PIANO_X = 0;
const int WHITE_KEY_WIDTH = 64;
const int BLACK_KEY_WIDTH = 41;
const int WHITE_KEY_SMALL_HEIGHT = 18;
const int WHITE_KEY_BIG_HEIGHT = 24;
const int BLACK_KEY_HEIGHT = 16;
const int C_KEY_LABEL_X = WHITE_KEY_WIDTH - 19;
const int KEY_LINE_HEIGHT = 12;
const int OCTAVE_HEIGHT = KEY_LINE_HEIGHT * KeysPerOctave; // = 12 * 12;
const int NOTE_EDIT_RESIZE_BAR = 6;
const int NOTE_EDIT_MIN_HEIGHT = 50;
const int KEY_AREA_MIN_HEIGHT = 100;
const int PR_BOTTOM_MARGIN = SCROLLBAR_SIZE;
const int PR_TOP_MARGIN = 16;
const int PR_RIGHT_MARGIN = SCROLLBAR_SIZE;
// width of area used for resizing (the grip at the end of a note)
const int RESIZE_AREA_WIDTH = 9;
// width of line for setting volume/panning of note
const int NOTE_EDIT_LINE_WIDTH = 3;
// key where to start
const int INITIAL_START_KEY = Key_C + Octave_4 * KeysPerOctave;
// number of each note to provide in quantization and note lengths
const int NUM_EVEN_LENGTHS = 6;
const int NUM_TRIPLET_LENGTHS = 5;
QPixmap * PianoRoll::s_whiteKeySmallPm = NULL;
QPixmap * PianoRoll::s_whiteKeySmallPressedPm = NULL;
QPixmap * PianoRoll::s_whiteKeyBigPm = NULL;
QPixmap * PianoRoll::s_whiteKeyBigPressedPm = NULL;
QPixmap * PianoRoll::s_blackKeyPm = NULL;
QPixmap * PianoRoll::s_blackKeyPressedPm = NULL;
QPixmap * PianoRoll::s_toolDraw = NULL;
QPixmap * PianoRoll::s_toolErase = NULL;
QPixmap * PianoRoll::s_toolSelect = NULL;
QPixmap * PianoRoll::s_toolMove = NULL;
QPixmap * PianoRoll::s_toolOpen = NULL;
TextFloat * PianoRoll::s_textFloat = NULL;
static QString s_noteStrings[12] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
static QString getNoteString( int key )
{
return s_noteStrings[key % 12] + QString::number( static_cast<int>( key / KeysPerOctave ) );
}
// used for drawing of piano
PianoRoll::PianoRollKeyTypes PianoRoll::prKeyOrder[] =
{
PR_WHITE_KEY_SMALL, PR_BLACK_KEY, PR_WHITE_KEY_BIG, PR_BLACK_KEY,
PR_WHITE_KEY_SMALL, PR_WHITE_KEY_SMALL, PR_BLACK_KEY, PR_WHITE_KEY_BIG,
PR_BLACK_KEY, PR_WHITE_KEY_BIG, PR_BLACK_KEY, PR_WHITE_KEY_SMALL
} ;
const int DEFAULT_PR_PPT = KEY_LINE_HEIGHT * DefaultStepsPerTact;
const QVector<double> PianoRoll::m_zoomLevels =
{ 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f };
PianoRoll::PianoRoll() :
m_nemStr( QVector<QString>() ),
m_noteEditMenu( NULL ),
m_semiToneMarkerMenu( NULL ),
m_zoomingModel(),
m_quantizeModel(),
m_noteLenModel(),
m_scaleModel(),
m_chordModel(),
m_pattern( NULL ),
m_currentPosition(),
m_recording( false ),
m_currentNote( NULL ),
m_action( ActionNone ),
m_noteEditMode( NoteEditVolume ),
m_moveBoundaryLeft( 0 ),
m_moveBoundaryTop( 0 ),
m_moveBoundaryRight( 0 ),
m_moveBoundaryBottom( 0 ),
m_mouseDownKey( 0 ),
m_mouseDownTick( 0 ),
m_lastMouseX( 0 ),
m_lastMouseY( 0 ),
m_oldNotesEditHeight( 100 ),
m_notesEditHeight( 100 ),
m_ppt( DEFAULT_PR_PPT ),
m_lenOfNewNotes( MidiTime( 0, DefaultTicksPerTact/4 ) ),
m_lastNoteVolume( DefaultVolume ),
m_lastNotePanning( DefaultPanning ),
m_startKey( INITIAL_START_KEY ),
m_lastKey( 0 ),
m_editMode( ModeDraw ),
m_ctrlMode( ModeDraw ),
m_mouseDownRight( false ),
m_scrollBack( false ),
m_barLineColor( 0, 0, 0 ),
m_beatLineColor( 0, 0, 0 ),
m_lineColor( 0, 0, 0 ),
m_noteModeColor( 0, 0, 0 ),
m_noteColor( 0, 0, 0 ),
m_barColor( 0, 0, 0 ),
m_selectedNoteColor( 0, 0, 0 ),
m_textColor( 0, 0, 0 ),
m_textColorLight( 0, 0, 0 ),
m_textShadow( 0, 0, 0 ),
m_markedSemitoneColor( 0, 0, 0 ),
m_noteOpacity( 255 ),
m_noteBorders( true ),
m_backgroundShade( 0, 0, 0 )
{
// gui names of edit modes
m_nemStr.push_back( tr( "Note Velocity" ) );
m_nemStr.push_back( tr( "Note Panning" ) );
QSignalMapper * signalMapper = new QSignalMapper( this );
m_noteEditMenu = new QMenu( this );
m_noteEditMenu->clear();
for( int i = 0; i < m_nemStr.size(); ++i )
{
QAction * act = new QAction( m_nemStr.at(i), this );
connect( act, SIGNAL(triggered()), signalMapper, SLOT(map()) );
signalMapper->setMapping( act, i );
m_noteEditMenu->addAction( act );
}
connect( signalMapper, SIGNAL(mapped(int)),
this, SLOT(changeNoteEditMode(int)) );
signalMapper = new QSignalMapper( this );
m_semiToneMarkerMenu = new QMenu( this );
QAction* markSemitoneAction = new QAction( tr("Mark/unmark current semitone"), this );
QAction* markAllOctaveSemitonesAction = new QAction( tr("Mark/unmark all corresponding octave semitones"), this );
QAction* markScaleAction = new QAction( tr("Mark current scale"), this );
QAction* markChordAction = new QAction( tr("Mark current chord"), this );
QAction* unmarkAllAction = new QAction( tr("Unmark all"), this );
QAction* copyAllNotesAction = new QAction( tr("Select all notes on this key"), this);
connect( markSemitoneAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
connect( markAllOctaveSemitonesAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
connect( markScaleAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
connect( markChordAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
connect( unmarkAllAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
connect( copyAllNotesAction, SIGNAL(triggered()), signalMapper, SLOT(map()) );
signalMapper->setMapping( markSemitoneAction, static_cast<int>( stmaMarkCurrentSemiTone ) );
signalMapper->setMapping( markAllOctaveSemitonesAction, static_cast<int>( stmaMarkAllOctaveSemiTones ) );
signalMapper->setMapping( markScaleAction, static_cast<int>( stmaMarkCurrentScale ) );
signalMapper->setMapping( markChordAction, static_cast<int>( stmaMarkCurrentChord ) );
signalMapper->setMapping( unmarkAllAction, static_cast<int>( stmaUnmarkAll ) );
signalMapper->setMapping( copyAllNotesAction, static_cast<int>( stmaCopyAllNotesOnKey ) );
markScaleAction->setEnabled( false );
markChordAction->setEnabled( false );
connect( this, SIGNAL(semiToneMarkerMenuScaleSetEnabled(bool)), markScaleAction, SLOT(setEnabled(bool)) );
connect( this, SIGNAL(semiToneMarkerMenuChordSetEnabled(bool)), markChordAction, SLOT(setEnabled(bool)) );
connect( signalMapper, SIGNAL(mapped(int)), this, SLOT(markSemiTone(int)) );
m_semiToneMarkerMenu->addAction( markSemitoneAction );
m_semiToneMarkerMenu->addAction( markAllOctaveSemitonesAction );
m_semiToneMarkerMenu->addAction( markScaleAction );
m_semiToneMarkerMenu->addAction( markChordAction );
m_semiToneMarkerMenu->addAction( unmarkAllAction );
m_semiToneMarkerMenu->addAction( copyAllNotesAction );
// init pixmaps
if( s_whiteKeySmallPm == NULL )
{
s_whiteKeySmallPm = new QPixmap( embed::getIconPixmap(
"pr_white_key_small" ) );
}
if( s_whiteKeySmallPressedPm == NULL )
{
s_whiteKeySmallPressedPm = new QPixmap( embed::getIconPixmap(
"pr_white_key_small_pressed" ) );
}
if( s_whiteKeyBigPm == NULL )
{
s_whiteKeyBigPm = new QPixmap( embed::getIconPixmap(
"pr_white_key_big" ) );
}
if( s_whiteKeyBigPressedPm == NULL )
{
s_whiteKeyBigPressedPm = new QPixmap( embed::getIconPixmap(
"pr_white_key_big_pressed" ) );
}
if( s_blackKeyPm == NULL )
{
s_blackKeyPm = new QPixmap( embed::getIconPixmap(
"pr_black_key" ) );
}
if( s_blackKeyPressedPm == NULL )
{
s_blackKeyPressedPm = new QPixmap( embed::getIconPixmap(
"pr_black_key_pressed" ) );
}
if( s_toolDraw == NULL )
{
s_toolDraw = new QPixmap( embed::getIconPixmap( "edit_draw" ) );
}
if( s_toolErase == NULL )
{
s_toolErase= new QPixmap( embed::getIconPixmap( "edit_erase" ) );
}
if( s_toolSelect == NULL )
{
s_toolSelect = new QPixmap( embed::getIconPixmap( "edit_select" ) );
}
if( s_toolMove == NULL )
{
s_toolMove = new QPixmap( embed::getIconPixmap( "edit_move" ) );
}
if( s_toolOpen == NULL )
{
s_toolOpen = new QPixmap( embed::getIconPixmap( "automation" ) );
}
// init text-float
if( s_textFloat == NULL )
{
s_textFloat = new TextFloat;
}
setAttribute( Qt::WA_OpaquePaintEvent, true );
// add time-line
m_timeLine = new TimeLineWidget( WHITE_KEY_WIDTH, 0, m_ppt,
Engine::getSong()->getPlayPos(
Song::Mode_PlayPattern ),
m_currentPosition, this );
connect( this, SIGNAL( positionChanged( const MidiTime & ) ),
m_timeLine, SLOT( updatePosition( const MidiTime & ) ) );
connect( m_timeLine, SIGNAL( positionChanged( const MidiTime & ) ),
this, SLOT( updatePosition( const MidiTime & ) ) );
// update timeline when in record-accompany mode
connect( Engine::getSong()->getPlayPos( Song::Mode_PlaySong ).m_timeLine,
SIGNAL( positionChanged( const MidiTime & ) ),
this,
SLOT( updatePositionAccompany( const MidiTime & ) ) );
// TODO
/* connect( engine::getSong()->getPlayPos( Song::Mode_PlayBB ).m_timeLine,
SIGNAL( positionChanged( const MidiTime & ) ),
this,
SLOT( updatePositionAccompany( const MidiTime & ) ) );*/
removeSelection();
// init scrollbars
m_leftRightScroll = new QScrollBar( Qt::Horizontal, this );
m_leftRightScroll->setSingleStep( 1 );
connect( m_leftRightScroll, SIGNAL( valueChanged( int ) ), this,
SLOT( horScrolled( int ) ) );
m_topBottomScroll = new QScrollBar( Qt::Vertical, this );
m_topBottomScroll->setSingleStep( 1 );
m_topBottomScroll->setPageStep( 20 );
connect( m_topBottomScroll, SIGNAL( valueChanged( int ) ), this,
SLOT( verScrolled( int ) ) );
// setup zooming-stuff
for( float const & zoomLevel : m_zoomLevels )
{
m_zoomingModel.addItem( QString( "%1\%" ).arg( zoomLevel * 100 ) );
}
m_zoomingModel.setValue( m_zoomingModel.findText( "100%" ) );
connect( &m_zoomingModel, SIGNAL( dataChanged() ),
this, SLOT( zoomingChanged() ) );
// Set up quantization model
m_quantizeModel.addItem( tr( "Note lock" ) );
for( int i = 0; i <= NUM_EVEN_LENGTHS; ++i )
{
m_quantizeModel.addItem( "1/" + QString::number( 1 << i ) );
}
for( int i = 0; i < NUM_TRIPLET_LENGTHS; ++i )
{
m_quantizeModel.addItem( "1/" + QString::number( (1 << i) * 3 ) );
}
m_quantizeModel.addItem( "1/192" );
m_quantizeModel.setValue( m_quantizeModel.findText( "1/16" ) );
connect( &m_quantizeModel, SIGNAL( dataChanged() ),
this, SLOT( quantizeChanged() ) );
// Set up note length model
m_noteLenModel.addItem( tr( "Last note" ),
new PixmapLoader( "edit_draw" ) );
const QString pixmaps[] = { "whole", "half", "quarter", "eighth",
"sixteenth", "thirtysecond", "triplethalf",
"tripletquarter", "tripleteighth",
"tripletsixteenth", "tripletthirtysecond" } ;
for( int i = 0; i < NUM_EVEN_LENGTHS; ++i )
{
PixmapLoader *loader = new PixmapLoader( "note_" + pixmaps[i] );
m_noteLenModel.addItem( "1/" + QString::number( 1 << i ), loader );
}
for( int i = 0; i < NUM_TRIPLET_LENGTHS; ++i )
{
PixmapLoader *loader = new PixmapLoader( "note_" + pixmaps[i+NUM_EVEN_LENGTHS] );
m_noteLenModel.addItem( "1/" + QString::number( (1 << i) * 3 ), loader );
}
m_noteLenModel.setValue( 0 );
// Note length change can cause a redraw if Q is set to lock
connect( &m_noteLenModel, SIGNAL( dataChanged() ),
this, SLOT( quantizeChanged() ) );
// Set up scale model
const InstrumentFunctionNoteStacking::ChordTable& chord_table =
InstrumentFunctionNoteStacking::ChordTable::getInstance();
m_scaleModel.addItem( tr("No scale") );
for( const InstrumentFunctionNoteStacking::Chord& chord : chord_table )
{
if( chord.isScale() )
{
m_scaleModel.addItem( chord.getName() );
}
}
m_scaleModel.setValue( 0 );
// change can update m_semiToneMarkerMenu
connect( &m_scaleModel, SIGNAL( dataChanged() ),
this, SLOT( updateSemiToneMarkerMenu() ) );
// Set up chord model
m_chordModel.addItem( tr("No chord") );
for( const InstrumentFunctionNoteStacking::Chord& chord : chord_table )
{
if( ! chord.isScale() )
{
m_chordModel.addItem( chord.getName() );
}
}
m_chordModel.setValue( 0 );
// change can update m_semiToneMarkerMenu
connect( &m_chordModel, SIGNAL( dataChanged() ),
this, SLOT( updateSemiToneMarkerMenu() ) );
setFocusPolicy( Qt::StrongFocus );
setFocus();
setMouseTracking( true );
connect( &m_scaleModel, SIGNAL( dataChanged() ),
this, SLOT( updateSemiToneMarkerMenu() ) );
connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int, int ) ),
this, SLOT( update() ) );
//connection for selecion from timeline
connect( m_timeLine, SIGNAL( regionSelectedFromPixels( int, int ) ),
this, SLOT( selectRegionFromPixels( int, int ) ) );
}
void PianoRoll::reset()
{
m_lastNoteVolume = DefaultVolume;
m_lastNotePanning = DefaultPanning;
}
void PianoRoll::showTextFloat(const QString &text, const QPoint &pos, int timeout)
{
s_textFloat->setText( text );
// show the float, offset slightly so as to not obscure anything
s_textFloat->moveGlobal( this, pos + QPoint(4, 16) );
if (timeout == -1)
{
s_textFloat->show();
}
else
{
s_textFloat->setVisibilityTimeOut( timeout );
}
}
void PianoRoll::showVolTextFloat(volume_t vol, const QPoint &pos, int timeout)
{
//! \todo display velocity for MIDI-based instruments
// possibly dBFS values too? not sure if it makes sense for note volumes...
showTextFloat( tr("Velocity: %1%").arg( vol ), pos, timeout );
}
void PianoRoll::showPanTextFloat(panning_t pan, const QPoint &pos, int timeout)
{
QString text;
if( pan < 0 )
{
text = tr("Panning: %1% left").arg( qAbs( pan ) );
}
else if( pan > 0 )
{
text = tr("Panning: %1% right").arg( qAbs( pan ) );
}
else
{
text = tr("Panning: center");
}
showTextFloat( text, pos, timeout );
}
void PianoRoll::changeNoteEditMode( int i )
{
m_noteEditMode = (NoteEditMode) i;
repaint();
}
void PianoRoll::markSemiTone( int i )
{
const int key = m_pianoKeySelected;
const InstrumentFunctionNoteStacking::Chord * chord = nullptr;
switch( static_cast<SemiToneMarkerAction>( i ) )
{
case stmaUnmarkAll:
m_markedSemiTones.clear();
break;
case stmaMarkCurrentSemiTone:
{
QList<int>::iterator it = qFind( m_markedSemiTones.begin(), m_markedSemiTones.end(), key );
if( it != m_markedSemiTones.end() )
{
m_markedSemiTones.erase( it );
}
else
{
m_markedSemiTones.push_back( key );
}
break;
}
case stmaMarkAllOctaveSemiTones:
{
QList<int> aok = getAllOctavesForKey(key);
if ( m_markedSemiTones.contains(key) )
{
// lets erase all of the ones that match this by octave
QList<int>::iterator i;
for (int ix = 0; ix < aok.size(); ++ix)
{
i = qFind(m_markedSemiTones.begin(), m_markedSemiTones.end(), aok.at(ix));
if (i != m_markedSemiTones.end())
{
m_markedSemiTones.erase(i);
}
}
}
else
{
// we should add all of the ones that match this by octave
m_markedSemiTones.append(aok);
}
break;
}
case stmaMarkCurrentScale:
chord = & InstrumentFunctionNoteStacking::ChordTable::getInstance()
.getScaleByName( m_scaleModel.currentText() );
case stmaMarkCurrentChord:
{
if( ! chord )
{
chord = & InstrumentFunctionNoteStacking::ChordTable::getInstance()
.getChordByName( m_chordModel.currentText() );
}
if( chord->isEmpty() )
{
break;
}
else if( chord->isScale() )
{
m_markedSemiTones.clear();
}
const int first = chord->isScale() ? 0 : key;
const int last = chord->isScale() ? NumKeys : key + chord->last();
const int cap = ( chord->isScale() || chord->last() == 0 ) ? KeysPerOctave : chord->last();
for( int i = first; i <= last; i++ )
{
if( chord->hasSemiTone( ( i + cap - ( key % cap ) ) % cap ) )
{
m_markedSemiTones.push_back( i );
}
}
break;
}
case stmaCopyAllNotesOnKey:
{
selectNotesOnKey();
break;
}
default:
;
}
qSort( m_markedSemiTones.begin(), m_markedSemiTones.end(), qGreater<int>() );
QList<int>::iterator new_end = std::unique( m_markedSemiTones.begin(), m_markedSemiTones.end() );
m_markedSemiTones.erase( new_end, m_markedSemiTones.end() );
}
PianoRoll::~PianoRoll()
{
}
void PianoRoll::setCurrentPattern( Pattern* newPattern )
{
if( hasValidPattern() )
{
m_pattern->instrumentTrack()->disconnect( this );
}
// force the song-editor to stop playing if it played pattern before
if( Engine::getSong()->isPlaying() &&
Engine::getSong()->playMode() == Song::Mode_PlayPattern )
{
Engine::getSong()->playPattern( NULL );
}
// set new data
m_pattern = newPattern;
m_currentPosition = 0;
m_currentNote = NULL;
m_startKey = INITIAL_START_KEY;
if( ! hasValidPattern() )
{
//resizeEvent( NULL );
update();
emit currentPatternChanged();
return;
}
m_leftRightScroll->setValue( 0 );
// determine the central key so that we can scroll to it
int central_key = 0;
int total_notes = 0;
for( const Note *note : m_pattern->notes() )
{
if( note->length() > 0 )
{
central_key += note->key();
++total_notes;
}
}
if( total_notes > 0 )
{
central_key = central_key / total_notes -
( KeysPerOctave * NumOctaves - m_totalKeysToScroll ) / 2;
m_startKey = tLimit( central_key, 0, NumOctaves * KeysPerOctave );
}
// resizeEvent() does the rest for us (scrolling, range-checking
// of start-notes and so on...)
resizeEvent( NULL );
// make sure to always get informed about the pattern being destroyed
connect( m_pattern, SIGNAL( destroyedPattern( Pattern* ) ), this, SLOT( hidePattern( Pattern* ) ) );
connect( m_pattern->instrumentTrack(), SIGNAL( midiNoteOn( const Note& ) ), this, SLOT( startRecordNote( const Note& ) ) );
connect( m_pattern->instrumentTrack(), SIGNAL( midiNoteOff( const Note& ) ), this, SLOT( finishRecordNote( const Note& ) ) );
connect( m_pattern->instrumentTrack()->pianoModel(), SIGNAL( dataChanged() ), this, SLOT( update() ) );
update();
emit currentPatternChanged();
}
void PianoRoll::hidePattern( Pattern* pattern )
{
if( m_pattern == pattern )
{
setCurrentPattern( NULL );
}
}
void PianoRoll::selectRegionFromPixels( int xStart, int xEnd )
{
xStart -= WHITE_KEY_WIDTH;
xEnd -= WHITE_KEY_WIDTH;
// select an area of notes
int pos_ticks = xStart * MidiTime::ticksPerTact() / m_ppt +
m_currentPosition;
int key_num = 0;
m_selectStartTick = pos_ticks;
m_selectedTick = 0;
m_selectStartKey = key_num;
m_selectedKeys = 1;
// change size of selection
// get tick in which the cursor is posated
pos_ticks = xEnd * MidiTime::ticksPerTact() / m_ppt +
m_currentPosition;
key_num = 120;
m_selectedTick = pos_ticks - m_selectStartTick;
if( (int) m_selectStartTick + m_selectedTick < 0 )
{
m_selectedTick = -static_cast<int>(
m_selectStartTick );
}
m_selectedKeys = key_num - m_selectStartKey;
if( key_num <= m_selectStartKey )
{
--m_selectedKeys;
}
computeSelectedNotes( false );
}
/** \brief qproperty access implementation */
QColor PianoRoll::barLineColor() const
{ return m_barLineColor; }
void PianoRoll::setBarLineColor( const QColor & c )
{ m_barLineColor = c; }
QColor PianoRoll::beatLineColor() const
{ return m_beatLineColor; }
void PianoRoll::setBeatLineColor( const QColor & c )
{ m_beatLineColor = c; }
QColor PianoRoll::lineColor() const
{ return m_lineColor; }
void PianoRoll::setLineColor( const QColor & c )
{ m_lineColor = c; }
QColor PianoRoll::noteModeColor() const
{ return m_noteModeColor; }
void PianoRoll::setNoteModeColor( const QColor & c )
{ m_noteModeColor = c; }
QColor PianoRoll::noteColor() const
{ return m_noteColor; }
void PianoRoll::setNoteColor( const QColor & c )
{ m_noteColor = c; }
QColor PianoRoll::barColor() const
{ return m_barColor; }
void PianoRoll::setBarColor( const QColor & c )
{ m_barColor = c; }
QColor PianoRoll::selectedNoteColor() const
{ return m_selectedNoteColor; }
void PianoRoll::setSelectedNoteColor( const QColor & c )
{ m_selectedNoteColor = c; }
QColor PianoRoll::textColor() const
{ return m_textColor; }
void PianoRoll::setTextColor( const QColor & c )
{ m_textColor = c; }
QColor PianoRoll::textColorLight() const
{ return m_textColorLight; }
void PianoRoll::setTextColorLight( const QColor & c )
{ m_textColorLight = c; }
QColor PianoRoll::textShadow() const
{ return m_textShadow; }
void PianoRoll::setTextShadow( const QColor & c )
{ m_textShadow = c; }
QColor PianoRoll::markedSemitoneColor() const
{ return m_markedSemitoneColor; }
void PianoRoll::setMarkedSemitoneColor( const QColor & c )
{ m_markedSemitoneColor = c; }
int PianoRoll::noteOpacity() const
{ return m_noteOpacity; }
void PianoRoll::setNoteOpacity( const int i )
{ m_noteOpacity = i; }
bool PianoRoll::noteBorders() const
{ return m_noteBorders; }
void PianoRoll::setNoteBorders( const bool b )
{ m_noteBorders = b; }
QColor PianoRoll::backgroundShade() const
{ return m_backgroundShade; }
void PianoRoll::setBackgroundShade( const QColor & c )
{ m_backgroundShade = c; }
void PianoRoll::drawNoteRect( QPainter & p, int x, int y,
int width, const Note * n, const QColor & noteCol,
const QColor & selCol, const int noteOpc, const bool borders )
{
++x;
++y;
width -= 2;
if( width <= 0 )
{
width = 2;
}
int volVal = qMin( 255, 100 + (int) ( ( (float)( n->getVolume() - MinVolume ) ) /
( (float)( MaxVolume - MinVolume ) ) * 155.0f) );
float rightPercent = qMin<float>( 1.0f,
( (float)( n->getPanning() - PanningLeft ) ) /
( (float)( PanningRight - PanningLeft ) ) * 2.0f );
float leftPercent = qMin<float>( 1.0f,
( (float)( PanningRight - n->getPanning() ) ) /
( (float)( PanningRight - PanningLeft ) ) * 2.0f );
QColor col = QColor( noteCol );
QPen pen;
if( n->selected() )
{
col = QColor( selCol );
}
const int borderWidth = borders ? 1 : 0;
const int noteHeight = KEY_LINE_HEIGHT - 1 - borderWidth;
int noteWidth = width + 1 - borderWidth;
// adjust note to make it a bit faded if it has a lower volume
// in stereo using gradients
QColor lcol = QColor::fromHsv( col.hue(), col.saturation(),
volVal * leftPercent, noteOpc );
QColor rcol = QColor::fromHsv( col.hue(), col.saturation(),
volVal * rightPercent, noteOpc );
QLinearGradient gradient( x, y, x, y + noteHeight );
gradient.setColorAt( 0, rcol );
gradient.setColorAt( 1, lcol );
p.setBrush( gradient );
if ( borders )
{
p.setPen( col );
}
else
{
p.setPen( Qt::NoPen );
}
p.drawRect( x, y, noteWidth, noteHeight );
// draw the note endmark, to hint the user to resize
p.setBrush( col );
if( width > 2 )
{
const int endmarkWidth = 3 - borderWidth;
p.drawRect( x + noteWidth - endmarkWidth, y, endmarkWidth, noteHeight );
}
}
void PianoRoll::drawDetuningInfo( QPainter & _p, const Note * _n, int _x,
int _y ) const
{
int middle_y = _y + KEY_LINE_HEIGHT / 2;
_p.setPen( noteColor() );
_p.setClipRect(WHITE_KEY_WIDTH, PR_TOP_MARGIN,
width() - WHITE_KEY_WIDTH,
keyAreaBottom() - PR_TOP_MARGIN);
int old_x = 0;
int old_y = 0;
timeMap & map = _n->detuning()->automationPattern()->getTimeMap();
for( timeMap::ConstIterator it = map.begin(); it != map.end(); ++it )
{
int pos_ticks = it.key();
int pos_x = _x + pos_ticks * m_ppt / MidiTime::ticksPerTact();
const float level = it.value();
int pos_y = middle_y - level * KEY_LINE_HEIGHT;
if( old_x != 0 && old_y != 0 )
{
switch( _n->detuning()->automationPattern()->progressionType() )
{
case AutomationPattern::DiscreteProgression:
_p.drawLine( old_x, old_y, pos_x, old_y );
_p.drawLine( pos_x, old_y, pos_x, pos_y );
break;
case AutomationPattern::CubicHermiteProgression: /* TODO */
case AutomationPattern::LinearProgression:
_p.drawLine( old_x, old_y, pos_x, pos_y );
break;
}
}
_p.drawLine( pos_x - 1, pos_y, pos_x + 1, pos_y );
_p.drawLine( pos_x, pos_y - 1, pos_x, pos_y + 1 );
old_x = pos_x;
old_y = pos_y;
}
}
void PianoRoll::removeSelection()
{
m_selectStartTick = 0;
m_selectedTick = 0;
m_selectStartKey = 0;
m_selectedKeys = 0;
}
void PianoRoll::clearSelectedNotes()
{
if( m_pattern != NULL )
{
for( Note *note : m_pattern->notes() )
{
note->setSelected( false );
}
}
}
void PianoRoll::shiftSemiTone( int amount ) // shift notes by amount semitones
{
if (!hasValidPattern()) {return;}
m_pattern->addJournalCheckPoint();
bool useAllNotes = ! isSelection();
for( Note *note : m_pattern->notes() )
{
// if none are selected, move all notes, otherwise
// only move selected notes
if( useAllNotes || note->selected() )
{
note->setKey( note->key() + amount );
}
}
m_pattern->rearrangeAllNotes();
m_pattern->dataChanged();
// we modified the song
update();
gui->songEditor()->update();
}
void PianoRoll::shiftPos( int amount ) //shift notes pos by amount
{
if (!hasValidPattern()) {return;}
m_pattern->addJournalCheckPoint();
bool useAllNotes = ! isSelection();
bool first = true;
for( Note *note : m_pattern->notes() )
{
// if none are selected, move all notes, otherwise
// only move selected notes
if( note->selected() || (useAllNotes && note->length() > 0) )
{
// don't let notes go to out of bounds
if( first )
{
m_moveBoundaryLeft = note->pos();
if( m_moveBoundaryLeft + amount < 0 )
{
amount += 0 - (amount + m_moveBoundaryLeft);
}
first = false;
}
note->setPos( note->pos() + amount );
}
}
m_pattern->rearrangeAllNotes();
m_pattern->updateLength();
m_pattern->dataChanged();
// we modified the song
update();
gui->songEditor()->update();
}
bool PianoRoll::isSelection() const // are any notes selected?
{
for( const Note *note : m_pattern->notes() )
{
if( note->selected() )
{
return true;
}
}
return false;
}
int PianoRoll::selectionCount() const // how many notes are selected?
{
int sum = 0;
for( const Note *note : m_pattern->notes() )
{
if( note->selected() )
{
++sum;
}
}
return sum;
}
void PianoRoll::keyPressEvent(QKeyEvent* ke )
{
if( hasValidPattern() && ke->modifiers() == Qt::NoModifier )
{
const int key_num = PianoView::getKeyFromKeyEvent( ke ) + ( DefaultOctave - 1 ) * KeysPerOctave;
if(! ke->isAutoRepeat() && key_num > -1)
{
m_pattern->instrumentTrack()->pianoModel()->handleKeyPress( key_num );
ke->accept();
}
}
switch( ke->key() )
{
case Qt::Key_Up:
case Qt::Key_Down:
{
int direction = (ke->key() == Qt::Key_Up ? +1 : -1);
if( ( ke->modifiers() & Qt::ControlModifier ) && m_action == ActionNone )
{
// shift selection up an octave
// if nothing selected, shift _everything_
if (hasValidPattern())
{
shiftSemiTone( 12 * direction );
}
}
else if((ke->modifiers() & Qt::ShiftModifier) && m_action == ActionNone)
{
// Move selected notes up by one semitone
if (hasValidPattern())
{
shiftSemiTone( 1 * direction );
}
}
else
{
// scroll
m_topBottomScroll->setValue( m_topBottomScroll->value() -
cm_scrollAmtVert * direction );
// if they are moving notes around or resizing,
// recalculate the note/resize position
if( m_action == ActionMoveNote ||
m_action == ActionResizeNote )
{
dragNotes( m_lastMouseX, m_lastMouseY,
ke->modifiers() & Qt::AltModifier,
ke->modifiers() & Qt::ShiftModifier,
ke->modifiers() & Qt::ControlModifier );
}
}
ke->accept();
break;
}
case Qt::Key_Right:
case Qt::Key_Left:
{
int direction = (ke->key() == Qt::Key_Right ? +1 : -1);
if( ke->modifiers() & Qt::ControlModifier && m_action == ActionNone )
{
// Move selected notes by one bar to the left
if (hasValidPattern())
{
shiftPos( direction * MidiTime::ticksPerTact() );
}
}
else if( ke->modifiers() & Qt::ShiftModifier && m_action == ActionNone)
{
// move notes
if (hasValidPattern())
{
bool quantized = ! ( ke->modifiers() & Qt::AltModifier );
int amt = quantized ? quantization() : 1;
shiftPos( direction * amt );
}
}
else if( ke->modifiers() & Qt::AltModifier)
{
// switch to editing a pattern adjacent to this one in the song editor
if (hasValidPattern())
{
Pattern * p = direction > 0 ? m_pattern->nextPattern()
: m_pattern->previousPattern();
if(p != NULL)
{
setCurrentPattern(p);
}
}
}
else
{
// scroll
m_leftRightScroll->setValue( m_leftRightScroll->value() +
direction * cm_scrollAmtHoriz );
// if they are moving notes around or resizing,
// recalculate the note/resize position
if( m_action == ActionMoveNote ||
m_action == ActionResizeNote )
{
dragNotes( m_lastMouseX, m_lastMouseY,
ke->modifiers() & Qt::AltModifier,
ke->modifiers() & Qt::ShiftModifier,
ke->modifiers() & Qt::ControlModifier );
}
}
ke->accept();
break;
}
case Qt::Key_A:
if( ke->modifiers() & Qt::ControlModifier )
{
ke->accept();
if (ke->modifiers() & Qt::ShiftModifier)
{
// Ctrl + Shift + A = deselect all notes
clearSelectedNotes();
}
else
{
// Ctrl + A = select all notes
selectAll();
}
update();
}
break;
case Qt::Key_Escape:
// Same as Ctrl + Shift + A
clearSelectedNotes();
break;
case Qt::Key_Backspace:
case Qt::Key_Delete:
deleteSelectedNotes();
ke->accept();
break;
case Qt::Key_Home:
m_timeLine->pos().setTicks( 0 );
m_timeLine->updatePosition();
ke->accept();
break;
case Qt::Key_0:
case Qt::Key_1:
case Qt::Key_2:
case Qt::Key_3:
case Qt::Key_4:
case Qt::Key_5:
case Qt::Key_6:
case Qt::Key_7:
case Qt::Key_8:
case Qt::Key_9:
{
int len = 1 + ke->key() - Qt::Key_0;
if( len == 10 )
{
len = 0;
}
if( ke->modifiers() & ( Qt::ControlModifier | Qt::KeypadModifier ) )
{
m_noteLenModel.setValue( len );
ke->accept();
}
else if( ke->modifiers() & Qt::AltModifier )
{
m_quantizeModel.setValue( len );
ke->accept();
}
break;
}
case Qt::Key_Control:
// Enter selection mode if:
// -> this window is active
// -> shift is not pressed
// (<S-C-drag> is shortcut for sticky note resize)
if ( !( ke->modifiers() & Qt::ShiftModifier ) && isActiveWindow() )
{
m_ctrlMode = m_editMode;
m_editMode = ModeSelect;
QApplication::changeOverrideCursor( Qt::ArrowCursor );
ke->accept();
}
break;
default:
break;
}
update();
}
void PianoRoll::keyReleaseEvent(QKeyEvent* ke )
{
if( hasValidPattern() && ke->modifiers() == Qt::NoModifier )
{
const int key_num = PianoView::getKeyFromKeyEvent( ke ) + ( DefaultOctave - 1 ) * KeysPerOctave;
if( ! ke->isAutoRepeat() && key_num > -1 )
{
m_pattern->instrumentTrack()->pianoModel()->handleKeyRelease( key_num );
ke->accept();
}
}
switch( ke->key() )
{
case Qt::Key_Control:
computeSelectedNotes( ke->modifiers() & Qt::ShiftModifier);
m_editMode = m_ctrlMode;
update();
break;
// update after undo/redo
case Qt::Key_Z:
case Qt::Key_R:
if( hasValidPattern() && ke->modifiers() == Qt::ControlModifier )
{
update();
}
break;
}
update();
}
void PianoRoll::leaveEvent(QEvent * e )
{
while( QApplication::overrideCursor() != NULL )
{
QApplication::restoreOverrideCursor();
}
QWidget::leaveEvent( e );
s_textFloat->hide();
update(); // cleaning inner mouse-related graphics
}
int PianoRoll::noteEditTop() const
{
return height() - PR_BOTTOM_MARGIN -
m_notesEditHeight + NOTE_EDIT_RESIZE_BAR;
}
int PianoRoll::noteEditBottom() const
{
return height() - PR_BOTTOM_MARGIN;
}
int PianoRoll::noteEditRight() const
{
return width() - PR_RIGHT_MARGIN;
}
int PianoRoll::noteEditLeft() const
{
return WHITE_KEY_WIDTH;
}
int PianoRoll::keyAreaTop() const
{
return PR_TOP_MARGIN;
}
int PianoRoll::keyAreaBottom() const
{
return height() - PR_BOTTOM_MARGIN - m_notesEditHeight;
}
void PianoRoll::mousePressEvent(QMouseEvent * me )
{
m_startedWithShift = me->modifiers() & Qt::ShiftModifier;
if( ! hasValidPattern() )
{
return;
}
if( m_editMode == ModeEditDetuning && noteUnderMouse() )
{
static QPointer<AutomationPattern> detuningPattern = nullptr;
if (detuningPattern.data() != nullptr)
{
detuningPattern->disconnect(this);
}
Note* n = noteUnderMouse();
if (n->detuning() == nullptr)
{
n->createDetuning();
}
detuningPattern = n->detuning()->automationPattern();
connect(detuningPattern.data(), SIGNAL(dataChanged()), this, SLOT(update()));
gui->automationEditor()->open(detuningPattern);
return;
}
// if holding control, go to selection mode unless shift is also pressed
if( me->modifiers() & Qt::ControlModifier && m_editMode != ModeSelect )
{
m_ctrlMode = m_editMode;
m_editMode = ModeSelect;
QApplication::changeOverrideCursor( QCursor( Qt::ArrowCursor ) );
update();
}
// keep track of the point where the user clicked down
if( me->button() == Qt::LeftButton )
{
m_moveStartX = me->x();
m_moveStartY = me->y();
}
if( me->y() > keyAreaBottom() && me->y() < noteEditTop() )
{
// resizing the note edit area
m_action = ActionResizeNoteEditArea;
m_oldNotesEditHeight = m_notesEditHeight;
return;
}
if( me->y() > PR_TOP_MARGIN )
{
bool edit_note = ( me->y() > noteEditTop() );
int key_num = getKey( me->y() );
int x = me->x();
if( x > WHITE_KEY_WIDTH )
{
// set, move or resize note
x -= WHITE_KEY_WIDTH;
// get tick in which the user clicked
int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt +
m_currentPosition;
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
// will be our iterator in the following loop
NoteVector::ConstIterator it = notes.begin()+notes.size()-1;
// loop through whole note-vector...
for( int i = 0; i < notes.size(); ++i )
{
Note *note = *it;
MidiTime len = note->length();
if( len < 0 )
{
len = 4;
}
// and check whether the user clicked on an
// existing note or an edit-line
if( pos_ticks >= note->pos() &&
len > 0 &&
(
( ! edit_note &&
pos_ticks <= note->pos() + len &&
note->key() == key_num )
||
( edit_note &&
pos_ticks <= note->pos() +
NOTE_EDIT_LINE_WIDTH * MidiTime::ticksPerTact() / m_ppt )
)
)
{
break;
}
--it;
}
// first check whether the user clicked in note-edit-
// area
if( edit_note )
{
m_pattern->addJournalCheckPoint();
// scribble note edit changes
mouseMoveEvent( me );
return;
}
// left button??
else if( me->button() == Qt::LeftButton &&
m_editMode == ModeDraw )
{
// whether this action creates new note(s) or not
bool is_new_note = false;
Note * created_new_note = NULL;
// did it reach end of vector because
// there's no note??
if( it == notes.begin()-1 )
{
is_new_note = true;
m_pattern->addJournalCheckPoint();
// then set new note
// clear selection and select this new note
clearSelectedNotes();
// +32 to quanitize the note correctly when placing notes with
// the mouse. We do this here instead of in note.quantized
// because live notes should still be quantized at the half.
MidiTime note_pos( pos_ticks - ( quantization() / 2 ) );
MidiTime note_len( newNoteLen() );
Note new_note( note_len, note_pos, key_num );
new_note.setSelected( true );
new_note.setPanning( m_lastNotePanning );
new_note.setVolume( m_lastNoteVolume );
created_new_note = m_pattern->addNote( new_note );
const InstrumentFunctionNoteStacking::Chord & chord = InstrumentFunctionNoteStacking::ChordTable::getInstance()
.getChordByName( m_chordModel.currentText() );
if( ! chord.isEmpty() )
{
// if a chord is selected, create following notes in chord
// or arpeggio mode
const bool arpeggio = me->modifiers() & Qt::ShiftModifier;
for( int i = 1; i < chord.size(); i++ )
{
if( arpeggio )
{
note_pos += note_len;
}
Note new_note( note_len, note_pos, key_num + chord[i] );
new_note.setSelected( true );
new_note.setPanning( m_lastNotePanning );
new_note.setVolume( m_lastNoteVolume );
m_pattern->addNote( new_note );
}
}
// reset it so that it can be used for
// ops (move, resize) after this
// code-block
it = notes.begin();
while( it != notes.end() && *it != created_new_note )
{
++it;
}
}
Note *current_note = *it;
m_currentNote = current_note;
m_lastNotePanning = current_note->getPanning();
m_lastNoteVolume = current_note->getVolume();
m_lenOfNewNotes = current_note->length();
// remember which key and tick we started with
m_mouseDownKey = m_startKey;
m_mouseDownTick = m_currentPosition;
bool first = true;
for( it = notes.begin(); it != notes.end(); ++it )
{
Note *note = *it;
// remember note starting positions
note->setOldKey( note->key() );
note->setOldPos( note->pos() );
note->setOldLength( note->length() );
if( note->selected() )
{
// figure out the bounding box of all the selected notes
if( first )
{
m_moveBoundaryLeft = note->pos().getTicks();
m_moveBoundaryRight = note->endPos();
m_moveBoundaryBottom = note->key();
m_moveBoundaryTop = note->key();
first = false;
}
else
{
m_moveBoundaryLeft = qMin(
note->pos().getTicks(),
(tick_t) m_moveBoundaryLeft );
m_moveBoundaryRight = qMax( (int) note->endPos(),
m_moveBoundaryRight );
m_moveBoundaryBottom = qMin( note->key(),
m_moveBoundaryBottom );
m_moveBoundaryTop = qMax( note->key(),
m_moveBoundaryTop );
}
}
}
// if clicked on an unselected note, remove selection
// and select that new note
if( ! m_currentNote->selected() )
{
clearSelectedNotes();
m_currentNote->setSelected( true );
m_moveBoundaryLeft = m_currentNote->pos().getTicks();
m_moveBoundaryRight = m_currentNote->endPos();
m_moveBoundaryBottom = m_currentNote->key();
m_moveBoundaryTop = m_currentNote->key();
}
// clicked at the "tail" of the note?
if( pos_ticks * m_ppt / MidiTime::ticksPerTact() >
m_currentNote->endPos() * m_ppt / MidiTime::ticksPerTact() - RESIZE_AREA_WIDTH
&& m_currentNote->length() > 0 )
{
m_pattern->addJournalCheckPoint();
// then resize the note
m_action = ActionResizeNote;
// set resize-cursor
QCursor c( Qt::SizeHorCursor );
QApplication::setOverrideCursor( c );
}
else
{
if( ! created_new_note )
{
m_pattern->addJournalCheckPoint();
}
// otherwise move it
m_action = ActionMoveNote;
// set move-cursor
QCursor c( Qt::SizeAllCursor );
QApplication::setOverrideCursor( c );
// if they're holding shift, copy all selected notes
if( ! is_new_note && me->modifiers() & Qt::ShiftModifier )
{
// vector to hold new notes until we're through the loop
QVector<Note> newNotes;
for( Note* const& note : notes )
{
if( note->selected() )
{
// copy this note
Note noteCopy( *note );
newNotes.push_back( noteCopy );
}
++it;
}
if( newNotes.size() != 0 )
{
//put notes from vector into piano roll
for( int i = 0; i < newNotes.size(); ++i)
{
Note * newNote = m_pattern->addNote( newNotes[i], false );
newNote->setSelected( false );
}
// added new notes, so must update engine, song, etc
Engine::getSong()->setModified();
update();
gui->songEditor()->update();
}
}
// play the note
testPlayNote( m_currentNote );
}
Engine::getSong()->setModified();
}
else if( ( me->buttons() == Qt::RightButton &&
m_editMode == ModeDraw ) ||
m_editMode == ModeErase )
{
// erase single note
m_mouseDownRight = true;
if( it != notes.begin()-1 )
{
m_pattern->addJournalCheckPoint();
m_pattern->removeNote( *it );
Engine::getSong()->setModified();
}
}
else if( me->button() == Qt::LeftButton &&
m_editMode == ModeSelect )
{
// select an area of notes
m_selectStartTick = pos_ticks;
m_selectedTick = 0;
m_selectStartKey = key_num;
m_selectedKeys = 1;
m_action = ActionSelectNotes;
// call mousemove to fix glitch where selection
// appears in wrong spot on mousedown
mouseMoveEvent( me );
}
update();
}
else if( me->y() < keyAreaBottom() )
{
// reference to last key needed for both
// right click (used for copy all keys on note)
// and for playing the key when left-clicked
m_lastKey = key_num;
// clicked on keyboard on the left
if( me->buttons() == Qt::RightButton )
{
// right click, tone marker contextual menu
m_pianoKeySelected = getKey( me->y() );
m_semiToneMarkerMenu->popup( mapToGlobal( QPoint( me->x(), me->y() ) ) );
}
else
{
// left click - play the note
int v = ( (float) x ) / ( (float) WHITE_KEY_WIDTH ) * MidiDefaultVelocity;
m_pattern->instrumentTrack()->pianoModel()->handleKeyPress( key_num, v );
}
}
else
{
if( me->buttons() == Qt::LeftButton )
{
// clicked in the box below the keys to the left of note edit area
m_noteEditMode = (NoteEditMode)(((int)m_noteEditMode)+1);
if( m_noteEditMode == NoteEditCount )
{
m_noteEditMode = (NoteEditMode) 0;
}
repaint();
}
else if( me->buttons() == Qt::RightButton )
{
// pop menu asking which one they want to edit
m_noteEditMenu->popup( mapToGlobal( QPoint( me->x(), me->y() ) ) );
}
}
}
}
void PianoRoll::mouseDoubleClickEvent(QMouseEvent * me )
{
if( ! hasValidPattern() )
{
return;
}
// if they clicked in the note edit area, enter value for the volume bar
if( me->x() > noteEditLeft() && me->x() < noteEditRight()
&& me->y() > noteEditTop() && me->y() < noteEditBottom() )
{
// get values for going through notes
int pixel_range = 4;
int x = me->x() - WHITE_KEY_WIDTH;
const int ticks_start = ( x-pixel_range/2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
const int ticks_end = ( x+pixel_range/2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
const int ticks_middle = x * MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
// go through notes to figure out which one we want to change
bool altPressed = me->modifiers() & Qt::AltModifier;
NoteVector nv;
for ( Note * i : m_pattern->notes() )
{
if( i->withinRange( ticks_start, ticks_end ) || ( i->selected() && !altPressed ) )
{
nv += i;
}
}
// make sure we're on a note
if( nv.size() > 0 )
{
const Note * closest = NULL;
int closest_dist = 9999999;
// if we caught multiple notes and we're not editing a
// selection, find the closest...
if( nv.size() > 1 && !isSelection() )
{
for ( const Note * i : nv )
{
const int dist = qAbs( i->pos().getTicks() - ticks_middle );
if( dist < closest_dist ) { closest = i; closest_dist = dist; }
}
// ... then remove all notes from the vector that aren't on the same exact time
NoteVector::Iterator it = nv.begin();
while( it != nv.end() )
{
const Note *note = *it;
if( note->pos().getTicks() != closest->pos().getTicks() )
{
it = nv.erase( it );
}
else
{
it++;
}
}
}
enterValue( &nv );
}
}
else
{
QWidget::mouseDoubleClickEvent(me);
}
}
void PianoRoll::testPlayNote( Note * n )
{
m_lastKey = n->key();
if( ! n->isPlaying() && ! m_recording )
{
n->setIsPlaying( true );
const int baseVelocity = m_pattern->instrumentTrack()->midiPort()->baseVelocity();
m_pattern->instrumentTrack()->pianoModel()->handleKeyPress( n->key(), n->midiVelocity( baseVelocity ) );
MidiEvent event( MidiMetaEvent, -1, n->key(), panningToMidi( n->getPanning() ) );
event.setMetaEvent( MidiNotePanning );
m_pattern->instrumentTrack()->processInEvent( event, 0 );
}
}
void PianoRoll::pauseTestNotes( bool pause )
{
for (Note *note : m_pattern->notes())
{
if( note->isPlaying() )
{
if( pause )
{
// stop note
m_pattern->instrumentTrack()->pianoModel()->handleKeyRelease( note->key() );
}
else
{
// start note
note->setIsPlaying( false );
testPlayNote( note );
}
}
}
}
void PianoRoll::testPlayKey( int key, int velocity, int pan )
{
// turn off old key
m_pattern->instrumentTrack()->pianoModel()->handleKeyRelease( m_lastKey );
// remember which one we're playing
m_lastKey = key;
// play new key
m_pattern->instrumentTrack()->pianoModel()->handleKeyPress( key, velocity );
}
void PianoRoll::computeSelectedNotes(bool shift)
{
if( m_selectStartTick == 0 &&
m_selectedTick == 0 &&
m_selectStartKey == 0 &&
m_selectedKeys == 0 )
{
// don't bother, there's no selection
return;
}
// setup selection-vars
int sel_pos_start = m_selectStartTick;
int sel_pos_end = m_selectStartTick+m_selectedTick;
if( sel_pos_start > sel_pos_end )
{
qSwap<int>( sel_pos_start, sel_pos_end );
}
int sel_key_start = m_selectStartKey - m_startKey + 1;
int sel_key_end = sel_key_start + m_selectedKeys;
if( sel_key_start > sel_key_end )
{
qSwap<int>( sel_key_start, sel_key_end );
}
//int y_base = noteEditTop() - 1;
if( hasValidPattern() )
{
for( Note *note : m_pattern->notes() )
{
// make a new selection unless they're holding shift
if( ! shift )
{
note->setSelected( false );
}
int len_ticks = note->length();
if( len_ticks == 0 )
{
continue;
}
else if( len_ticks < 0 )
{
len_ticks = 4;
}
const int key = note->key() - m_startKey + 1;
int pos_ticks = note->pos();
// if the selection even barely overlaps the note
if( key > sel_key_start &&
key <= sel_key_end &&
pos_ticks + len_ticks > sel_pos_start &&
pos_ticks < sel_pos_end )
{
// remove from selection when holding shift
bool selected = shift && note->selected();
note->setSelected( ! selected);
}
}
}
removeSelection();
update();
}
void PianoRoll::mouseReleaseEvent( QMouseEvent * me )
{
bool mustRepaint = false;
s_textFloat->hide();
if( me->button() & Qt::LeftButton )
{
mustRepaint = true;
if( m_action == ActionSelectNotes && m_editMode == ModeSelect )
{
// select the notes within the selection rectangle and
// then destroy the selection rectangle
computeSelectedNotes(
me->modifiers() & Qt::ShiftModifier );
}
else if( m_action == ActionMoveNote )
{
// we moved one or more notes so they have to be
// moved properly according to new starting-
// time in the note-array of pattern
m_pattern->rearrangeAllNotes();
}
if( m_action == ActionMoveNote || m_action == ActionResizeNote )
{
// if we only moved one note, deselect it so we can
// edit the notes in the note edit area
if( selectionCount() == 1 )
{
clearSelectedNotes();
}
}
}
if( me->button() & Qt::RightButton )
{
m_mouseDownRight = false;
mustRepaint = true;
}
if( hasValidPattern() )
{
// turn off all notes that are playing
for ( Note *note : m_pattern->notes() )
{
if( note->isPlaying() )
{
m_pattern->instrumentTrack()->pianoModel()->
handleKeyRelease( note->key() );
note->setIsPlaying( false );
}
}
// stop playing keys that we let go of
m_pattern->instrumentTrack()->pianoModel()->
handleKeyRelease( m_lastKey );
}
m_currentNote = NULL;
m_action = ActionNone;
if( m_editMode == ModeDraw )
{
QApplication::restoreOverrideCursor();
}
if( mustRepaint )
{
repaint();
}
}
void PianoRoll::mouseMoveEvent( QMouseEvent * me )
{
if( ! hasValidPattern() )
{
update();
return;
}
if( m_action == ActionNone && me->buttons() == 0 )
{
if( me->y() > keyAreaBottom() && me->y() < noteEditTop() )
{
QApplication::setOverrideCursor(
QCursor( Qt::SizeVerCursor ) );
return;
}
}
else if( m_action == ActionResizeNoteEditArea )
{
// change m_notesEditHeight and then repaint
m_notesEditHeight = tLimit<int>(
m_oldNotesEditHeight - ( me->y() - m_moveStartY ),
NOTE_EDIT_MIN_HEIGHT,
height() - PR_TOP_MARGIN - NOTE_EDIT_RESIZE_BAR -
PR_BOTTOM_MARGIN - KEY_AREA_MIN_HEIGHT );
repaint();
return;
}
if( me->y() > PR_TOP_MARGIN || m_action != ActionNone )
{
bool edit_note = ( me->y() > noteEditTop() )
&& m_action != ActionSelectNotes;
int key_num = getKey( me->y() );
int x = me->x();
// see if they clicked on the keyboard on the left
if( x < WHITE_KEY_WIDTH && m_action == ActionNone
&& ! edit_note && key_num != m_lastKey
&& me->buttons() & Qt::LeftButton )
{
// clicked on a key, play the note
testPlayKey( key_num, ( (float) x ) / ( (float) WHITE_KEY_WIDTH ) * MidiDefaultVelocity, 0 );
update();
return;
}
x -= WHITE_KEY_WIDTH;
if( me->buttons() & Qt::LeftButton
&& m_editMode == ModeDraw
&& (m_action == ActionMoveNote || m_action == ActionResizeNote ) )
{
// handle moving notes and resizing them
bool replay_note = key_num != m_lastKey
&& m_action == ActionMoveNote;
if( replay_note || ( m_action == ActionMoveNote && ( me->modifiers() & Qt::ShiftModifier ) && ! m_startedWithShift ) )
{
pauseTestNotes();
}
dragNotes( me->x(), me->y(),
me->modifiers() & Qt::AltModifier,
me->modifiers() & Qt::ShiftModifier,
me->modifiers() & Qt::ControlModifier );
if( replay_note && m_action == ActionMoveNote && ! ( ( me->modifiers() & Qt::ShiftModifier ) && ! m_startedWithShift ) )
{
pauseTestNotes( false );
}
}
else if( m_editMode != ModeErase &&
( edit_note || m_action == ActionChangeNoteProperty ) &&
( me->buttons() & Qt::LeftButton || me->buttons() & Qt::MiddleButton
|| ( me->buttons() & Qt::RightButton && me->modifiers() & Qt::ShiftModifier ) ) )
{
// editing note properties
// Change notes within a certain pixel range of where
// the mouse cursor is
int pixel_range = 14;
// convert to ticks so that we can check which notes
// are in the range
int ticks_start = ( x-pixel_range/2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
int ticks_end = ( x+pixel_range/2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
// determine what volume/panning to set note to
// if middle-click, set to defaults
volume_t vol = DefaultVolume;
panning_t pan = DefaultPanning;
if( me->buttons() & Qt::LeftButton )
{
vol = tLimit<int>( MinVolume +
( ( (float)noteEditBottom() ) - ( (float)me->y() ) ) /
( (float)( noteEditBottom() - noteEditTop() ) ) *
( MaxVolume - MinVolume ),
MinVolume, MaxVolume );
pan = tLimit<int>( PanningLeft +
( (float)( noteEditBottom() - me->y() ) ) /
( (float)( noteEditBottom() - noteEditTop() ) ) *
( (float)( PanningRight - PanningLeft ) ),
PanningLeft, PanningRight);
}
if( m_noteEditMode == NoteEditVolume )
{
m_lastNoteVolume = vol;
showVolTextFloat( vol, me->pos() );
}
else if( m_noteEditMode == NoteEditPanning )
{
m_lastNotePanning = pan;
showPanTextFloat( pan, me->pos() );
}
// When alt is pressed we only edit the note under the cursor
bool altPressed = me->modifiers() & Qt::AltModifier;
// We iterate from last note in pattern to the first,
// chronologically
NoteVector::ConstIterator it = notes.begin()+notes.size()-1;
for( int i = 0; i < notes.size(); ++i )
{
Note* n = *it;
bool isUnderPosition = n->withinRange( ticks_start, ticks_end );
// Play note under the cursor
if ( isUnderPosition ) { testPlayNote( n ); }
// If note is:
// Under the cursor, when there is no selection
// Selected, and alt is not pressed
// Under the cursor, selected, and alt is pressed
if ( ( isUnderPosition && !isSelection() ) ||
( n->selected() && !altPressed ) ||
( isUnderPosition && n->selected() && altPressed )
)
{
if( m_noteEditMode == NoteEditVolume )
{
n->setVolume( vol );
const int baseVelocity = m_pattern->instrumentTrack()->midiPort()->baseVelocity();
m_pattern->instrumentTrack()->processInEvent( MidiEvent( MidiKeyPressure, -1, n->key(), n->midiVelocity( baseVelocity ) ) );
}
else if( m_noteEditMode == NoteEditPanning )
{
n->setPanning( pan );
MidiEvent evt( MidiMetaEvent, -1, n->key(), panningToMidi( pan ) );
evt.setMetaEvent( MidiNotePanning );
m_pattern->instrumentTrack()->processInEvent( evt );
}
}
else if( n->isPlaying() && !isSelection() )
{
// mouse not over this note, stop playing it.
m_pattern->instrumentTrack()->pianoModel()->handleKeyRelease( n->key() );
n->setIsPlaying( false );
}
--it;
}
// Emit pattern has changed
m_pattern->dataChanged();
}
else if( me->buttons() == Qt::NoButton && m_editMode == ModeDraw )
{
// set move- or resize-cursor
// get tick in which the cursor is posated
int pos_ticks = ( x * MidiTime::ticksPerTact() ) /
m_ppt + m_currentPosition;
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
// will be our iterator in the following loop
NoteVector::ConstIterator it = notes.begin()+notes.size()-1;
// loop through whole note-vector...
for( int i = 0; i < notes.size(); ++i )
{
Note *note = *it;
// and check whether the cursor is over an
// existing note
if( pos_ticks >= note->pos() &&
pos_ticks <= note->pos() +
note->length() &&
note->key() == key_num &&
note->length() > 0 )
{
break;
}
--it;
}
// did it reach end of vector because there's
// no note??
if( it != notes.begin()-1 )
{
Note *note = *it;
// x coordinate of the right edge of the note
int noteRightX = ( note->pos() + note->length() -
m_currentPosition) * m_ppt/MidiTime::ticksPerTact();
// cursor at the "tail" of the note?
bool atTail = note->length() > 0 && x > noteRightX -
RESIZE_AREA_WIDTH;
Qt::CursorShape cursorShape = atTail ? Qt::SizeHorCursor :
Qt::SizeAllCursor;
if( QApplication::overrideCursor() )
{
if( QApplication::overrideCursor()->shape() != cursorShape )
{
while( QApplication::overrideCursor() != NULL )
{
QApplication::restoreOverrideCursor();
}
QApplication::setOverrideCursor(QCursor(cursorShape));
}
}
else
{
QApplication::setOverrideCursor(QCursor(cursorShape));
}
}
else
{
// the cursor is over no note, so restore cursor
while( QApplication::overrideCursor() != NULL )
{
QApplication::restoreOverrideCursor();
}
}
}
else if( me->buttons() & Qt::LeftButton &&
m_editMode == ModeSelect &&
m_action == ActionSelectNotes )
{
// change size of selection
// get tick in which the cursor is posated
int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt +
m_currentPosition;
m_selectedTick = pos_ticks - m_selectStartTick;
if( (int) m_selectStartTick + m_selectedTick < 0 )
{
m_selectedTick = -static_cast<int>(
m_selectStartTick );
}
m_selectedKeys = key_num - m_selectStartKey;
if( key_num <= m_selectStartKey )
{
--m_selectedKeys;
}
}
else if( ( m_editMode == ModeDraw && me->buttons() & Qt::RightButton )
|| ( m_editMode == ModeErase && me->buttons() ) )
{
// holding down right-click to delete notes or holding down
// any key if in erase mode
// get tick in which the user clicked
int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt +
m_currentPosition;
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
// will be our iterator in the following loop
NoteVector::ConstIterator it = notes.begin();
// loop through whole note-vector...
while( it != notes.end() )
{
Note *note = *it;
MidiTime len = note->length();
if( len < 0 )
{
len = 4;
}
// and check whether the user clicked on an
// existing note or an edit-line
if( pos_ticks >= note->pos() &&
len > 0 &&
(
( ! edit_note &&
pos_ticks <= note->pos() + len &&
note->key() == key_num )
||
( edit_note &&
pos_ticks <= note->pos() +
NOTE_EDIT_LINE_WIDTH *
MidiTime::ticksPerTact() /
m_ppt )
)
)
{
// delete this note
m_pattern->removeNote( note );
Engine::getSong()->setModified();
}
else
{
++it;
}
}
}
}
else
{
if( me->buttons() & Qt::LeftButton &&
m_editMode == ModeSelect &&
m_action == ActionSelectNotes )
{
int x = me->x() - WHITE_KEY_WIDTH;
if( x < 0 && m_currentPosition > 0 )
{
x = 0;
QCursor::setPos( mapToGlobal( QPoint(
WHITE_KEY_WIDTH,
me->y() ) ) );
if( m_currentPosition >= 4 )
{
m_leftRightScroll->setValue(
m_currentPosition - 4 );
}
else
{
m_leftRightScroll->setValue( 0 );
}
}
else if( x > width() - WHITE_KEY_WIDTH )
{
x = width() - WHITE_KEY_WIDTH;
QCursor::setPos( mapToGlobal( QPoint( width(),
me->y() ) ) );
m_leftRightScroll->setValue( m_currentPosition +
4 );
}
// get tick in which the cursor is posated
int pos_ticks = x * MidiTime::ticksPerTact()/ m_ppt +
m_currentPosition;
m_selectedTick = pos_ticks -
m_selectStartTick;
if( (int) m_selectStartTick + m_selectedTick <
0 )
{
m_selectedTick = -static_cast<int>(
m_selectStartTick );
}
int key_num = getKey( me->y() );
int visible_keys = ( height() - PR_TOP_MARGIN -
PR_BOTTOM_MARGIN -
m_notesEditHeight ) /
KEY_LINE_HEIGHT + 2;
const int s_key = m_startKey - 1;
if( key_num <= s_key )
{
QCursor::setPos( mapToGlobal( QPoint( me->x(),
keyAreaBottom() ) ) );
m_topBottomScroll->setValue(
m_topBottomScroll->value() + 1 );
key_num = s_key;
}
else if( key_num >= s_key + visible_keys )
{
QCursor::setPos( mapToGlobal( QPoint( me->x(),
PR_TOP_MARGIN ) ) );
m_topBottomScroll->setValue(
m_topBottomScroll->value() - 1 );
key_num = s_key + visible_keys;
}
m_selectedKeys = key_num - m_selectStartKey;
if( key_num <= m_selectStartKey )
{
--m_selectedKeys;
}
}
QApplication::restoreOverrideCursor();
}
m_lastMouseX = me->x();
m_lastMouseY = me->y();
update();
}
void PianoRoll::dragNotes( int x, int y, bool alt, bool shift, bool ctrl )
{
// dragging one or more notes around
// convert pixels to ticks and keys
int off_x = x - m_moveStartX;
int off_ticks = off_x * MidiTime::ticksPerTact() / m_ppt;
int off_key = getKey( y ) - getKey( m_moveStartY );
// handle scroll changes while dragging
off_ticks -= m_mouseDownTick - m_currentPosition;
off_key -= m_mouseDownKey - m_startKey;
// if they're not holding alt, quantize the offset
if( ! alt )
{
off_ticks = floor( off_ticks / quantization() )
* quantization();
}
// make sure notes won't go outside boundary conditions
if( m_action == ActionMoveNote && ! ( shift && ! m_startedWithShift ) )
{
if( m_moveBoundaryLeft + off_ticks < 0 )
{
off_ticks -= (off_ticks + m_moveBoundaryLeft);
}
if( m_moveBoundaryTop + off_key > NumKeys )
{
off_key -= NumKeys - (m_moveBoundaryTop + off_key);
}
if( m_moveBoundaryBottom + off_key < 0 )
{
off_key -= (m_moveBoundaryBottom + off_key);
}
}
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
if (m_action == ActionMoveNote)
{
for (Note *note : notes)
{
if( note->selected() )
{
if( shift && ! m_startedWithShift )
{
// quick resize, toggled by holding shift after starting a note move, but not before
int ticks_new = note->oldLength().getTicks() + off_ticks;
if( ticks_new <= 0 )
{
ticks_new = 1;
}
note->setLength( MidiTime( ticks_new ) );
m_lenOfNewNotes = note->length();
}
else
{
// moving note
int pos_ticks = note->oldPos().getTicks() + off_ticks;
int key_num = note->oldKey() + off_key;
// ticks can't be negative
pos_ticks = qMax(0, pos_ticks);
// upper/lower bound checks on key_num
key_num = qMax(0, key_num);
key_num = qMin(key_num, NumKeys);
note->setPos( MidiTime( pos_ticks ) );
note->setKey( key_num );
}
}
}
}
else if (m_action == ActionResizeNote)
{
// When resizing notes:
// If shift is not pressed, resize the selected notes but do not rearrange them
// If shift is pressed we resize and rearrange only the selected notes
// If shift + ctrl then we also rearrange all posterior notes (sticky)
// If shift is pressed but only one note is selected, apply sticky
if (shift)
{
// Algorithm:
// Relative to the starting point of the left-most selected note,
// all selected note start-points and *endpoints* (not length) should be scaled by a calculated factor.
// This factor is such that the endpoint of the note whose handle is being dragged should lie under the cursor.
// first, determine the start-point of the left-most selected note:
int stretchStartTick = -1;
for (const Note *note : notes)
{
if (note->selected() && (stretchStartTick < 0 || note->oldPos().getTicks() < stretchStartTick))
{
stretchStartTick = note->oldPos().getTicks();
}
}
// determine the ending tick of the right-most selected note
const Note *posteriorNote = nullptr;
for (const Note *note : notes)
{
if (note->selected() && (posteriorNote == nullptr ||
note->oldPos().getTicks() + note->oldLength().getTicks() >
posteriorNote->oldPos().getTicks() + posteriorNote->oldLength().getTicks()))
{
posteriorNote = note;
}
}
int posteriorEndTick = posteriorNote->pos().getTicks() + posteriorNote->length().getTicks();
// end-point of the note whose handle is being dragged:
int stretchEndTick = m_currentNote->oldPos().getTicks() + m_currentNote->oldLength().getTicks();
// Calculate factor by which to scale the start-point and end-point of all selected notes
float scaleFactor = (float)(stretchEndTick - stretchStartTick + off_ticks) / qMax(1, stretchEndTick - stretchStartTick);
scaleFactor = qMax(0.0f, scaleFactor);
// process all selected notes & determine how much the endpoint of the right-most note was shifted
int posteriorDeltaThisFrame = 0;
for (Note *note : notes)
{
if(note->selected())
{
// scale relative start and end positions by scaleFactor
int newStart = stretchStartTick + scaleFactor *
(note->oldPos().getTicks() - stretchStartTick);
int newEnd = stretchStartTick + scaleFactor *
(note->oldPos().getTicks()+note->oldLength().getTicks() - stretchStartTick);
// if not holding alt, quantize the offsets
if(!alt)
{
// quantize start time
int oldStart = note->oldPos().getTicks();
int startDiff = newStart - oldStart;
startDiff = floor(startDiff / quantization()) * quantization();
newStart = oldStart + startDiff;
// quantize end time
int oldEnd = oldStart + note->oldLength().getTicks();
int endDiff = newEnd - oldEnd;
endDiff = floor(endDiff / quantization()) * quantization();
newEnd = oldEnd + endDiff;
}
int newLength = qMax(1, newEnd-newStart);
if (note == posteriorNote)
{
posteriorDeltaThisFrame = (newStart+newLength) -
(note->pos().getTicks() + note->length().getTicks());
}
note->setLength( MidiTime(newLength) );
note->setPos( MidiTime(newStart) );
m_lenOfNewNotes = note->length();
}
}
if (ctrl || selectionCount() == 1)
{
// if holding ctrl or only one note is selected, reposition posterior notes
for (Note *note : notes)
{
if (!note->selected() && note->pos().getTicks() >= posteriorEndTick)
{
int newStart = note->pos().getTicks() + posteriorDeltaThisFrame;
note->setPos( MidiTime(newStart) );
}
}
}
}
else
{
// shift is not pressed; stretch length of selected notes but not their position
for (Note *note : notes)
{
if (note->selected())
{
int newLength = note->oldLength() + off_ticks;
newLength = qMax(1, newLength);
note->setLength( MidiTime(newLength) );
m_lenOfNewNotes = note->length();
}
}
}
}
m_pattern->updateLength();
m_pattern->dataChanged();
Engine::getSong()->setModified();
}
int PianoRoll::xCoordOfTick(int tick )
{
return WHITE_KEY_WIDTH + ( ( tick - m_currentPosition )
* m_ppt / MidiTime::ticksPerTact() );
}
void PianoRoll::paintEvent(QPaintEvent * pe )
{
bool drawNoteNames = ConfigManager::inst()->value( "ui", "printnotelabels").toInt();
QStyleOption opt;
opt.initFrom( this );
QPainter p( this );
style()->drawPrimitive( QStyle::PE_Widget, &opt, &p, this );
QBrush bgColor = p.background();
// fill with bg color
p.fillRect( 0, 0, width(), height(), bgColor );
// set font-size to 8
p.setFont( pointSize<8>( p.font() ) );
// y_offset is used to align the piano-keys on the key-lines
int y_offset = 0;
// calculate y_offset according to first key
switch( prKeyOrder[m_startKey % KeysPerOctave] )
{
case PR_BLACK_KEY: y_offset = KEY_LINE_HEIGHT / 4; break;
case PR_WHITE_KEY_BIG: y_offset = KEY_LINE_HEIGHT / 2; break;
case PR_WHITE_KEY_SMALL:
if( prKeyOrder[( ( m_startKey + 1 ) %
KeysPerOctave)] != PR_BLACK_KEY )
{
y_offset = KEY_LINE_HEIGHT / 2;
}
break;
}
// start drawing at the bottom
int key_line_y = keyAreaBottom() - 1;
// used for aligning black-keys later
int first_white_key_height = WHITE_KEY_SMALL_HEIGHT;
// key-counter - only needed for finding out whether the processed
// key is the first one
int keys_processed = 0;
int key = m_startKey;
// draw all white keys...
for( int y = key_line_y + 1 + y_offset; y > PR_TOP_MARGIN;
key_line_y -= KEY_LINE_HEIGHT, ++keys_processed )
{
// check for white key that is only half visible on the
// bottom of piano-roll
if( keys_processed == 0 &&
prKeyOrder[m_startKey % KeysPerOctave] ==
PR_BLACK_KEY )
{
// draw it!
p.drawPixmap( PIANO_X, y - WHITE_KEY_SMALL_HEIGHT,
*s_whiteKeySmallPm );
// update y-pos
y -= WHITE_KEY_SMALL_HEIGHT / 2;
// move first black key down (we didn't draw whole
// white key so black key needs to be lifted down)
// (default for first_white_key_height =
// WHITE_KEY_SMALL_HEIGHT, so WHITE_KEY_SMALL_HEIGHT/2
// is smaller)
first_white_key_height = WHITE_KEY_SMALL_HEIGHT / 2;
}
// check whether to draw a big or a small white key
if( prKeyOrder[key % KeysPerOctave] == PR_WHITE_KEY_SMALL )
{
// draw a small one while checking if it is pressed or not
if( hasValidPattern() && m_pattern->instrumentTrack()->pianoModel()->isKeyPressed( key ) )
{
p.drawPixmap( PIANO_X, y - WHITE_KEY_SMALL_HEIGHT, *s_whiteKeySmallPressedPm );
}
else
{
p.drawPixmap( PIANO_X, y - WHITE_KEY_SMALL_HEIGHT, *s_whiteKeySmallPm );
}
// update y-pos
y -= WHITE_KEY_SMALL_HEIGHT;
}
else if( prKeyOrder[key % KeysPerOctave] == PR_WHITE_KEY_BIG )
{
// draw a big one while checking if it is pressed or not
if( hasValidPattern() && m_pattern->instrumentTrack()->pianoModel()->isKeyPressed( key ) )
{
p.drawPixmap( PIANO_X, y - WHITE_KEY_BIG_HEIGHT, *s_whiteKeyBigPressedPm );
}
else
{
p.drawPixmap( PIANO_X, y-WHITE_KEY_BIG_HEIGHT, *s_whiteKeyBigPm );
}
// if a big white key has been the first key,
// black keys needs to be lifted up
if( keys_processed == 0 )
{
first_white_key_height = WHITE_KEY_BIG_HEIGHT;
}
// update y-pos
y -= WHITE_KEY_BIG_HEIGHT;
}
// Compute the corrections for the note names
int yCorrectionForNoteLabels = 0;
int keyCode = key % KeysPerOctave;
switch( keyCode )
{
case 0:
case 5:
yCorrectionForNoteLabels = -4;
break;
case 2:
case 7:
case 9:
yCorrectionForNoteLabels = -2;
break;
case 4:
case 11:
yCorrectionForNoteLabels = 2;
break;
}
if( Piano::isWhiteKey( key ) )
{
// Draw note names if activated in the preferences, C notes are always drawn
if ( key % 12 == 0 || drawNoteNames )
{
QString noteString = getNoteString( key );
QPoint textStart( WHITE_KEY_WIDTH - 18, key_line_y );
textStart += QPoint( 0, yCorrectionForNoteLabels );
p.setPen( textShadow() );
p.drawText( textStart + QPoint( 1, 1 ), noteString );
// The C key is painted darker than the other ones
if ( key % 12 == 0 )
{
p.setPen( textColor() );
}
else
{
p.setPen( textColorLight() );
}
p.drawText( textStart, noteString );
}
}
++key;
}
// reset all values, because now we're going to draw all black keys
key = m_startKey;
keys_processed = 0;
int white_cnt = 0;
// and go!
for( int y = keyAreaBottom() + y_offset;
y > PR_TOP_MARGIN; ++keys_processed )
{
// check for black key that is only half visible on the bottom
// of piano-roll
if( keys_processed == 0
// current key may not be a black one
&& prKeyOrder[key % KeysPerOctave] != PR_BLACK_KEY
// but the previous one must be black (we must check this
// because there might be two white keys (E-F)
&& prKeyOrder[( key - 1 ) % KeysPerOctave] ==
PR_BLACK_KEY )
{
// draw the black key!
p.drawPixmap( PIANO_X, y - BLACK_KEY_HEIGHT / 2,
*s_blackKeyPm );
// is the one after the start-note a black key??
if( prKeyOrder[( key + 1 ) % KeysPerOctave] !=
PR_BLACK_KEY )
{
// no, then move it up!
y -= KEY_LINE_HEIGHT / 2;
}
}
// current key black?
if( prKeyOrder[key % KeysPerOctave] == PR_BLACK_KEY)
{
// then draw it (calculation of y very complicated,
// but that's the only working solution, sorry...)
// check if the key is pressed or not
if( hasValidPattern() && m_pattern->instrumentTrack()->pianoModel()->isKeyPressed( key ) )
{
p.drawPixmap( PIANO_X, y - ( first_white_key_height -
WHITE_KEY_SMALL_HEIGHT ) -
WHITE_KEY_SMALL_HEIGHT/2 - 1 -
BLACK_KEY_HEIGHT, *s_blackKeyPressedPm );
}
else
{
p.drawPixmap( PIANO_X, y - ( first_white_key_height -
WHITE_KEY_SMALL_HEIGHT ) -
WHITE_KEY_SMALL_HEIGHT/2 - 1 -
BLACK_KEY_HEIGHT, *s_blackKeyPm );
}
// update y-pos
y -= WHITE_KEY_BIG_HEIGHT;
// reset white-counter
white_cnt = 0;
}
else
{
// simple workaround for increasing x if there were
// two white keys (e.g. between E and F)
++white_cnt;
if( white_cnt > 1 )
{
y -= WHITE_KEY_BIG_HEIGHT/2;
}
}
++key;
}
// erase the area below the piano, because there might be keys that
// should be only half-visible
p.fillRect( QRect( 0, keyAreaBottom(),
WHITE_KEY_WIDTH, noteEditBottom() - keyAreaBottom() ), bgColor );
// display note editing info
QFont f = p.font();
f.setBold( false );
p.setFont( pointSize<10>( f ) );
p.setPen( noteModeColor() );
p.drawText( QRect( 0, keyAreaBottom(),
WHITE_KEY_WIDTH, noteEditBottom() - keyAreaBottom() ),
Qt::AlignCenter | Qt::TextWordWrap,
m_nemStr.at( m_noteEditMode ) + ":" );
// set clipping area, because we are not allowed to paint over
// keyboard...
p.setClipRect( WHITE_KEY_WIDTH, PR_TOP_MARGIN,
width() - WHITE_KEY_WIDTH,
height() - PR_TOP_MARGIN - PR_BOTTOM_MARGIN );
// draw the grid
if( hasValidPattern() )
{
int q, x, tick;
if( m_zoomingModel.value() > 3 )
{
// If we're over 100% zoom, we allow all quantization level grids
q = quantization();
}
else if( quantization() % 3 != 0 )
{
// If we're under 100% zoom, we allow quantization grid up to 1/24 for triplets
// to ensure a dense doesn't fill out the background
q = quantization() < 8 ? 8 : quantization();
}
else {
// If we're under 100% zoom, we allow quantization grid up to 1/32 for normal notes
q = quantization() < 6 ? 6 : quantization();
}
// First we draw the vertical quantization lines
for( tick = m_currentPosition - m_currentPosition % q, x = xCoordOfTick( tick );
x <= width(); tick += q, x = xCoordOfTick( tick ) )
{
p.setPen( lineColor() );
p.drawLine( x, PR_TOP_MARGIN, x, height() - PR_BOTTOM_MARGIN );
}
// Draw horizontal lines
key = m_startKey;
for( int y = keyAreaBottom() - 1; y > PR_TOP_MARGIN;
y -= KEY_LINE_HEIGHT )
{
if( static_cast<Keys>( key % KeysPerOctave ) == Key_C )
{
// C note gets accented
p.setPen( beatLineColor() );
}
else
{
p.setPen( lineColor() );
}
p.drawLine( WHITE_KEY_WIDTH, y, width(), y );
++key;
}
// Draw alternating shades on bars
float timeSignature = static_cast<float>( Engine::getSong()->getTimeSigModel().getNumerator() )
/ static_cast<float>( Engine::getSong()->getTimeSigModel().getDenominator() );
float zoomFactor = m_zoomLevels[m_zoomingModel.value()];
//the bars which disappears at the left side by scrolling
int leftBars = m_currentPosition * zoomFactor / MidiTime::ticksPerTact();
//iterates the visible bars and draw the shading on uneven bars
for( int x = WHITE_KEY_WIDTH, barCount = leftBars; x < width() + m_currentPosition * zoomFactor / timeSignature; x += m_ppt, ++barCount )
{
if( ( barCount + leftBars ) % 2 != 0 )
{
p.fillRect( x - m_currentPosition * zoomFactor / timeSignature, PR_TOP_MARGIN, m_ppt,
height() - ( PR_BOTTOM_MARGIN + PR_TOP_MARGIN ), backgroundShade() );
}
}
// Draw the vertical beat lines
int ticksPerBeat = DefaultTicksPerTact /
Engine::getSong()->getTimeSigModel().getDenominator();
for( tick = m_currentPosition - m_currentPosition % ticksPerBeat,
x = xCoordOfTick( tick ); x <= width();
tick += ticksPerBeat, x = xCoordOfTick( tick ) )
{
p.setPen( beatLineColor() );
p.drawLine( x, PR_TOP_MARGIN, x, height() - PR_BOTTOM_MARGIN );
}
// Draw the vertical bar lines
for( tick = m_currentPosition - m_currentPosition % MidiTime::ticksPerTact(),
x = xCoordOfTick( tick ); x <= width();
tick += MidiTime::ticksPerTact(), x = xCoordOfTick( tick ) )
{
p.setPen( barLineColor() );
p.drawLine( x, PR_TOP_MARGIN, x, height() - PR_BOTTOM_MARGIN );
}
// draw marked semitones after the grid
for( int i = 0; i < m_markedSemiTones.size(); i++ )
{
const int key_num = m_markedSemiTones.at( i );
const int y = keyAreaBottom() + 5
- KEY_LINE_HEIGHT * ( key_num - m_startKey + 1 );
if( y > keyAreaBottom() )
{
break;
}
p.fillRect( WHITE_KEY_WIDTH + 1, y - KEY_LINE_HEIGHT / 2, width() - 10, KEY_LINE_HEIGHT + 1,
markedSemitoneColor() );
}
}
// following code draws all notes in visible area
// and the note editing stuff (volume, panning, etc)
// setup selection-vars
int sel_pos_start = m_selectStartTick;
int sel_pos_end = m_selectStartTick+m_selectedTick;
if( sel_pos_start > sel_pos_end )
{
qSwap<int>( sel_pos_start, sel_pos_end );
}
int sel_key_start = m_selectStartKey - m_startKey + 1;
int sel_key_end = sel_key_start + m_selectedKeys;
if( sel_key_start > sel_key_end )
{
qSwap<int>( sel_key_start, sel_key_end );
}
int y_base = keyAreaBottom() - 1;
if( hasValidPattern() )
{
p.setClipRect( WHITE_KEY_WIDTH, PR_TOP_MARGIN,
width() - WHITE_KEY_WIDTH,
height() - PR_TOP_MARGIN );
const int visible_keys = ( keyAreaBottom()-keyAreaTop() ) /
KEY_LINE_HEIGHT + 2;
QPolygonF editHandles;
for( const Note *note : m_pattern->notes() )
{
int len_ticks = note->length();
if( len_ticks == 0 )
{
continue;
}
else if( len_ticks < 0 )
{
len_ticks = 4;
}
const int key = note->key() - m_startKey + 1;
int pos_ticks = note->pos();
int note_width = len_ticks * m_ppt / MidiTime::ticksPerTact();
const int x = ( pos_ticks - m_currentPosition ) *
m_ppt / MidiTime::ticksPerTact();
// skip this note if not in visible area at all
if( !( x + note_width >= 0 && x <= width() - WHITE_KEY_WIDTH ) )
{
continue;
}
// is the note in visible area?
if( key > 0 && key <= visible_keys )
{
// we've done and checked all, let's draw the
// note
drawNoteRect( p, x + WHITE_KEY_WIDTH,
y_base - key * KEY_LINE_HEIGHT,
note_width, note, noteColor(), selectedNoteColor(),
noteOpacity(), noteBorders() );
}
// draw note editing stuff
int editHandleTop = 0;
if( m_noteEditMode == NoteEditVolume )
{
QColor color = barColor().lighter( 30 + ( note->getVolume() * 90 / MaxVolume ) );
if( note->selected() )
{
color = selectedNoteColor();
}
p.setPen( QPen( color, NOTE_EDIT_LINE_WIDTH ) );
editHandleTop = noteEditBottom() -
( (float)( note->getVolume() - MinVolume ) ) /
( (float)( MaxVolume - MinVolume ) ) *
( (float)( noteEditBottom() - noteEditTop() ) );
p.drawLine( QLineF ( noteEditLeft() + x + 0.5, editHandleTop + 0.5,
noteEditLeft() + x + 0.5, noteEditBottom() + 0.5 ) );
}
else if( m_noteEditMode == NoteEditPanning )
{
QColor color = noteColor();
if( note->selected() )
{
color = selectedNoteColor();
}
p.setPen( QPen( color, NOTE_EDIT_LINE_WIDTH ) );
editHandleTop = noteEditBottom() -
( (float)( note->getPanning() - PanningLeft ) ) /
( (float)( (PanningRight - PanningLeft ) ) ) *
( (float)( noteEditBottom() - noteEditTop() ) );
p.drawLine( QLine( noteEditLeft() + x, noteEditTop() +
( (float)( noteEditBottom() - noteEditTop() ) ) / 2.0f,
noteEditLeft() + x , editHandleTop ) );
}
editHandles << QPoint ( x + noteEditLeft(),
editHandleTop );
if( note->hasDetuningInfo() )
{
drawDetuningInfo( p, note,
x + WHITE_KEY_WIDTH,
y_base - key * KEY_LINE_HEIGHT );
p.setClipRect(WHITE_KEY_WIDTH, PR_TOP_MARGIN,
width() - WHITE_KEY_WIDTH,
height() - PR_TOP_MARGIN);
}
}
p.setPen( QPen( noteColor(), NOTE_EDIT_LINE_WIDTH + 2 ) );
p.drawPoints( editHandles );
}
else
{
QFont f = p.font();
f.setBold( true );
p.setFont( pointSize<14>( f ) );
p.setPen( QApplication::palette().color( QPalette::Active,
QPalette::BrightText ) );
p.drawText( WHITE_KEY_WIDTH + 20, PR_TOP_MARGIN + 40,
tr( "Please open a pattern by double-clicking "
"on it!" ) );
}
p.setClipRect( WHITE_KEY_WIDTH, PR_TOP_MARGIN, width() -
WHITE_KEY_WIDTH, height() - PR_TOP_MARGIN -
m_notesEditHeight - PR_BOTTOM_MARGIN );
// now draw selection-frame
int x = ( ( sel_pos_start - m_currentPosition ) * m_ppt ) /
MidiTime::ticksPerTact();
int w = ( ( ( sel_pos_end - m_currentPosition ) * m_ppt ) /
MidiTime::ticksPerTact() ) - x;
int y = (int) y_base - sel_key_start * KEY_LINE_HEIGHT;
int h = (int) y_base - sel_key_end * KEY_LINE_HEIGHT - y;
p.setPen( selectedNoteColor() );
p.setBrush( Qt::NoBrush );
p.drawRect( x + WHITE_KEY_WIDTH, y, w, h );
// TODO: Get this out of paint event
int l = ( hasValidPattern() )? (int) m_pattern->length() : 0;
// reset scroll-range
if( m_leftRightScroll->maximum() != l )
{
m_leftRightScroll->setRange( 0, l );
m_leftRightScroll->setPageStep( l );
}
// set line colors
QColor editAreaCol = QColor( lineColor() );
QColor currentKeyCol = QColor( beatLineColor() );
editAreaCol.setAlpha( 64 );
currentKeyCol.setAlpha( 64 );
// horizontal line for the key under the cursor
if( hasValidPattern() )
{
int key_num = getKey( mapFromGlobal( QCursor::pos() ).y() );
p.fillRect( 10, keyAreaBottom() + 3 - KEY_LINE_HEIGHT *
( key_num - m_startKey + 1 ), width() - 10, KEY_LINE_HEIGHT - 7, currentKeyCol );
}
// bar to resize note edit area
p.setClipRect( 0, 0, width(), height() );
p.fillRect( QRect( 0, keyAreaBottom(),
width()-PR_RIGHT_MARGIN, NOTE_EDIT_RESIZE_BAR ), editAreaCol );
const QPixmap * cursor = NULL;
// draw current edit-mode-icon below the cursor
switch( m_editMode )
{
case ModeDraw:
if( m_mouseDownRight )
{
cursor = s_toolErase;
}
else if( m_action == ActionMoveNote )
{
cursor = s_toolMove;
}
else
{
cursor = s_toolDraw;
}
break;
case ModeErase: cursor = s_toolErase; break;
case ModeSelect: cursor = s_toolSelect; break;
case ModeEditDetuning: cursor = s_toolOpen; break;
}
QPoint mousePosition = mapFromGlobal( QCursor::pos() );
if( cursor != NULL && mousePosition.y() > keyAreaTop() && mousePosition.x() > noteEditLeft())
{
p.drawPixmap( mousePosition + QPoint( 8, 8 ), *cursor );
}
}
// responsible for moving/resizing scrollbars after window-resizing
void PianoRoll::resizeEvent(QResizeEvent * re)
{
m_leftRightScroll->setGeometry( WHITE_KEY_WIDTH,
height() -
SCROLLBAR_SIZE,
width()-WHITE_KEY_WIDTH,
SCROLLBAR_SIZE );
m_topBottomScroll->setGeometry( width() - SCROLLBAR_SIZE, PR_TOP_MARGIN,
SCROLLBAR_SIZE,
height() - PR_TOP_MARGIN -
SCROLLBAR_SIZE );
int total_pixels = OCTAVE_HEIGHT * NumOctaves - ( height() -
PR_TOP_MARGIN - PR_BOTTOM_MARGIN -
m_notesEditHeight );
m_totalKeysToScroll = total_pixels * KeysPerOctave / OCTAVE_HEIGHT;
m_topBottomScroll->setRange( 0, m_totalKeysToScroll );
if( m_startKey > m_totalKeysToScroll )
{
m_startKey = m_totalKeysToScroll;
}
m_topBottomScroll->setValue( m_totalKeysToScroll - m_startKey );
Engine::getSong()->getPlayPos( Song::Mode_PlayPattern
).m_timeLine->setFixedWidth( width() );
update();
}
void PianoRoll::wheelEvent(QWheelEvent * we )
{
we->accept();
// handle wheel events for note edit area - for editing note vol/pan with mousewheel
if( we->x() > noteEditLeft() && we->x() < noteEditRight()
&& we->y() > noteEditTop() && we->y() < noteEditBottom() )
{
if (!hasValidPattern()) {return;}
// get values for going through notes
int pixel_range = 8;
int x = we->x() - WHITE_KEY_WIDTH;
int ticks_start = ( x - pixel_range / 2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
int ticks_end = ( x + pixel_range / 2 ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
// When alt is pressed we only edit the note under the cursor
bool altPressed = we->modifiers() & Qt::AltModifier;
// go through notes to figure out which one we want to change
NoteVector nv;
for ( Note * i : m_pattern->notes() )
{
if( i->withinRange( ticks_start, ticks_end ) || ( i->selected() && !altPressed ) )
{
nv += i;
}
}
if( nv.size() > 0 )
{
const int step = we->delta() > 0 ? 1.0 : -1.0;
if( m_noteEditMode == NoteEditVolume )
{
for ( Note * n : nv )
{
volume_t vol = tLimit<int>( n->getVolume() + step, MinVolume, MaxVolume );
n->setVolume( vol );
}
bool allVolumesEqual = std::all_of( nv.begin(), nv.end(),
[nv](const Note *note)
{
return note->getVolume() == nv[0]->getVolume();
});
if ( allVolumesEqual )
{
// show the volume hover-text only if all notes have the
// same volume
showVolTextFloat( nv[0]->getVolume(), we->pos(), 1000 );
}
}
else if( m_noteEditMode == NoteEditPanning )
{
for ( Note * n : nv )
{
panning_t pan = tLimit<int>( n->getPanning() + step, PanningLeft, PanningRight );
n->setPanning( pan );
}
bool allPansEqual = std::all_of( nv.begin(), nv.end(),
[nv](const Note *note)
{
return note->getPanning() == nv[0]->getPanning();
});
if ( allPansEqual )
{
// show the pan hover-text only if all notes have the same
// panning
showPanTextFloat( nv[0]->getPanning(), we->pos(), 1000 );
}
}
update();
}
}
// not in note edit area, so handle scrolling/zooming and quantization change
else
if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::AltModifier )
{
int q = m_quantizeModel.value();
if( we->delta() > 0 )
{
q--;
}
else if( we->delta() < 0 )
{
q++;
}
q = qBound( 0, q, m_quantizeModel.size() - 1 );
m_quantizeModel.setValue( q );
}
else if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::ShiftModifier )
{
int l = m_noteLenModel.value();
if( we->delta() > 0 )
{
l--;
}
else if( we->delta() < 0 )
{
l++;
}
l = qBound( 0, l, m_noteLenModel.size() - 1 );
m_noteLenModel.setValue( l );
}
else if( we->modifiers() & Qt::ControlModifier )
{
int z = m_zoomingModel.value();
if( we->delta() > 0 )
{
z++;
}
else if( we->delta() < 0 )
{
z--;
}
z = qBound( 0, z, m_zoomingModel.size() - 1 );
// update combobox with zooming-factor
m_zoomingModel.setValue( z );
}
else if( we->modifiers() & Qt::ShiftModifier
|| we->orientation() == Qt::Horizontal )
{
m_leftRightScroll->setValue( m_leftRightScroll->value() -
we->delta() * 2 / 15 );
}
else
{
m_topBottomScroll->setValue( m_topBottomScroll->value() -
we->delta() / 30 );
}
}
void PianoRoll::focusOutEvent( QFocusEvent * )
{
if( hasValidPattern() )
{
for( int i = 0; i < NumKeys; ++i )
{
m_pattern->instrumentTrack()->pianoModel()->midiEventProcessor()->processInEvent( MidiEvent( MidiNoteOff, -1, i, 0 ) );
m_pattern->instrumentTrack()->pianoModel()->setKeyState( i, false );
}
}
m_editMode = m_ctrlMode;
update();
}
int PianoRoll::getKey(int y ) const
{
int key_line_y = keyAreaBottom() - 1;
// pressed key on piano
int key_num = ( key_line_y - y ) / KEY_LINE_HEIGHT;
key_num += m_startKey;
// some range-checking-stuff
if( key_num < 0 )
{
key_num = 0;
}
if( key_num >= KeysPerOctave * NumOctaves )
{
key_num = KeysPerOctave * NumOctaves - 1;
}
return key_num;
}
QList<int> PianoRoll::getAllOctavesForKey( int keyToMirror ) const
{
QList<int> keys;
for (int i=keyToMirror % KeysPerOctave; i < NumKeys; i += KeysPerOctave)
{
keys.append(i);
}
return keys;
}
Song::PlayModes PianoRoll::desiredPlayModeForAccompany() const
{
if( m_pattern->getTrack()->trackContainer() ==
Engine::getBBTrackContainer() )
{
return Song::Mode_PlayBB;
}
return Song::Mode_PlaySong;
}
void PianoRoll::play()
{
if( ! hasValidPattern() )
{
return;
}
if( Engine::getSong()->playMode() != Song::Mode_PlayPattern )
{
Engine::getSong()->playPattern( m_pattern );
}
else
{
Engine::getSong()->togglePause();
}
}
void PianoRoll::record()
{
if( Engine::getSong()->isPlaying() )
{
stop();
}
if( m_recording || ! hasValidPattern() )
{
return;
}
m_pattern->addJournalCheckPoint();
m_recording = true;
Engine::getSong()->playPattern( m_pattern, false );
}
void PianoRoll::recordAccompany()
{
if( Engine::getSong()->isPlaying() )
{
stop();
}
if( m_recording || ! hasValidPattern() )
{
return;
}
m_pattern->addJournalCheckPoint();
m_recording = true;
if( m_pattern->getTrack()->trackContainer() == Engine::getSong() )
{
Engine::getSong()->playSong();
}
else
{
Engine::getSong()->playBB();
}
}
void PianoRoll::stop()
{
Engine::getSong()->stop();
m_recording = false;
m_scrollBack = ( m_timeLine->autoScroll() == TimeLineWidget::AutoScrollEnabled );
}
void PianoRoll::startRecordNote(const Note & n )
{
if( m_recording && hasValidPattern() &&
Engine::getSong()->isPlaying() &&
(Engine::getSong()->playMode() == desiredPlayModeForAccompany() ||
Engine::getSong()->playMode() == Song::Mode_PlayPattern ))
{
MidiTime sub;
if( Engine::getSong()->playMode() == Song::Mode_PlaySong )
{
sub = m_pattern->startPosition();
}
Note n1( 1, Engine::getSong()->getPlayPos(
Engine::getSong()->playMode() ) - sub,
n.key(), n.getVolume(), n.getPanning() );
if( n1.pos() >= 0 )
{
m_recordingNotes << n1;
}
}
}
void PianoRoll::finishRecordNote(const Note & n )
{
if( m_recording && hasValidPattern() &&
Engine::getSong()->isPlaying() &&
( Engine::getSong()->playMode() ==
desiredPlayModeForAccompany() ||
Engine::getSong()->playMode() ==
Song::Mode_PlayPattern ) )
{
for( QList<Note>::Iterator it = m_recordingNotes.begin();
it != m_recordingNotes.end(); ++it )
{
if( it->key() == n.key() )
{
Note n1( n.length(), it->pos(),
it->key(), it->getVolume(),
it->getPanning() );
n1.quantizeLength( quantization() );
//Get selected chord
const InstrumentFunctionNoteStacking::Chord & chord = InstrumentFunctionNoteStacking::ChordTable::getInstance()
.getChordByName( m_chordModel.currentText() );
if( !chord.isEmpty() )
{
for( int i = 1; i < chord.size(); i++ )
{
Note new_note( n.length(), it->pos(), it->key() + chord[i] );
new_note.setPanning( it->getPanning() );
new_note.setVolume( it->getVolume() );
m_pattern->addNote( new_note );
}
}
m_pattern->addNote( n1 );
update();
m_recordingNotes.erase( it );
break;
}
}
}
}
void PianoRoll::horScrolled(int new_pos )
{
m_currentPosition = new_pos;
emit positionChanged( m_currentPosition );
update();
}
void PianoRoll::verScrolled( int new_pos )
{
// revert value
m_startKey = m_totalKeysToScroll - new_pos;
update();
}
void PianoRoll::setEditMode(int mode)
{
m_ctrlMode = m_editMode = (EditModes) mode;
}
void PianoRoll::selectAll()
{
if( ! hasValidPattern() )
{
return;
}
// if first_time = true, we HAVE to set the vars for select
bool first_time = true;
for( const Note *note : m_pattern->notes() )
{
int len_ticks = static_cast<int>( note->length() ) > 0 ?
static_cast<int>( note->length() ) : 1;
const int key = note->key();
int pos_ticks = note->pos();
if( key <= m_selectStartKey || first_time )
{
// if we move start-key down, we have to add
// the difference between old and new start-key
// to m_selectedKeys, otherwise the selection
// is just moved down...
m_selectedKeys += m_selectStartKey
- ( key - 1 );
m_selectStartKey = key - 1;
}
if( key >= m_selectedKeys + m_selectStartKey ||
first_time )
{
m_selectedKeys = key - m_selectStartKey;
}
if( pos_ticks < m_selectStartTick ||
first_time )
{
m_selectStartTick = pos_ticks;
}
if( pos_ticks + len_ticks >
m_selectStartTick + m_selectedTick ||
first_time )
{
m_selectedTick = pos_ticks +
len_ticks -
m_selectStartTick;
}
first_time = false;
}
}
// returns vector with pointers to all selected notes
NoteVector PianoRoll::getSelectedNotes()
{
NoteVector selectedNotes;
if (hasValidPattern())
{
for( Note *note : m_pattern->notes() )
{
if( note->selected() )
{
selectedNotes.push_back( note );
}
}
}
return selectedNotes;
}
// selects all notess associated with m_lastKey
void PianoRoll::selectNotesOnKey()
{
if (hasValidPattern()) {
for (Note * note : m_pattern->notes()) {
if (note->key() == m_lastKey) {
note->setSelected(true);
}
}
}
}
void PianoRoll::enterValue( NoteVector* nv )
{
if( m_noteEditMode == NoteEditVolume )
{
bool ok;
int new_val;
new_val = QInputDialog::getInt( this, "Piano roll: note velocity",
tr( "Please enter a new value between %1 and %2:" ).
arg( MinVolume ).arg( MaxVolume ),
(*nv)[0]->getVolume(),
MinVolume, MaxVolume, 1, &ok );
if( ok )
{
for ( Note * n : *nv )
{
n->setVolume( new_val );
}
m_lastNoteVolume = new_val;
}
}
else if( m_noteEditMode == NoteEditPanning )
{
bool ok;
int new_val;
new_val = QInputDialog::getInt( this, "Piano roll: note panning",
tr( "Please enter a new value between %1 and %2:" ).
arg( PanningLeft ).arg( PanningRight ),
(*nv)[0]->getPanning(),
PanningLeft, PanningRight, 1, &ok );
if( ok )
{
for ( Note * n : *nv )
{
n->setPanning( new_val );
}
m_lastNotePanning = new_val;
}
}
}
void PianoRoll::copyToClipboard( const NoteVector & notes ) const
{
DataFile dataFile( DataFile::ClipboardData );
QDomElement note_list = dataFile.createElement( "note-list" );
dataFile.content().appendChild( note_list );
MidiTime start_pos( notes.front()->pos().getTact(), 0 );
for( const Note *note : notes )
{
Note clip_note( *note );
clip_note.setPos( clip_note.pos( start_pos ) );
clip_note.saveState( dataFile, note_list );
}
QMimeData * clip_content = new QMimeData;
clip_content->setData( Clipboard::mimeType(), dataFile.toString().toUtf8() );
QApplication::clipboard()->setMimeData( clip_content,
QClipboard::Clipboard );
}
void PianoRoll::copySelectedNotes()
{
NoteVector selected_notes = getSelectedNotes();
if( ! selected_notes.empty() )
{
copyToClipboard( selected_notes );
}
}
void PianoRoll::cutSelectedNotes()
{
if( ! hasValidPattern() )
{
return;
}
NoteVector selected_notes = getSelectedNotes();
if( ! selected_notes.empty() )
{
copyToClipboard( selected_notes );
Engine::getSong()->setModified();
for( Note *note : selected_notes )
{
// note (the memory of it) is also deleted by
// pattern::removeNote(...) so we don't have to do that
m_pattern->removeNote( note );
}
}
update();
gui->songEditor()->update();
}
void PianoRoll::pasteNotes()
{
if( ! hasValidPattern() )
{
return;
}
QString value = QApplication::clipboard()
->mimeData( QClipboard::Clipboard )
->data( Clipboard::mimeType() );
if( ! value.isEmpty() )
{
DataFile dataFile( value.toUtf8() );
QDomNodeList list = dataFile.elementsByTagName( Note::classNodeName() );
// remove selection and select the newly pasted notes
clearSelectedNotes();
if( ! list.isEmpty() )
{
m_pattern->addJournalCheckPoint();
}
for( int i = 0; ! list.item( i ).isNull(); ++i )
{
// create the note
Note cur_note;
cur_note.restoreState( list.item( i ).toElement() );
cur_note.setPos( cur_note.pos() + Note::quantized( m_timeLine->pos(), quantization() ) );
// select it
cur_note.setSelected( true );
// add to pattern
m_pattern->addNote( cur_note, false );
}
// we only have to do the following lines if we pasted at
// least one note...
Engine::getSong()->setModified();
update();
gui->songEditor()->update();
}
}
void PianoRoll::deleteSelectedNotes()
{
if( ! hasValidPattern() )
{
return;
}
bool update_after_delete = false;
m_pattern->addJournalCheckPoint();
// get note-vector of current pattern
const NoteVector & notes = m_pattern->notes();
// will be our iterator in the following loop
NoteVector::ConstIterator it = notes.begin();
while( it != notes.end() )
{
Note *note = *it;
if( note->selected() )
{
// delete this note
m_pattern->removeNote( note );
update_after_delete = true;
// start over, make sure we get all the notes
it = notes.begin();
}
else
{
++it;
}
}
if( update_after_delete )
{
Engine::getSong()->setModified();
update();
gui->songEditor()->update();
}
}
void PianoRoll::autoScroll( const MidiTime & t )
{
const int w = width() - WHITE_KEY_WIDTH;
if( t > m_currentPosition + w * MidiTime::ticksPerTact() / m_ppt )
{
m_leftRightScroll->setValue( t.getTact() * MidiTime::ticksPerTact() );
}
else if( t < m_currentPosition )
{
MidiTime t2 = qMax( t - w * MidiTime::ticksPerTact() *
MidiTime::ticksPerTact() / m_ppt, (tick_t) 0 );
m_leftRightScroll->setValue( t2.getTact() * MidiTime::ticksPerTact() );
}
m_scrollBack = false;
}
void PianoRoll::updatePosition( const MidiTime & t )
{
if( ( Engine::getSong()->isPlaying()
&& Engine::getSong()->playMode() == Song::Mode_PlayPattern
&& m_timeLine->autoScroll() == TimeLineWidget::AutoScrollEnabled
) || m_scrollBack )
{
autoScroll( t );
}
}
void PianoRoll::updatePositionAccompany( const MidiTime & t )
{
Song * s = Engine::getSong();
if( m_recording && hasValidPattern() &&
s->playMode() != Song::Mode_PlayPattern )
{
MidiTime pos = t;
if( s->playMode() != Song::Mode_PlayBB )
{
pos -= m_pattern->startPosition();
}
if( (int) pos > 0 )
{
s->getPlayPos( Song::Mode_PlayPattern ).setTicks( pos );
autoScroll( pos );
}
}
}
void PianoRoll::zoomingChanged()
{
m_ppt = m_zoomLevels[m_zoomingModel.value()] * DEFAULT_PR_PPT;
assert( m_ppt > 0 );
m_timeLine->setPixelsPerTact( m_ppt );
update();
}
void PianoRoll::quantizeChanged()
{
update();
}
int PianoRoll::quantization() const
{
if( m_quantizeModel.value() == 0 )
{
if( m_noteLenModel.value() > 0 )
{
return newNoteLen();
}
else
{
return DefaultTicksPerTact / 16;
}
}
QString text = m_quantizeModel.currentText();
return DefaultTicksPerTact / text.right( text.length() - 2 ).toInt();
}
void PianoRoll::quantizeNotes()
{
if( ! hasValidPattern() )
{
return;
}
m_pattern->addJournalCheckPoint();
NoteVector notes = getSelectedNotes();
if( notes.empty() )
{
for( Note* n : m_pattern->notes() )
{
notes.push_back( n );
}
}
for( Note* n : notes )
{
if( n->length() == MidiTime( 0 ) )
{
continue;
}
Note copy(*n);
m_pattern->removeNote( n );
copy.quantizePos( quantization() );
m_pattern->addNote( copy );
}
update();
gui->songEditor()->update();
Engine::getSong()->setModified();
}
void PianoRoll::updateSemiToneMarkerMenu()
{
const InstrumentFunctionNoteStacking::ChordTable& chord_table =
InstrumentFunctionNoteStacking::ChordTable::getInstance();
const InstrumentFunctionNoteStacking::Chord& scale =
chord_table.getScaleByName( m_scaleModel.currentText() );
const InstrumentFunctionNoteStacking::Chord& chord =
chord_table.getChordByName( m_chordModel.currentText() );
emit semiToneMarkerMenuScaleSetEnabled( ! scale.isEmpty() );
emit semiToneMarkerMenuChordSetEnabled( ! chord.isEmpty() );
}
MidiTime PianoRoll::newNoteLen() const
{
if( m_noteLenModel.value() == 0 )
{
return m_lenOfNewNotes;
}
QString text = m_noteLenModel.currentText();
return DefaultTicksPerTact / text.right( text.length() - 2 ).toInt();
}
bool PianoRoll::mouseOverNote()
{
return hasValidPattern() && noteUnderMouse() != NULL;
}
Note * PianoRoll::noteUnderMouse()
{
QPoint pos = mapFromGlobal( QCursor::pos() );
if( pos.x() <= WHITE_KEY_WIDTH
|| pos.x() > width() - SCROLLBAR_SIZE
|| pos.y() < PR_TOP_MARGIN
|| pos.y() > keyAreaBottom() )
{
return NULL;
}
int key_num = getKey( pos.y() );
int pos_ticks = ( pos.x() - WHITE_KEY_WIDTH ) *
MidiTime::ticksPerTact() / m_ppt + m_currentPosition;
// loop through whole note-vector...
for( Note* const& note : m_pattern->notes() )
{
// and check whether the cursor is over an
// existing note
if( pos_ticks >= note->pos()
&& pos_ticks <= note->endPos()
&& note->key() == key_num
&& note->length() > 0 )
{
return note;
}
}
return NULL;
}
PianoRollWindow::PianoRollWindow() :
Editor(true),
m_editor(new PianoRoll())
{
setCentralWidget( m_editor );
m_playAction->setToolTip(tr( "Play/pause current pattern (Space)" ) );
m_recordAction->setToolTip(tr( "Record notes from MIDI-device/channel-piano" ) );
m_recordAccompanyAction->setToolTip( tr( "Record notes from MIDI-device/channel-piano while playing song or BB track" ) );
m_stopAction->setToolTip( tr( "Stop playing of current pattern (Space)" ) );
m_playAction->setWhatsThis(
tr( "Click here to play the current pattern. "
"This is useful while editing it. The pattern is "
"automatically looped when its end is reached." ) );
m_recordAction->setWhatsThis(
tr( "Click here to record notes from a MIDI-"
"device or the virtual test-piano of the according "
"channel-window to the current pattern. When recording "
"all notes you play will be written to this pattern "
"and you can play and edit them afterwards." ) );
m_recordAccompanyAction->setWhatsThis(
tr( "Click here to record notes from a MIDI-"
"device or the virtual test-piano of the according "
"channel-window to the current pattern. When recording "
"all notes you play will be written to this pattern "
"and you will hear the song or BB track in the background." ) );
m_stopAction->setWhatsThis(
tr( "Click here to stop playback of current pattern." ) );
DropToolBar *notesActionsToolBar = addDropToolBarToTop( tr( "Edit actions" ) );
// init edit-buttons at the top
ActionGroup* editModeGroup = new ActionGroup( this );
QAction* drawAction = editModeGroup->addAction( embed::getIconPixmap( "edit_draw" ), tr( "Draw mode (Shift+D)" ) );
QAction* eraseAction = editModeGroup->addAction( embed::getIconPixmap( "edit_erase" ), tr("Erase mode (Shift+E)" ) );
QAction* selectAction = editModeGroup->addAction( embed::getIconPixmap( "edit_select" ), tr( "Select mode (Shift+S)" ) );
QAction* pitchBendAction = editModeGroup->addAction( embed::getIconPixmap( "automation" ), tr("Pitch Bend mode (Shift+T)" ) );
drawAction->setChecked( true );
drawAction->setShortcut( Qt::SHIFT | Qt::Key_D );
eraseAction->setShortcut( Qt::SHIFT | Qt::Key_E );
selectAction->setShortcut( Qt::SHIFT | Qt::Key_S );
pitchBendAction->setShortcut( Qt::SHIFT | Qt::Key_T );
drawAction->setWhatsThis(
tr( "Click here and draw mode will be activated. In this "
"mode you can add, resize and move notes. This "
"is the default mode which is used most of the time. "
"You can also press 'Shift+D' on your keyboard to "
"activate this mode. In this mode, hold %1 to "
"temporarily go into select mode." ).arg(
#ifdef LMMS_BUILD_APPLE
"⌘" ) );
#else
"Ctrl" ) );
#endif
eraseAction->setWhatsThis(
tr( "Click here and erase mode will be activated. In this "
"mode you can erase notes. You can also press "
"'Shift+E' on your keyboard to activate this mode." ) );
selectAction->setWhatsThis(
tr( "Click here and select mode will be activated. "
"In this mode you can select notes. Alternatively, "
"you can hold %1 in draw mode to temporarily use "
"select mode." ).arg(
#ifdef LMMS_BUILD_APPLE
"⌘" ) );
#else
"Ctrl" ) );
#endif
pitchBendAction->setWhatsThis(
tr( "Click here and Pitch Bend mode will be activated. "
"In this mode you can click a note to open its "
"automation detuning. You can utilize this to slide "
"notes from one to another. You can also press "
"'Shift+T' on your keyboard to activate this mode." ) );
connect( editModeGroup, SIGNAL( triggered( int ) ), m_editor, SLOT( setEditMode( int ) ) );
QAction* quantizeAction = new QAction(embed::getIconPixmap( "quantize" ), tr( "Quantize" ), this );
connect( quantizeAction, SIGNAL( triggered() ), m_editor, SLOT( quantizeNotes() ) );
notesActionsToolBar->addAction( drawAction );
notesActionsToolBar->addAction( eraseAction );
notesActionsToolBar->addAction( selectAction );
notesActionsToolBar->addAction( pitchBendAction );
notesActionsToolBar->addSeparator();
notesActionsToolBar->addAction( quantizeAction );
// Copy + paste actions
DropToolBar *copyPasteActionsToolBar = addDropToolBarToTop( tr( "Copy paste controls" ) );
QAction* cutAction = new QAction(embed::getIconPixmap( "edit_cut" ),
tr( "Cut selected notes (%1+X)" ).arg(
#ifdef LMMS_BUILD_APPLE
"⌘" ), this );
#else
"Ctrl" ), this );
#endif
QAction* copyAction = new QAction(embed::getIconPixmap( "edit_copy" ),
tr( "Copy selected notes (%1+C)" ).arg(
#ifdef LMMS_BUILD_APPLE
"⌘"), this);
#else
"Ctrl" ), this );
#endif
QAction* pasteAction = new QAction(embed::getIconPixmap( "edit_paste" ),
tr( "Paste notes from clipboard (%1+V)" ).arg(
#ifdef LMMS_BUILD_APPLE
"⌘" ), this );
#else
"Ctrl" ), this );
#endif
cutAction->setWhatsThis(
tr( "Click here and the selected notes will be cut into the "
"clipboard. You can paste them anywhere in any pattern "
"by clicking on the paste button." ) );
copyAction->setWhatsThis(
tr( "Click here and the selected notes will be copied into the "
"clipboard. You can paste them anywhere in any pattern "
"by clicking on the paste button." ) );
pasteAction->setWhatsThis(
tr( "Click here and the notes from the clipboard will be "
"pasted at the first visible measure." ) );
cutAction->setShortcut( Qt::CTRL | Qt::Key_X );
copyAction->setShortcut( Qt::CTRL | Qt::Key_C );
pasteAction->setShortcut( Qt::CTRL | Qt::Key_V );
connect( cutAction, SIGNAL( triggered() ), m_editor, SLOT( cutSelectedNotes() ) );
connect( copyAction, SIGNAL( triggered() ), m_editor, SLOT( copySelectedNotes() ) );
connect( pasteAction, SIGNAL( triggered() ), m_editor, SLOT( pasteNotes() ) );
copyPasteActionsToolBar->addAction( cutAction );
copyPasteActionsToolBar->addAction( copyAction );
copyPasteActionsToolBar->addAction( pasteAction );
DropToolBar *timeLineToolBar = addDropToolBarToTop( tr( "Timeline controls" ) );
m_editor->m_timeLine->addToolButtons( timeLineToolBar );
addToolBarBreak();
DropToolBar *zoomAndNotesToolBar = addDropToolBarToTop( tr( "Zoom and note controls" ) );
QLabel * zoom_lbl = new QLabel( m_toolBar );
zoom_lbl->setPixmap( embed::getIconPixmap( "zoom" ) );
m_zoomingComboBox = new ComboBox( m_toolBar );
m_zoomingComboBox->setModel( &m_editor->m_zoomingModel );
m_zoomingComboBox->setFixedSize( 64, 22 );
// setup quantize-stuff
QLabel * quantize_lbl = new QLabel( m_toolBar );
quantize_lbl->setPixmap( embed::getIconPixmap( "quantize" ) );
m_quantizeComboBox = new ComboBox( m_toolBar );
m_quantizeComboBox->setModel( &m_editor->m_quantizeModel );
m_quantizeComboBox->setFixedSize( 64, 22 );
// setup note-len-stuff
QLabel * note_len_lbl = new QLabel( m_toolBar );
note_len_lbl->setPixmap( embed::getIconPixmap( "note" ) );
m_noteLenComboBox = new ComboBox( m_toolBar );
m_noteLenComboBox->setModel( &m_editor->m_noteLenModel );
m_noteLenComboBox->setFixedSize( 105, 22 );
// setup scale-stuff
QLabel * scale_lbl = new QLabel( m_toolBar );
scale_lbl->setPixmap( embed::getIconPixmap( "scale" ) );
m_scaleComboBox = new ComboBox( m_toolBar );
m_scaleComboBox->setModel( &m_editor->m_scaleModel );
m_scaleComboBox->setFixedSize( 105, 22 );
// setup chord-stuff
QLabel * chord_lbl = new QLabel( m_toolBar );
chord_lbl->setPixmap( embed::getIconPixmap( "chord" ) );
m_chordComboBox = new ComboBox( m_toolBar );
m_chordComboBox->setModel( &m_editor->m_chordModel );
m_chordComboBox->setFixedSize( 105, 22 );
zoomAndNotesToolBar->addWidget( zoom_lbl );
zoomAndNotesToolBar->addWidget( m_zoomingComboBox );
zoomAndNotesToolBar->addSeparator();
zoomAndNotesToolBar->addWidget( quantize_lbl );
zoomAndNotesToolBar->addWidget( m_quantizeComboBox );
zoomAndNotesToolBar->addSeparator();
zoomAndNotesToolBar->addWidget( note_len_lbl );
zoomAndNotesToolBar->addWidget( m_noteLenComboBox );
zoomAndNotesToolBar->addSeparator();
zoomAndNotesToolBar->addWidget( scale_lbl );
zoomAndNotesToolBar->addWidget( m_scaleComboBox );
zoomAndNotesToolBar->addSeparator();
zoomAndNotesToolBar->addWidget( chord_lbl );
zoomAndNotesToolBar->addWidget( m_chordComboBox );
m_zoomingComboBox->setWhatsThis(
tr(
"This controls the magnification of an axis. "
"It can be helpful to choose magnification for a specific "
"task. For ordinary editing, the magnification should be "
"fitted to your smallest notes. "
) );
m_quantizeComboBox->setWhatsThis(
tr(
"The 'Q' stands for quantization, and controls the grid size "
"notes and control points snap to. "
"With smaller quantization values, you can draw shorter notes "
"in Piano Roll, and more exact control points in the "
"Automation Editor."
) );
m_noteLenComboBox->setWhatsThis(
tr(
"This lets you select the length of new notes. "
"'Last Note' means that LMMS will use the note length of "
"the note you last edited"
) );
m_scaleComboBox->setWhatsThis(
tr(
"The feature is directly connected to the context-menu "
"on the virtual keyboard, to the left in Piano Roll. "
"After you have chosen the scale you want "
"in this drop-down menu, "
"you can right click on a desired key in the virtual keyboard, "
"and then choose 'Mark current Scale'. "
"LMMS will highlight all notes that belongs to the chosen scale, "
"and in the key you have selected!"
) );
m_chordComboBox->setWhatsThis(
tr(
"Let you select a chord which LMMS then can draw or highlight."
"You can find the most common chords in this drop-down menu. "
"After you have selected a chord, click anywhere to place the chord, and right "
"click on the virtual keyboard to open context menu and highlight the chord. "
"To return to single note placement, you need to choose 'No chord' "
"in this drop-down menu."
) );
// setup our actual window
setFocusPolicy( Qt::StrongFocus );
setFocus();
setWindowIcon( embed::getIconPixmap( "piano" ) );
setCurrentPattern( NULL );
// Connections
connect( m_editor, SIGNAL( currentPatternChanged() ), this, SIGNAL( currentPatternChanged() ) );
connect( m_editor, SIGNAL( currentPatternChanged() ), this, SLOT( patternRenamed() ) );
}
const Pattern* PianoRollWindow::currentPattern() const
{
return m_editor->currentPattern();
}
void PianoRollWindow::setCurrentPattern( Pattern* pattern )
{
m_editor->setCurrentPattern( pattern );
if ( pattern )
{
setWindowTitle( tr( "Piano-Roll - %1" ).arg( pattern->name() ) );
connect( pattern->instrumentTrack(), SIGNAL( nameChanged() ), this, SLOT( patternRenamed()) );
connect( pattern, SIGNAL( dataChanged() ), this, SLOT( patternRenamed() ) );
}
else
{
setWindowTitle( tr( "Piano-Roll - no pattern" ) );
}
}
bool PianoRollWindow::isRecording() const
{
return m_editor->isRecording();
}
int PianoRollWindow::quantization() const
{
return m_editor->quantization();
}
void PianoRollWindow::play()
{
m_editor->play();
}
void PianoRollWindow::stop()
{
m_editor->stop();
}
void PianoRollWindow::record()
{
m_editor->record();
}
void PianoRollWindow::recordAccompany()
{
m_editor->recordAccompany();
}
void PianoRollWindow::stopRecording()
{
m_editor->stopRecording();
}
void PianoRollWindow::reset()
{
m_editor->reset();
}
void PianoRollWindow::saveSettings( QDomDocument & doc, QDomElement & de )
{
MainWindow::saveWidgetState( this, de );
}
void PianoRollWindow::loadSettings( const QDomElement & de )
{
MainWindow::restoreWidgetState( this, de );
}
QSize PianoRollWindow::sizeHint() const
{
return { INITIAL_PIANOROLL_WIDTH, INITIAL_PIANOROLL_HEIGHT };
}
void PianoRollWindow::patternRenamed()
{
if ( currentPattern() )
{
setWindowTitle( tr( "Piano-Roll - %1" ).arg( currentPattern()->name() ) );
}
else
{
setWindowTitle( tr( "Piano-Roll - no pattern" ) );
}
}
void PianoRollWindow::focusInEvent( QFocusEvent * event )
{
// when the window is given focus, also give focus to the actual piano roll
m_editor->setFocus( event->reason() );
}