/* * AutomationEditor.cpp - implementation of AutomationEditor which is used for * actual setting of dynamic values * * Copyright (c) 2008-2014 Tobias Doerffel * Copyright (c) 2008-2013 Paul Giblock * Copyright (c) 2006-2008 Javier Serrano Polo * * 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 "AutomationEditor.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifndef __USE_XOPEN #define __USE_XOPEN #endif #include #include "ActionGroup.h" #include "SongEditor.h" #include "MainWindow.h" #include "GuiApplication.h" #include "embed.h" #include "Engine.h" #include "PixmapButton.h" #include "templates.h" #include "gui_templates.h" #include "TimeLineWidget.h" #include "ToolTip.h" #include "TextFloat.h" #include "ComboBox.h" #include "BBTrackContainer.h" #include "PianoRoll.h" #include "debug.h" #include "MeterModel.h" #include "StringPairDrag.h" #include "ProjectJournal.h" QPixmap * AutomationEditor::s_toolDraw = NULL; QPixmap * AutomationEditor::s_toolErase = NULL; QPixmap * AutomationEditor::s_toolSelect = NULL; QPixmap * AutomationEditor::s_toolMove = NULL; QPixmap * AutomationEditor::s_toolYFlip = NULL; QPixmap * AutomationEditor::s_toolXFlip = NULL; const QVector AutomationEditor::m_zoomXLevels = { 8.0f, 4.0f, 2.0f, 1.0f, 0.5f, 0.25f, 0.125f }; AutomationEditor::AutomationEditor() : QWidget(), m_zoomingXModel(), m_zoomingYModel(), m_quantizeModel(), m_patternMutex( QMutex::Recursive ), m_pattern( NULL ), m_minLevel( 0 ), m_maxLevel( 0 ), m_step( 1 ), m_scrollLevel( 0 ), m_bottomLevel( 0 ), m_topLevel( 0 ), m_currentPosition(), m_action( NONE ), m_moveStartLevel( 0 ), m_moveStartTick( 0 ), m_drawLastLevel( 0.0f ), m_drawLastTick( 0 ), m_ppt( DEFAULT_PPT ), m_y_delta( DEFAULT_Y_DELTA ), m_y_auto( true ), m_editMode( DRAW ), m_scrollBack( false ), m_barLineColor( 0, 0, 0 ), m_beatLineColor( 0, 0, 0 ), m_lineColor( 0, 0, 0 ), m_graphColor( Qt::SolidPattern ), m_vertexColor( 0,0,0 ), m_scaleColor( Qt::SolidPattern ), m_crossColor( 0, 0, 0 ), m_backgroundShade( 0, 0, 0 ) { connect( this, SIGNAL( currentPatternChanged() ), this, SLOT( updateAfterPatternChange() ), Qt::QueuedConnection ); connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int, int ) ), this, SLOT( update() ) ); setAttribute( Qt::WA_OpaquePaintEvent, true ); //keeps the direction of the widget, undepended on the locale setLayoutDirection( Qt::LeftToRight ); m_tensionModel = new FloatModel(1.0, 0.0, 1.0, 0.01); connect( m_tensionModel, SIGNAL( dataChanged() ), this, SLOT( setTension() ) ); for( int i = 0; i < 7; ++i ) { m_quantizeModel.addItem( "1/" + QString::number( 1 << i ) ); } for( int i = 0; i < 5; ++i ) { m_quantizeModel.addItem( "1/" + QString::number( ( 1 << i ) * 3 ) ); } m_quantizeModel.addItem( "1/192" ); connect( &m_quantizeModel, SIGNAL(dataChanged() ), this, SLOT( setQuantization() ) ); m_quantizeModel.setValue( m_quantizeModel.findText( "1/8" ) ); if( s_toolYFlip == NULL ) { s_toolYFlip = new QPixmap( embed::getIconPixmap( "flip_y" ) ); } if( s_toolXFlip == NULL ) { s_toolXFlip = new QPixmap( embed::getIconPixmap( "flip_x" ) ); } // add time-line m_timeLine = new TimeLineWidget( VALUES_WIDTH, 0, m_ppt, Engine::getSong()->getPlayPos( Song::Mode_PlayAutomationPattern ), 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 & ) ) ); 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 ) ) ); // init pixmaps 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" ) ); } setCurrentPattern( NULL ); setMouseTracking( true ); } AutomationEditor::~AutomationEditor() { m_zoomingXModel.disconnect(); m_zoomingYModel.disconnect(); m_quantizeModel.disconnect(); m_tensionModel->disconnect(); delete m_tensionModel; } void AutomationEditor::setCurrentPattern(AutomationPattern * new_pattern ) { if (m_pattern) { m_pattern->disconnect(this); } m_patternMutex.lock(); m_pattern = new_pattern; m_patternMutex.unlock(); if (m_pattern != nullptr) { connect(m_pattern, SIGNAL(dataChanged()), this, SLOT(update())); } emit currentPatternChanged(); } void AutomationEditor::saveSettings(QDomDocument & doc, QDomElement & dom_parent) { MainWindow::saveWidgetState(parentWidget(), dom_parent, QSize( 640, 400 )); } void AutomationEditor::loadSettings( const QDomElement & dom_parent) { MainWindow::restoreWidgetState(parentWidget(), dom_parent); } // qproperty access methods QColor AutomationEditor::barLineColor() const { return m_barLineColor; } void AutomationEditor::setBarLineColor( const QColor & c ) { m_barLineColor = c; } QColor AutomationEditor::beatLineColor() const { return m_beatLineColor; } void AutomationEditor::setBeatLineColor( const QColor & c ) { m_beatLineColor = c; } QColor AutomationEditor::lineColor() const { return m_lineColor; } void AutomationEditor::setLineColor( const QColor & c ) { m_lineColor = c; } QBrush AutomationEditor::graphColor() const { return m_graphColor; } void AutomationEditor::setGraphColor( const QBrush & c ) { m_graphColor = c; } QColor AutomationEditor::vertexColor() const { return m_vertexColor; } void AutomationEditor::setVertexColor( const QColor & c ) { m_vertexColor = c; } QBrush AutomationEditor::scaleColor() const { return m_scaleColor; } void AutomationEditor::setScaleColor( const QBrush & c ) { m_scaleColor = c; } QColor AutomationEditor::crossColor() const { return m_crossColor; } void AutomationEditor::setCrossColor( const QColor & c ) { m_crossColor = c; } QColor AutomationEditor::backgroundShade() const { return m_backgroundShade; } void AutomationEditor::setBackgroundShade( const QColor & c ) { m_backgroundShade = c; } void AutomationEditor::updateAfterPatternChange() { QMutexLocker m( &m_patternMutex ); m_currentPosition = 0; if( !validPattern() ) { m_minLevel = m_maxLevel = m_scrollLevel = 0; m_step = 1; resizeEvent( NULL ); return; } m_minLevel = m_pattern->firstObject()->minValue(); m_maxLevel = m_pattern->firstObject()->maxValue(); m_step = m_pattern->firstObject()->step(); m_scrollLevel = ( m_minLevel + m_maxLevel ) / 2; m_tensionModel->setValue( m_pattern->getTension() ); // resizeEvent() does the rest for us (scrolling, range-checking // of levels and so on...) resizeEvent( NULL ); update(); } void AutomationEditor::update() { QWidget::update(); QMutexLocker m( &m_patternMutex ); // Note detuning? if( m_pattern && !m_pattern->getTrack() ) { gui->pianoRoll()->update(); } } void AutomationEditor::removeSelection() { m_selectStartTick = 0; m_selectedTick = 0; m_selectStartLevel = 0; m_selectedLevels = 0; } void AutomationEditor::keyPressEvent(QKeyEvent * ke ) { switch( ke->key() ) { case Qt::Key_Up: m_topBottomScroll->setValue( m_topBottomScroll->value() - 1 ); ke->accept(); break; case Qt::Key_Down: m_topBottomScroll->setValue( m_topBottomScroll->value() + 1 ); ke->accept(); break; case Qt::Key_Left: if( ( m_timeLine->pos() -= 16 ) < 0 ) { m_timeLine->pos().setTicks( 0 ); } m_timeLine->updatePosition(); ke->accept(); break; case Qt::Key_Right: m_timeLine->pos() += 16; m_timeLine->updatePosition(); ke->accept(); break; //TODO: m_selectButton and m_moveButton are broken. /*case Qt::Key_A: if( ke->modifiers() & Qt::ControlModifier ) { m_selectButton->setChecked( true ); selectAll(); update(); ke->accept(); } break; case Qt::Key_Delete: deleteSelectedValues(); ke->accept(); break;*/ case Qt::Key_Home: m_timeLine->pos().setTicks( 0 ); m_timeLine->updatePosition(); ke->accept(); break; default: break; } } void AutomationEditor::leaveEvent(QEvent * e ) { while( QApplication::overrideCursor() != NULL ) { QApplication::restoreOverrideCursor(); } QWidget::leaveEvent( e ); } void AutomationEditor::drawLine( int x0, float y0, int x1, float y1 ) { int deltax = qRound( qAbs( x1 - x0 ) ); float deltay = qAbs( y1 - y0 ); int x = x0; float y = y0; int xstep; int ystep; if( deltax < AutomationPattern::quantization() ) { return; } deltax /= AutomationPattern::quantization(); float yscale = deltay / ( deltax ); if( x0 < x1) { xstep = AutomationPattern::quantization(); } else { xstep = -( AutomationPattern::quantization() ); } if( y0 < y1 ) { ystep = 1; } else { ystep = -1; } int i = 0; while( i < deltax ) { y = y0 + ( ystep * yscale * i ); x += xstep; i += 1; m_pattern->removeValue( MidiTime( x ) ); m_pattern->putValue( MidiTime( x ), y ); } } void AutomationEditor::mousePressEvent( QMouseEvent* mouseEvent ) { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } if( mouseEvent->y() > TOP_MARGIN ) { float level = getLevel( mouseEvent->y() ); int x = mouseEvent->x(); if( x > VALUES_WIDTH ) { // set or move value x -= VALUES_WIDTH; // get tick in which the user clicked int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt + m_currentPosition; // get time map of current pattern timeMap & time_map = m_pattern->getTimeMap(); // will be our iterator in the following loop timeMap::iterator it = time_map.begin(); // loop through whole time-map... while( it != time_map.end() ) { MidiTime len = 4; // and check whether the user clicked on an // existing value if( pos_ticks >= it.key() && len > 0 && ( it+1==time_map.end() || pos_ticks <= (it+1).key() ) && ( pos_ticks<= it.key() + MidiTime::ticksPerTact() *4 / m_ppt ) && level <= it.value() ) { break; } ++it; } // left button?? if( mouseEvent->button() == Qt::LeftButton && m_editMode == DRAW ) { m_pattern->addJournalCheckPoint(); // Connect the dots if( mouseEvent->modifiers() & Qt::ShiftModifier ) { drawLine( m_drawLastTick, m_drawLastLevel, pos_ticks, level ); } m_drawLastTick = pos_ticks; m_drawLastLevel = level; // did it reach end of map because // there's no value?? if( it == time_map.end() ) { // then set new value MidiTime value_pos( pos_ticks ); MidiTime new_time = m_pattern->setDragValue( value_pos, level, true, mouseEvent->modifiers() & Qt::ControlModifier ); // reset it so that it can be used for // ops (move, resize) after this // code-block it = time_map.find( new_time ); } // move it m_action = MOVE_VALUE; int aligned_x = (int)( (float)( ( it.key() - m_currentPosition ) * m_ppt ) / MidiTime::ticksPerTact() ); m_moveXOffset = x - aligned_x - 1; // set move-cursor QCursor c( Qt::SizeAllCursor ); QApplication::setOverrideCursor( c ); Engine::getSong()->setModified(); } else if( ( mouseEvent->button() == Qt::RightButton && m_editMode == DRAW ) || m_editMode == ERASE ) { m_pattern->addJournalCheckPoint(); // erase single value if( it != time_map.end() ) { m_pattern->removeValue( it.key() ); Engine::getSong()->setModified(); } m_action = NONE; } else if( mouseEvent->button() == Qt::LeftButton && m_editMode == SELECT ) { // select an area of values m_selectStartTick = pos_ticks; m_selectedTick = 0; m_selectStartLevel = level; m_selectedLevels = 1; m_action = SELECT_VALUES; } else if( mouseEvent->button() == Qt::RightButton && m_editMode == SELECT ) { // when clicking right in select-move, we // switch to move-mode //m_moveButton->setChecked( true ); } else if( mouseEvent->button() == Qt::LeftButton && m_editMode == MOVE ) { m_pattern->addJournalCheckPoint(); // move selection (including selected values) // save position where move-process began m_moveStartTick = pos_ticks; m_moveStartLevel = level; m_action = MOVE_SELECTION; Engine::getSong()->setModified(); } else if( mouseEvent->button() == Qt::RightButton && m_editMode == MOVE ) { // when clicking right in select-move, we // switch to draw-mode //m_drawButton->setChecked( true ); } update(); } } } void AutomationEditor::mouseReleaseEvent(QMouseEvent * mouseEvent ) { if( m_editMode == DRAW ) { if( m_action == MOVE_VALUE ) { m_pattern->applyDragValue(); } QApplication::restoreOverrideCursor(); } m_action = NONE; } void AutomationEditor::mouseMoveEvent(QMouseEvent * mouseEvent ) { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { update(); return; } if( mouseEvent->y() > TOP_MARGIN ) { float level = getLevel( mouseEvent->y() ); int x = mouseEvent->x(); x -= VALUES_WIDTH; if( m_action == MOVE_VALUE ) { x -= m_moveXOffset; } int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt + m_currentPosition; if( mouseEvent->buttons() & Qt::LeftButton && m_editMode == DRAW ) { if( m_action == MOVE_VALUE ) { // moving value if( pos_ticks < 0 ) { pos_ticks = 0; } drawLine( m_drawLastTick, m_drawLastLevel, pos_ticks, level ); m_drawLastTick = pos_ticks; m_drawLastLevel = level; // we moved the value so the value has to be // moved properly according to new starting- // time in the time map of pattern m_pattern->setDragValue( MidiTime( pos_ticks ), level, true, mouseEvent->modifiers() & Qt::ControlModifier ); } Engine::getSong()->setModified(); } else if( ( mouseEvent->buttons() & Qt::RightButton && m_editMode == DRAW ) || ( mouseEvent->buttons() & Qt::LeftButton && m_editMode == ERASE ) ) { // int resolution needed to improve the sensitivity of // the erase manoeuvre with zoom levels < 100% int zoom = m_zoomingXModel.value(); int resolution = 1 + zoom * zoom; for( int i = -resolution; i < resolution; ++i ) { m_pattern->removeValue( MidiTime( pos_ticks + i ) ); } } else if( mouseEvent->buttons() & Qt::NoButton && m_editMode == DRAW ) { // set move- or resize-cursor // get time map of current pattern timeMap & time_map = m_pattern->getTimeMap(); // will be our iterator in the following loop timeMap::iterator it = time_map.begin(); // loop through whole time map... for( ; it != time_map.end(); ++it ) { // and check whether the cursor is over an // existing value if( pos_ticks >= it.key() && ( it+1==time_map.end() || pos_ticks <= (it+1).key() ) && level <= it.value() ) { break; } } // did it reach end of map because there's // no value?? if( it != time_map.end() ) { if( QApplication::overrideCursor() ) { if( QApplication::overrideCursor()->shape() != Qt::SizeAllCursor ) { while( QApplication::overrideCursor() != NULL ) { QApplication::restoreOverrideCursor(); } QCursor c( Qt::SizeAllCursor ); QApplication::setOverrideCursor( c ); } } else { QCursor c( Qt::SizeAllCursor ); QApplication::setOverrideCursor( c ); } } else { // the cursor is over no value, so restore // cursor while( QApplication::overrideCursor() != NULL ) { QApplication::restoreOverrideCursor(); } } } else if( mouseEvent->buttons() & Qt::LeftButton && m_editMode == SELECT && m_action == SELECT_VALUES ) { // change size of selection if( x < 0 && m_currentPosition > 0 ) { x = 0; QCursor::setPos( mapToGlobal( QPoint( VALUES_WIDTH, mouseEvent->y() ) ) ); if( m_currentPosition >= 4 ) { m_leftRightScroll->setValue( m_currentPosition - 4 ); } else { m_leftRightScroll->setValue( 0 ); } } else if( x > width() - VALUES_WIDTH ) { x = width() - VALUES_WIDTH; QCursor::setPos( mapToGlobal( QPoint( width(), mouseEvent->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 = -m_selectStartTick; } m_selectedLevels = level - m_selectStartLevel; if( level <= m_selectStartLevel ) { --m_selectedLevels; } } else if( mouseEvent->buttons() & Qt::LeftButton && m_editMode == MOVE && m_action == MOVE_SELECTION ) { // move selection + selected values // do horizontal move-stuff int pos_ticks = x * MidiTime::ticksPerTact() / m_ppt + m_currentPosition; int ticks_diff = pos_ticks - m_moveStartTick; if( m_selectedTick > 0 ) { if( (int) m_selectStartTick + ticks_diff < 0 ) { ticks_diff = -m_selectStartTick; } } else { if( (int) m_selectStartTick + m_selectedTick + ticks_diff < 0 ) { ticks_diff = -( m_selectStartTick + m_selectedTick ); } } m_selectStartTick += ticks_diff; int tact_diff = ticks_diff / MidiTime::ticksPerTact(); ticks_diff = ticks_diff % MidiTime::ticksPerTact(); // do vertical move-stuff float level_diff = level - m_moveStartLevel; if( m_selectedLevels > 0 ) { if( m_selectStartLevel + level_diff < m_minLevel ) { level_diff = m_minLevel - m_selectStartLevel; } else if( m_selectStartLevel + m_selectedLevels + level_diff > m_maxLevel ) { level_diff = m_maxLevel - m_selectStartLevel - m_selectedLevels; } } else { if( m_selectStartLevel + m_selectedLevels + level_diff < m_minLevel ) { level_diff = m_minLevel - m_selectStartLevel - m_selectedLevels; } else if( m_selectStartLevel + level_diff > m_maxLevel ) { level_diff = m_maxLevel - m_selectStartLevel; } } m_selectStartLevel += level_diff; timeMap new_selValuesForMove; for( timeMap::iterator it = m_selValuesForMove.begin(); it != m_selValuesForMove.end(); ++it ) { MidiTime new_value_pos; if( it.key() ) { int value_tact = ( it.key() / MidiTime::ticksPerTact() ) + tact_diff; int value_ticks = ( it.key() % MidiTime::ticksPerTact() ) + ticks_diff; // ensure value_ticks range if( value_ticks / MidiTime::ticksPerTact() ) { value_tact += value_ticks / MidiTime::ticksPerTact(); value_ticks %= MidiTime::ticksPerTact(); } m_pattern->removeValue( it.key() ); new_value_pos = MidiTime( value_tact, value_ticks ); } new_selValuesForMove[ m_pattern->putValue( new_value_pos, it.value () + level_diff, false )] = it.value() + level_diff; } m_selValuesForMove = new_selValuesForMove; m_moveStartTick = pos_ticks; m_moveStartLevel = level; } } else { if( mouseEvent->buttons() & Qt::LeftButton && m_editMode == SELECT && m_action == SELECT_VALUES ) { int x = mouseEvent->x() - VALUES_WIDTH; if( x < 0 && m_currentPosition > 0 ) { x = 0; QCursor::setPos( mapToGlobal( QPoint( VALUES_WIDTH, mouseEvent->y() ) ) ); if( m_currentPosition >= 4 ) { m_leftRightScroll->setValue( m_currentPosition - 4 ); } else { m_leftRightScroll->setValue( 0 ); } } else if( x > width() - VALUES_WIDTH ) { x = width() - VALUES_WIDTH; QCursor::setPos( mapToGlobal( QPoint( width(), mouseEvent->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 = -m_selectStartTick; } float level = getLevel( mouseEvent->y() ); if( level <= m_bottomLevel ) { QCursor::setPos( mapToGlobal( QPoint( mouseEvent->x(), height() - SCROLLBAR_SIZE ) ) ); m_topBottomScroll->setValue( m_topBottomScroll->value() + 1 ); level = m_bottomLevel; } else if( level >= m_topLevel ) { QCursor::setPos( mapToGlobal( QPoint( mouseEvent->x(), TOP_MARGIN ) ) ); m_topBottomScroll->setValue( m_topBottomScroll->value() - 1 ); level = m_topLevel; } m_selectedLevels = level - m_selectStartLevel; if( level <= m_selectStartLevel ) { --m_selectedLevels; } } QApplication::restoreOverrideCursor(); } update(); } inline void AutomationEditor::drawCross( QPainter & p ) { QPoint mouse_pos = mapFromGlobal( QCursor::pos() ); float level = getLevel( mouse_pos.y() ); int grid_bottom = height() - SCROLLBAR_SIZE - 1; float cross_y = m_y_auto ? grid_bottom - ( ( grid_bottom - TOP_MARGIN ) * ( level - m_minLevel ) / (float)( m_maxLevel - m_minLevel ) ) : grid_bottom - ( level - m_bottomLevel ) * m_y_delta; p.setPen( crossColor() ); p.drawLine( VALUES_WIDTH, (int) cross_y, width(), (int) cross_y ); p.drawLine( mouse_pos.x(), TOP_MARGIN, mouse_pos.x(), height() - SCROLLBAR_SIZE ); QPoint tt_pos = QCursor::pos(); tt_pos.ry() -= 64; tt_pos.rx() += 32; float scaledLevel = m_pattern->firstObject()->scaledValue( level ); QToolTip::showText( tt_pos, QString::number( scaledLevel ), this ); } inline void AutomationEditor::drawAutomationPoint( QPainter & p, timeMap::iterator it ) { int x = xCoordOfTick( it.key() ); int y = yCoordOfLevel( it.value() ); const int outerRadius = qBound( 2, ( m_ppt * AutomationPattern::quantization() ) / 576, 5 ); // man, getting this calculation right took forever p.setPen( QPen( vertexColor().lighter( 200 ) ) ); p.setBrush( QBrush( vertexColor() ) ); p.drawEllipse( x - outerRadius, y - outerRadius, outerRadius * 2, outerRadius * 2 ); } void AutomationEditor::paintEvent(QPaintEvent * pe ) { QMutexLocker m( &m_patternMutex ); QStyleOption opt; opt.initFrom( this ); QPainter p( this ); style()->drawPrimitive( QStyle::PE_Widget, &opt, &p, this ); // get foreground color QColor fgColor = p.pen().brush().color(); // get background color and fill background QBrush bgColor = p.background(); p.fillRect( 0, 0, width(), height(), bgColor ); // set font-size to 8 p.setFont( pointSize<8>( p.font() ) ); int grid_height = height() - TOP_MARGIN - SCROLLBAR_SIZE; // start drawing at the bottom int grid_bottom = height() - SCROLLBAR_SIZE - 1; p.fillRect( 0, TOP_MARGIN, VALUES_WIDTH, height() - TOP_MARGIN, scaleColor() ); // print value numbers int font_height = p.fontMetrics().height(); Qt::Alignment text_flags = (Qt::Alignment)( Qt::AlignRight | Qt::AlignVCenter ); if( validPattern() ) { if( m_y_auto ) { int y[] = { grid_bottom, TOP_MARGIN + font_height / 2 }; float level[] = { m_minLevel, m_maxLevel }; for( int i = 0; i < 2; ++i ) { const QString & label = m_pattern->firstObject() ->displayValue( level[i] ); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::Shadow ) ); p.drawText( 1, y[i] - font_height + 1, VALUES_WIDTH - 10, 2 * font_height, text_flags, label ); p.setPen( fgColor ); p.drawText( 0, y[i] - font_height, VALUES_WIDTH - 10, 2 * font_height, text_flags, label ); } } else { int y; int level = (int) m_bottomLevel; int printable = qMax( 1, 5 * DEFAULT_Y_DELTA / m_y_delta ); int module = level % printable; if( module ) { int inv_module = ( printable - module ) % printable; level += inv_module; } for( ; level <= m_topLevel; level += printable ) { const QString & label = m_pattern->firstObject() ->displayValue( level ); y = yCoordOfLevel( level ); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::Shadow ) ); p.drawText( 1, y - font_height + 1, VALUES_WIDTH - 10, 2 * font_height, text_flags, label ); p.setPen( fgColor ); p.drawText( 0, y - font_height, VALUES_WIDTH - 10, 2 * font_height, text_flags, label ); } } } // set clipping area, because we are not allowed to paint over // keyboard... p.setClipRect( VALUES_WIDTH, TOP_MARGIN, width() - VALUES_WIDTH, grid_height ); // draw vertical raster if( m_pattern ) { int tick, x, q; int x_line_end = (int)( m_y_auto || m_topLevel < m_maxLevel ? TOP_MARGIN : grid_bottom - ( m_topLevel - m_bottomLevel ) * m_y_delta ); if( m_zoomingXModel.value() > 3 ) { // If we're over 100% zoom, we allow all quantization level grids q = AutomationPattern::quantization(); } else if( AutomationPattern::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 = AutomationPattern::quantization() < 8 ? 8 : AutomationPattern::quantization(); } else { // If we're under 100% zoom, we allow quantization grid up to 1/32 for normal notes q = AutomationPattern::quantization() < 6 ? 6 : AutomationPattern::quantization(); } // 3 independent loops, because quantization might not divide evenly into // exotic denominators (e.g. 7/11 time), which are allowed ATM. // First quantization grid... for( tick = m_currentPosition - m_currentPosition % q, x = xCoordOfTick( tick ); x<=width(); tick += q, x = xCoordOfTick( tick ) ) { p.setPen( lineColor() ); p.drawLine( x, grid_bottom, x, x_line_end ); } /// \todo move this horizontal line drawing code into the same loop as the value ticks? if( m_y_auto ) { QPen pen( beatLineColor() ); pen.setStyle( Qt::DotLine ); p.setPen( pen ); float y_delta = ( grid_bottom - TOP_MARGIN ) / 8.0f; for( int i = 1; i < 8; ++i ) { int y = (int)( grid_bottom - i * y_delta ); p.drawLine( VALUES_WIDTH, y, width(), y ); } } else { float y; for( int level = (int)m_bottomLevel; level <= m_topLevel; level++) { y = yCoordOfLevel( (float)level ); if( level % 10 == 0 ) { p.setPen( beatLineColor() ); } else { p.setPen( lineColor() ); } // draw level line p.drawLine( VALUES_WIDTH, (int) y, width(), (int) y ); } } // alternating shades for better contrast // count the bars which disappear on left by scrolling float zoomFactor = m_zoomXLevels[m_zoomingXModel.value()]; int barCount = m_currentPosition / MidiTime::ticksPerTact(); int leftBars = m_currentPosition * zoomFactor / m_ppt; for( int x = VALUES_WIDTH; x < width() + m_currentPosition * zoomFactor; x += m_ppt, ++barCount ) { if( ( barCount + leftBars ) % 2 != 0 ) { p.fillRect( x - m_currentPosition * zoomFactor, TOP_MARGIN, m_ppt, height() - ( SCROLLBAR_SIZE + TOP_MARGIN ), backgroundShade() ); } } // Draw the beat grid int ticksPerBeat = DefaultTicksPerTact / Engine::getSong()->getTimeSigModel().getDenominator(); // triplet mode occurs if the note quantization isn't a multiple of 3 // note that the automation editor does not support triplets yet if( AutomationPattern::quantization() % 3 != 0 ) { ticksPerBeat = static_cast( ticksPerBeat * 2.0/3.0 ); } for( tick = m_currentPosition - m_currentPosition % ticksPerBeat, x = xCoordOfTick( tick ); x<=width(); tick += ticksPerBeat, x = xCoordOfTick( tick ) ) { p.setPen( beatLineColor() ); p.drawLine( x, grid_bottom, x, x_line_end ); } // and finally bars 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, grid_bottom, x, x_line_end ); } } // following code draws all visible values // 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( sel_pos_start, sel_pos_end ); } float selLevel_start = m_selectStartLevel; float selLevel_end = selLevel_start + m_selectedLevels; if( selLevel_start > selLevel_end ) { qSwap( selLevel_start, selLevel_end ); } if( validPattern() ) { //NEEDS Change in CSS //int len_ticks = 4; timeMap & time_map = m_pattern->getTimeMap(); //Don't bother doing/rendering anything if there is no automation points if( time_map.size() > 0 ) { timeMap::iterator it = time_map.begin(); while( it+1 != time_map.end() ) { // skip this section if it occurs completely before the // visible area int next_x = xCoordOfTick( (it+1).key() ); if( next_x < 0 ) { ++it; continue; } int x = xCoordOfTick( it.key() ); if( x > width() ) { break; } //NEEDS Change in CSS /*bool is_selected = false; // if we're in move-mode, we may only draw // values in selected area, that have originally // been selected and not values that are now in // selection because the user moved it... if( m_editMode == MOVE ) { if( m_selValuesForMove.contains( it.key() ) ) { is_selected = true; } } else if( it.value() >= selLevel_start && it.value() <= selLevel_end && it.key() >= sel_pos_start && it.key() + len_ticks <= sel_pos_end ) { is_selected = true; }*/ float *values = m_pattern->valuesAfter( it.key() ); float nextValue; if ( m_pattern->valuesAfter( ( it + 1 ).key() ) != NULL ) nextValue = *( m_pattern->valuesAfter( ( it + 1 ).key() ) ); else nextValue = values[ ( it + 1 ).key() - it.key() -1 ]; p.setRenderHints( QPainter::Antialiasing, true ); QPainterPath path; path.moveTo( QPointF( xCoordOfTick( it.key() ), yCoordOfLevel( 0 ) ) ); for( int i = 0; i < ( it + 1 ).key() - it.key(); i++ ) { path.lineTo( QPointF( xCoordOfTick( it.key() + i ), yCoordOfLevel( values[i] ) ) ); //NEEDS Change in CSS //drawLevelTick( p, it.key() + i, values[i], is_selected ); } path.lineTo( QPointF( xCoordOfTick( ( it + 1 ).key() ), yCoordOfLevel( nextValue ) ) ); path.lineTo( QPointF( xCoordOfTick( ( it + 1 ).key() ), yCoordOfLevel( 0 ) ) ); path.lineTo( QPointF( xCoordOfTick( it.key() ), yCoordOfLevel( 0 ) ) ); p.fillPath( path, graphColor() ); p.setRenderHints( QPainter::Antialiasing, false ); delete [] values; // Draw circle drawAutomationPoint(p, it); ++it; } for( int i = it.key(), x = xCoordOfTick( i ); x <= width(); i++, x = xCoordOfTick( i ) ) { // TODO: Find out if the section after the last control // point is able to be selected and if so set this // boolean correctly drawLevelTick( p, i, it.value()); ////NEEDS Change in CSS:, false ); } // Draw circle(the last one) drawAutomationPoint(p, it); } } else { QFont f = p.font(); f.setBold( true ); p.setFont( pointSize<14>( f ) ); p.setPen( QApplication::palette().color( QPalette::Active, QPalette::BrightText ) ); p.drawText( VALUES_WIDTH + 20, TOP_MARGIN + 40, width() - VALUES_WIDTH - 20 - SCROLLBAR_SIZE, grid_height - 40, Qt::TextWordWrap, tr( "Please open an automation pattern with " "the context menu of a control!" ) ); } // now draw selection-frame int x = ( sel_pos_start - m_currentPosition ) * m_ppt / MidiTime::ticksPerTact(); int w = ( sel_pos_end - sel_pos_start ) * m_ppt / MidiTime::ticksPerTact(); int y, h; if( m_y_auto ) { y = (int)( grid_bottom - ( ( grid_bottom - TOP_MARGIN ) * ( selLevel_start - m_minLevel ) / (float)( m_maxLevel - m_minLevel ) ) ); h = (int)( grid_bottom - ( ( grid_bottom - TOP_MARGIN ) * ( selLevel_end - m_minLevel ) / (float)( m_maxLevel - m_minLevel ) ) - y ); } else { y = (int)( grid_bottom - ( selLevel_start - m_bottomLevel ) * m_y_delta ); h = (int)( ( selLevel_start - selLevel_end ) * m_y_delta ); } p.setPen( QColor( 0, 64, 192 ) ); p.drawRect( x + VALUES_WIDTH, y, w, h ); // TODO: Get this out of paint event int l = validPattern() ? (int) m_pattern->length() : 0; // reset scroll-range if( m_leftRightScroll->maximum() != l ) { m_leftRightScroll->setRange( 0, l ); m_leftRightScroll->setPageStep( l ); } if( validPattern() && GuiApplication::instance()->automationEditor()->hasFocus() ) { drawCross( p ); } const QPixmap * cursor = NULL; // draw current edit-mode-icon below the cursor switch( m_editMode ) { case DRAW: cursor = s_toolDraw; break; case ERASE: cursor = s_toolErase; break; case SELECT: cursor = s_toolSelect; break; case MOVE: cursor = s_toolMove; break; } p.drawPixmap( mapFromGlobal( QCursor::pos() ) + QPoint( 8, 8 ), *cursor ); } int AutomationEditor::xCoordOfTick(int tick ) { return VALUES_WIDTH + ( ( tick - m_currentPosition ) * m_ppt / MidiTime::ticksPerTact() ); } float AutomationEditor::yCoordOfLevel(float level ) { int grid_bottom = height() - SCROLLBAR_SIZE; if( m_y_auto ) { return ( grid_bottom - ( grid_bottom - TOP_MARGIN ) * ( level - m_minLevel ) / ( m_maxLevel - m_minLevel ) ); } else { return ( grid_bottom - ( level - m_bottomLevel ) * m_y_delta ); } } //NEEDS Change in CSS void AutomationEditor::drawLevelTick(QPainter & p, int tick, float value) // bool is_selected ) { int grid_bottom = height() - SCROLLBAR_SIZE - 1; const int x = xCoordOfTick( tick ); int rect_width = xCoordOfTick( tick+1 ) - x; // is the level in visible area? if( ( value >= m_bottomLevel && value <= m_topLevel ) || ( value > m_topLevel && m_topLevel >= 0 ) || ( value < m_bottomLevel && m_bottomLevel <= 0 ) ) { int y_start = yCoordOfLevel( value ); int rect_height; if( m_y_auto ) { int y_end = (int)( grid_bottom + ( grid_bottom - TOP_MARGIN ) * m_minLevel / ( m_maxLevel - m_minLevel ) ); rect_height = y_end - y_start; } else { rect_height = (int)( value * m_y_delta ); } //NEEDS Change in CSS /*QBrush currentColor = is_selected ? QBrush( QColor( 0x00, 0x40, 0xC0 ) ) : graphColor(); */ QBrush currentColor = graphColor(); p.fillRect( x, y_start, rect_width, rect_height, currentColor ); } else { printf("not in range\n"); } } // responsible for moving/resizing scrollbars after window-resizing void AutomationEditor::resizeEvent(QResizeEvent * re) { m_leftRightScroll->setGeometry( VALUES_WIDTH, height() - SCROLLBAR_SIZE, width() - VALUES_WIDTH, SCROLLBAR_SIZE ); int grid_height = height() - TOP_MARGIN - SCROLLBAR_SIZE; m_topBottomScroll->setGeometry( width() - SCROLLBAR_SIZE, TOP_MARGIN, SCROLLBAR_SIZE, grid_height ); int half_grid = grid_height / 2; int total_pixels = (int)( ( m_maxLevel - m_minLevel ) * m_y_delta + 1 ); if( !m_y_auto && grid_height < total_pixels ) { int min_scroll = (int)( m_minLevel + floorf( half_grid / (float)m_y_delta ) ); int max_scroll = (int)( m_maxLevel - (int)floorf( ( grid_height - half_grid ) / (float)m_y_delta ) ); m_topBottomScroll->setRange( min_scroll, max_scroll ); } else { m_topBottomScroll->setRange( (int) m_scrollLevel, (int) m_scrollLevel ); } m_topBottomScroll->setValue( (int) m_scrollLevel ); if( Engine::getSong() ) { Engine::getSong()->getPlayPos( Song::Mode_PlayAutomationPattern ).m_timeLine->setFixedWidth( width() ); } updateTopBottomLevels(); update(); } void AutomationEditor::wheelEvent(QWheelEvent * we ) { we->accept(); if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::ShiftModifier ) { int y = m_zoomingYModel.value(); if( we->delta() > 0 ) { y++; } if( we->delta() < 0 ) { y--; } y = qBound( 0, y, m_zoomingYModel.size() - 1 ); m_zoomingYModel.setValue( y ); } else if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::AltModifier ) { int q = m_quantizeModel.value(); if( we->delta() > 0 ) { q--; } if( we->delta() < 0 ) { q++; } q = qBound( 0, q, m_quantizeModel.size() - 1 ); m_quantizeModel.setValue( q ); update(); } else if( we->modifiers() & Qt::ControlModifier ) { int x = m_zoomingXModel.value(); if( we->delta() > 0 ) { x--; } if( we->delta() < 0 ) { x++; } x = qBound( 0, x, m_zoomingXModel.size() - 1 ); m_zoomingXModel.setValue( x ); } 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 ); } } float AutomationEditor::getLevel(int y ) { int level_line_y = height() - SCROLLBAR_SIZE - 1; // pressed level float level = roundf( ( m_bottomLevel + ( m_y_auto ? ( m_maxLevel - m_minLevel ) * ( level_line_y - y ) / (float)( level_line_y - ( TOP_MARGIN + 2 ) ) : ( level_line_y - y ) / (float)m_y_delta ) ) / m_step ) * m_step; // some range-checking-stuff level = qBound( m_bottomLevel, level, m_topLevel ); return( level ); } inline bool AutomationEditor::inBBEditor() { QMutexLocker m( &m_patternMutex ); return( validPattern() && m_pattern->getTrack()->trackContainer() == Engine::getBBTrackContainer() ); } void AutomationEditor::play() { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } if( !m_pattern->getTrack() ) { if( Engine::getSong()->playMode() != Song::Mode_PlayPattern ) { Engine::getSong()->stop(); Engine::getSong()->playPattern( gui->pianoRoll()->currentPattern() ); } else if( Engine::getSong()->isStopped() == false ) { Engine::getSong()->togglePause(); } else { Engine::getSong()->playPattern( gui->pianoRoll()->currentPattern() ); } } else if( inBBEditor() ) { Engine::getBBTrackContainer()->play(); } else { if( Engine::getSong()->isStopped() == true ) { Engine::getSong()->playSong(); } else { Engine::getSong()->togglePause(); } } } void AutomationEditor::stop() { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } if( m_pattern->getTrack() && inBBEditor() ) { Engine::getBBTrackContainer()->stop(); } else { Engine::getSong()->stop(); } m_scrollBack = true; } void AutomationEditor::horScrolled(int new_pos ) { m_currentPosition = new_pos; emit positionChanged( m_currentPosition ); update(); } void AutomationEditor::verScrolled(int new_pos ) { m_scrollLevel = new_pos; updateTopBottomLevels(); update(); } void AutomationEditor::setEditMode(AutomationEditor::EditModes mode) { if (m_editMode == mode) return; m_editMode = mode; switch (mode) { case DRAW: case ERASE: case SELECT: removeSelection(); break; case MOVE: m_selValuesForMove.clear(); getSelectedValues(m_selValuesForMove); } update(); } void AutomationEditor::setEditMode(int mode) { setEditMode((AutomationEditor::EditModes) mode); } void AutomationEditor::setProgressionType(AutomationPattern::ProgressionTypes type) { if (validPattern()) { m_pattern->addJournalCheckPoint(); QMutexLocker m(&m_patternMutex); m_pattern->setProgressionType(type); Engine::getSong()->setModified(); update(); } } void AutomationEditor::setProgressionType(int type) { setProgressionType((AutomationPattern::ProgressionTypes) type); } void AutomationEditor::setTension() { if ( m_pattern ) { m_pattern->setTension( QString::number( m_tensionModel->value() ) ); update(); } } void AutomationEditor::selectAll() { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } timeMap & time_map = m_pattern->getTimeMap(); timeMap::iterator it = time_map.begin(); m_selectStartTick = 0; m_selectedTick = m_pattern->length(); m_selectStartLevel = it.value(); m_selectedLevels = 1; while( ++it != time_map.end() ) { const float level = it.value(); if( level < m_selectStartLevel ) { // if we move start-level down, we have to add // the difference between old and new start-level // to m_selectedLevels, otherwise the selection // is just moved down... m_selectedLevels += m_selectStartLevel - level; m_selectStartLevel = level; } else if( level >= m_selectStartLevel + m_selectedLevels ) { m_selectedLevels = level - m_selectStartLevel + 1; } } } // returns vector with pointers to all selected values void AutomationEditor::getSelectedValues( timeMap & selected_values ) { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } int sel_pos_start = m_selectStartTick; int sel_pos_end = sel_pos_start + m_selectedTick; if( sel_pos_start > sel_pos_end ) { qSwap( sel_pos_start, sel_pos_end ); } float selLevel_start = m_selectStartLevel; float selLevel_end = selLevel_start + m_selectedLevels; if( selLevel_start > selLevel_end ) { qSwap( selLevel_start, selLevel_end ); } timeMap & time_map = m_pattern->getTimeMap(); for( timeMap::iterator it = time_map.begin(); it != time_map.end(); ++it ) { //TODO: Add constant tick_t len_ticks = MidiTime::ticksPerTact() / 16; float level = it.value(); tick_t pos_ticks = it.key(); if( level >= selLevel_start && level <= selLevel_end && pos_ticks >= sel_pos_start && pos_ticks + len_ticks <= sel_pos_end ) { selected_values[it.key()] = level; } } } void AutomationEditor::copySelectedValues() { m_valuesToCopy.clear(); timeMap selected_values; getSelectedValues( selected_values ); if( !selected_values.isEmpty() ) { for( timeMap::iterator it = selected_values.begin(); it != selected_values.end(); ++it ) { m_valuesToCopy[it.key()] = it.value(); } TextFloat::displayMessage( tr( "Values copied" ), tr( "All selected values were copied to the " "clipboard." ), embed::getIconPixmap( "edit_copy" ), 2000 ); } } void AutomationEditor::cutSelectedValues() { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } m_pattern->addJournalCheckPoint(); m_valuesToCopy.clear(); timeMap selected_values; getSelectedValues( selected_values ); if( !selected_values.isEmpty() ) { Engine::getSong()->setModified(); for( timeMap::iterator it = selected_values.begin(); it != selected_values.end(); ++it ) { m_valuesToCopy[it.key()] = it.value(); m_pattern->removeValue( it.key() ); } } update(); gui->songEditor()->update(); } void AutomationEditor::pasteValues() { QMutexLocker m( &m_patternMutex ); if( validPattern() && !m_valuesToCopy.isEmpty() ) { m_pattern->addJournalCheckPoint(); for( timeMap::iterator it = m_valuesToCopy.begin(); it != m_valuesToCopy.end(); ++it ) { m_pattern->putValue( it.key() + m_currentPosition, it.value() ); } // we only have to do the following lines if we pasted at // least one value... Engine::getSong()->setModified(); update(); gui->songEditor()->update(); } } void AutomationEditor::deleteSelectedValues() { QMutexLocker m( &m_patternMutex ); if( !validPattern() ) { return; } m_pattern->addJournalCheckPoint(); timeMap selected_values; getSelectedValues( selected_values ); const bool update_after_delete = !selected_values.empty(); for( timeMap::iterator it = selected_values.begin(); it != selected_values.end(); ++it ) { m_pattern->removeValue( it.key() ); } if( update_after_delete == true ) { Engine::getSong()->setModified(); update(); gui->songEditor()->update(); } } void AutomationEditor::updatePosition(const MidiTime & t ) { if( ( Engine::getSong()->isPlaying() && Engine::getSong()->playMode() == Song::Mode_PlayAutomationPattern ) || m_scrollBack == true ) { const int w = width() - VALUES_WIDTH; if( t > m_currentPosition + w * MidiTime::ticksPerTact() / m_ppt ) { m_leftRightScroll->setValue( t.getTact() * MidiTime::ticksPerTact() ); } else if( t < m_currentPosition ) { MidiTime t_ = qMax( t - w * MidiTime::ticksPerTact() * MidiTime::ticksPerTact() / m_ppt, 0 ); m_leftRightScroll->setValue( t_.getTact() * MidiTime::ticksPerTact() ); } m_scrollBack = false; } } void AutomationEditor::zoomingXChanged() { m_ppt = m_zoomXLevels[m_zoomingXModel.value()] * DEFAULT_PPT; assert( m_ppt > 0 ); m_timeLine->setPixelsPerTact( m_ppt ); update(); } void AutomationEditor::zoomingYChanged() { const QString & zfac = m_zoomingYModel.currentText(); m_y_auto = zfac == "Auto"; if( !m_y_auto ) { m_y_delta = zfac.left( zfac.length() - 1 ).toInt() * DEFAULT_Y_DELTA / 100; } #ifdef LMMS_DEBUG assert( m_y_delta > 0 ); #endif resizeEvent( NULL ); } void AutomationEditor::setQuantization() { int quantization = m_quantizeModel.value(); if( quantization < 7 ) { quantization = 1 << quantization; } else if( quantization < 12 ) { quantization = 1 << ( quantization - 7 ); quantization *= 3; } else { quantization = DefaultTicksPerTact; } quantization = DefaultTicksPerTact / quantization; AutomationPattern::setQuantization( quantization ); } void AutomationEditor::updateTopBottomLevels() { if( m_y_auto ) { m_bottomLevel = m_minLevel; m_topLevel = m_maxLevel; return; } int total_pixels = (int)( ( m_maxLevel - m_minLevel ) * m_y_delta + 1 ); int grid_height = height() - TOP_MARGIN - SCROLLBAR_SIZE; int half_grid = grid_height / 2; if( total_pixels > grid_height ) { int centralLevel = (int)( m_minLevel + m_maxLevel - m_scrollLevel ); m_bottomLevel = centralLevel - ( half_grid / (float)m_y_delta ); if( m_bottomLevel < m_minLevel ) { m_bottomLevel = m_minLevel; m_topLevel = m_minLevel + (int)floorf( grid_height / (float)m_y_delta ); } else { m_topLevel = m_bottomLevel + (int)floorf( grid_height / (float)m_y_delta ); if( m_topLevel > m_maxLevel ) { m_topLevel = m_maxLevel; m_bottomLevel = m_maxLevel - (int)floorf( grid_height / (float)m_y_delta ); } } } else { m_bottomLevel = m_minLevel; m_topLevel = m_maxLevel; } } AutomationEditorWindow::AutomationEditorWindow() : Editor(), m_editor(new AutomationEditor()) { setCentralWidget(m_editor); // Play/stop buttons m_playAction->setToolTip(tr( "Play/pause current pattern (Space)" )); m_playAction->setWhatsThis( tr( "Click here if you want to play the current pattern. " "This is useful while editing it. The pattern is " "automatically looped when the end is reached." ) ); m_stopAction->setToolTip(tr("Stop playing of current pattern (Space)")); m_stopAction->setWhatsThis( tr( "Click here if you want to stop playing of the " "current pattern." ) ); // Edit mode buttons DropToolBar *editActionsToolBar = addDropToolBarToTop(tr("Edit actions")); ActionGroup* editModeGroup = new ActionGroup(this); QAction* drawAction = editModeGroup->addAction(embed::getIconPixmap("edit_draw"), tr("Draw mode (Shift+D)")); drawAction->setShortcut(Qt::SHIFT | Qt::Key_D); drawAction->setChecked(true); QAction* eraseAction = editModeGroup->addAction(embed::getIconPixmap("edit_erase"), tr("Erase mode (Shift+E)")); eraseAction->setShortcut(Qt::SHIFT | Qt::Key_E); m_flipYAction = new QAction(embed::getIconPixmap("flip_y"), tr("Flip vertically"), this); m_flipXAction = new QAction(embed::getIconPixmap("flip_x"), tr("Flip horizontally"), this); m_flipYAction->setWhatsThis( tr( "Click here and the pattern will be inverted." "The points are flipped in the y direction. " ) ); m_flipXAction->setWhatsThis( tr( "Click here and the pattern will be reversed. " "The points are flipped in the x direction." ) ); // TODO: m_selectButton and m_moveButton are broken. // m_selectButton = new QAction(embed::getIconPixmap("edit_select"), tr("Select mode (Shift+S)"), editModeGroup); // m_moveButton = new QAction(embed::getIconPixmap("edit_move"), tr("Move selection mode (Shift+M)"), editModeGroup); drawAction->setWhatsThis( tr( "Click here and draw-mode will be activated. In this " "mode you can add and move single values. 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." ) ); eraseAction->setWhatsThis( tr( "Click here and erase-mode will be activated. In this " "mode you can erase single values. You can also press " "'Shift+E' on your keyboard to activate this mode." ) ); /*m_selectButton->setWhatsThis( tr( "Click here and select-mode will be activated. In this " "mode you can select values. This is necessary " "if you want to cut, copy, paste, delete, or move " "values. You can also press 'Shift+S' on your keyboard " "to activate this mode." ) ); m_moveButton->setWhatsThis( tr( "If you click here, move-mode will be activated. In this " "mode you can move the values you selected in select-" "mode. You can also press 'Shift+M' on your keyboard " "to activate this mode." ) );*/ connect(editModeGroup, SIGNAL(triggered(int)), m_editor, SLOT(setEditMode(int))); editActionsToolBar->addAction(drawAction); editActionsToolBar->addAction(eraseAction); // editActionsToolBar->addAction(m_selectButton); // editActionsToolBar->addAction(m_moveButton); editActionsToolBar->addAction(m_flipXAction); editActionsToolBar->addAction(m_flipYAction); // Interpolation actions DropToolBar *interpolationActionsToolBar = addDropToolBarToTop(tr("Interpolation controls")); ActionGroup* progression_type_group = new ActionGroup(this); m_discreteAction = progression_type_group->addAction( embed::getIconPixmap("progression_discrete"), tr("Discrete progression")); m_discreteAction->setChecked(true); m_linearAction = progression_type_group->addAction( embed::getIconPixmap("progression_linear"), tr("Linear progression")); m_cubicHermiteAction = progression_type_group->addAction( embed::getIconPixmap("progression_cubic_hermite"), tr( "Cubic Hermite progression")); connect(progression_type_group, SIGNAL(triggered(int)), m_editor, SLOT(setProgressionType(int))); // setup tension-stuff m_tensionKnob = new Knob( knobSmall_17, this, "Tension" ); m_tensionKnob->setModel(m_editor->m_tensionModel); ToolTip::add(m_tensionKnob, tr("Tension value for spline")); m_tensionKnob->setWhatsThis( tr("A higher tension value may make a smoother curve " "but overshoot some values. A low tension " "value will cause the slope of the curve to " "level off at each control point.")); connect(m_cubicHermiteAction, SIGNAL(toggled(bool)), m_tensionKnob, SLOT(setEnabled(bool))); m_discreteAction->setWhatsThis( tr( "Click here to choose discrete progressions for this " "automation pattern. The value of the connected " "object will remain constant between control points " "and be set immediately to the new value when each " "control point is reached." ) ); m_linearAction->setWhatsThis( tr( "Click here to choose linear progressions for this " "automation pattern. The value of the connected " "object will change at a steady rate over time " "between control points to reach the correct value at " "each control point without a sudden change." ) ); m_cubicHermiteAction->setWhatsThis( tr( "Click here to choose cubic hermite progressions for this " "automation pattern. The value of the connected " "object will change in a smooth curve and ease in to " "the peaks and valleys." ) ); interpolationActionsToolBar->addSeparator(); interpolationActionsToolBar->addAction(m_discreteAction); interpolationActionsToolBar->addAction(m_linearAction); interpolationActionsToolBar->addAction(m_cubicHermiteAction); interpolationActionsToolBar->addSeparator(); interpolationActionsToolBar->addWidget( new QLabel( tr("Tension: "), interpolationActionsToolBar )); interpolationActionsToolBar->addWidget( m_tensionKnob ); // Copy paste buttons /*DropToolBar *copyPasteActionsToolBar = addDropToolBarToTop(tr("Copy paste actions"));*/ QAction* cutAction = new QAction(embed::getIconPixmap("edit_cut"), tr("Cut selected values (%1+X)").arg( #ifdef LMMS_BUILD_APPLE "⌘"), this); #else "Ctrl"), this); #endif QAction* copyAction = new QAction(embed::getIconPixmap("edit_copy"), tr("Copy selected values (%1+C)").arg( #ifdef LMMS_BUILD_APPLE "⌘"), this); #else "Ctrl"), this); #endif QAction* pasteAction = new QAction(embed::getIconPixmap("edit_paste"), tr("Paste values from clipboard (%1+V)").arg( #ifdef LMMS_BUILD_APPLE "⌘"), this); #else "Ctrl"), this); #endif cutAction->setWhatsThis( tr( "Click here and selected values 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 selected values 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 values 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(cutSelectedValues())); connect(copyAction, SIGNAL(triggered()), m_editor, SLOT(copySelectedValues())); connect(pasteAction, SIGNAL(triggered()), m_editor, SLOT(pasteValues())); // Select is broken // copyPasteActionsToolBar->addAction( cutAction ); // copyPasteActionsToolBar->addAction( copyAction ); // copyPasteActionsToolBar->addAction( pasteAction ); DropToolBar *timeLineToolBar = addDropToolBarToTop(tr("Timeline controls")); m_editor->m_timeLine->addToolButtons(timeLineToolBar); addToolBarBreak(); // Zoom controls DropToolBar *zoomToolBar = addDropToolBarToTop(tr("Zoom controls")); QLabel * zoom_x_label = new QLabel( zoomToolBar ); zoom_x_label->setPixmap( embed::getIconPixmap( "zoom_x" ) ); m_zoomingXComboBox = new ComboBox( zoomToolBar ); m_zoomingXComboBox->setFixedSize( 80, 22 ); for( float const & zoomLevel : m_editor->m_zoomXLevels ) { m_editor->m_zoomingXModel.addItem( QString( "%1\%" ).arg( zoomLevel * 100 ) ); } m_editor->m_zoomingXModel.setValue( m_editor->m_zoomingXModel.findText( "100%" ) ); m_zoomingXComboBox->setModel( &m_editor->m_zoomingXModel ); connect( &m_editor->m_zoomingXModel, SIGNAL( dataChanged() ), m_editor, SLOT( zoomingXChanged() ) ); QLabel * zoom_y_label = new QLabel( zoomToolBar ); zoom_y_label->setPixmap( embed::getIconPixmap( "zoom_y" ) ); m_zoomingYComboBox = new ComboBox( zoomToolBar ); m_zoomingYComboBox->setFixedSize( 80, 22 ); m_editor->m_zoomingYModel.addItem( "Auto" ); for( int i = 0; i < 7; ++i ) { m_editor->m_zoomingYModel.addItem( QString::number( 25 << i ) + "%" ); } m_editor->m_zoomingYModel.setValue( m_editor->m_zoomingYModel.findText( "Auto" ) ); m_zoomingYComboBox->setModel( &m_editor->m_zoomingYModel ); connect( &m_editor->m_zoomingYModel, SIGNAL( dataChanged() ), m_editor, SLOT( zoomingYChanged() ) ); zoomToolBar->addWidget( zoom_x_label ); zoomToolBar->addWidget( m_zoomingXComboBox ); zoomToolBar->addSeparator(); zoomToolBar->addWidget( zoom_y_label ); zoomToolBar->addWidget( m_zoomingYComboBox ); // Quantization controls DropToolBar *quantizationActionsToolBar = addDropToolBarToTop(tr("Quantization controls")); QLabel * quantize_lbl = new QLabel( m_toolBar ); quantize_lbl->setPixmap( embed::getIconPixmap( "quantize" ) ); m_quantizeComboBox = new ComboBox( m_toolBar ); m_quantizeComboBox->setFixedSize( 60, 22 ); m_quantizeComboBox->setModel( &m_editor->m_quantizeModel ); quantizationActionsToolBar->addWidget( quantize_lbl ); quantizationActionsToolBar->addWidget( m_quantizeComboBox ); m_quantizeComboBox->setToolTip( tr( "Quantization" ) ); m_quantizeComboBox->setWhatsThis( tr( "Quantization. Sets the smallest " "step size for the Automation Point. By default " "this also sets the length, clearing out other " "points in the range. Press to override " "this behaviour." ) ); // Setup our actual window setFocusPolicy( Qt::StrongFocus ); setFocus(); setWindowIcon( embed::getIconPixmap( "automation" ) ); setAcceptDrops( true ); m_toolBar->setAcceptDrops( true ); } AutomationEditorWindow::~AutomationEditorWindow() { } void AutomationEditorWindow::setCurrentPattern(AutomationPattern* pattern) { // Disconnect our old pattern if (currentPattern() != nullptr) { m_editor->m_pattern->disconnect(this); m_flipXAction->disconnect(); m_flipYAction->disconnect(); } m_editor->setCurrentPattern(pattern); // Set our window's title if (pattern == nullptr) { setWindowTitle( tr( "Automation Editor - no pattern" ) ); return; } setWindowTitle( tr( "Automation Editor - %1" ).arg( m_editor->m_pattern->name() ) ); switch(m_editor->m_pattern->progressionType()) { case AutomationPattern::DiscreteProgression: m_discreteAction->setChecked(true); m_tensionKnob->setEnabled(false); break; case AutomationPattern::LinearProgression: m_linearAction->setChecked(true); m_tensionKnob->setEnabled(false); break; case AutomationPattern::CubicHermiteProgression: m_cubicHermiteAction->setChecked(true); m_tensionKnob->setEnabled(true); break; } // Connect new pattern if (pattern) { connect(pattern, SIGNAL(dataChanged()), this, SLOT(update())); connect( pattern, SIGNAL( dataChanged() ), this, SLOT( updateWindowTitle() ) ); connect(pattern, SIGNAL(destroyed()), this, SLOT(clearCurrentPattern())); connect(m_flipXAction, SIGNAL(triggered()), pattern, SLOT(flipX())); connect(m_flipYAction, SIGNAL(triggered()), pattern, SLOT(flipY())); } emit currentPatternChanged(); } const AutomationPattern* AutomationEditorWindow::currentPattern() { return m_editor->currentPattern(); } void AutomationEditorWindow::dropEvent( QDropEvent *_de ) { QString type = StringPairDrag::decodeKey( _de ); QString val = StringPairDrag::decodeValue( _de ); if( type == "automatable_model" ) { AutomatableModel * mod = dynamic_cast( Engine::projectJournal()-> journallingObject( val.toInt() ) ); if( mod != NULL ) { bool added = m_editor->m_pattern->addObject( mod ); if ( !added ) { TextFloat::displayMessage( mod->displayName(), tr( "Model is already connected " "to this pattern." ), embed::getIconPixmap( "automation" ), 2000 ); } setCurrentPattern( m_editor->m_pattern ); } } update(); } void AutomationEditorWindow::dragEnterEvent( QDragEnterEvent *_dee ) { StringPairDrag::processDragEnterEvent( _dee, "automatable_model" ); } void AutomationEditorWindow::open(AutomationPattern* pattern) { setCurrentPattern(pattern); parentWidget()->show(); show(); setFocus(); } QSize AutomationEditorWindow::sizeHint() const { return {INITIAL_WIDTH, INITIAL_HEIGHT}; } void AutomationEditorWindow::clearCurrentPattern() { m_editor->m_pattern = nullptr; setCurrentPattern(nullptr); } void AutomationEditorWindow::play() { m_editor->play(); setPauseIcon(Engine::getSong()->isPlaying()); } void AutomationEditorWindow::stop() { m_editor->stop(); } void AutomationEditorWindow::updateWindowTitle() { if ( m_editor->m_pattern == nullptr) { setWindowTitle( tr( "Automation Editor - no pattern" ) ); return; } setWindowTitle( tr( "Automation Editor - %1" ).arg( m_editor->m_pattern->name() ) ); }