1 /*
2 * AutomationPatternView.cpp - implementation of view for AutomationPattern
3 *
4 * Copyright (c) 2008-2010 Tobias Doerffel <tobydox/at/users.sourceforge.net>
5 *
6 * This file is part of LMMS - https://lmms.io
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public
19 * License along with this program (see COPYING); if not, write to the
20 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301 USA.
22 *
23 */
24 #include "AutomationPatternView.h"
25
26 #include <QMouseEvent>
27 #include <QPainter>
28 #include <QPainterPath>
29 #include <QMenu>
30
31 #include "AutomationEditor.h"
32 #include "embed.h"
33 #include "GuiApplication.h"
34 #include "gui_templates.h"
35 #include "ProjectJournal.h"
36 #include "RenameDialog.h"
37 #include "StringPairDrag.h"
38 #include "TextFloat.h"
39 #include "ToolTip.h"
40
41 #include "Engine.h"
42
43
44 QPixmap * AutomationPatternView::s_pat_rec = NULL;
45
AutomationPatternView(AutomationPattern * _pattern,TrackView * _parent)46 AutomationPatternView::AutomationPatternView( AutomationPattern * _pattern,
47 TrackView * _parent ) :
48 TrackContentObjectView( _pattern, _parent ),
49 m_pat( _pattern ),
50 m_paintPixmap()
51 {
52 connect( m_pat, SIGNAL( dataChanged() ),
53 this, SLOT( update() ) );
54 connect( gui->automationEditor(), SIGNAL( currentPatternChanged() ),
55 this, SLOT( update() ) );
56
57 setAttribute( Qt::WA_OpaquePaintEvent, true );
58
59 ToolTip::add( this, tr( "double-click to open this pattern in "
60 "automation editor" ) );
61 setStyle( QApplication::style() );
62
63 if( s_pat_rec == NULL ) { s_pat_rec = new QPixmap( embed::getIconPixmap(
64 "pat_rec" ) ); }
65
66 update();
67 }
68
69
70
71
~AutomationPatternView()72 AutomationPatternView::~AutomationPatternView()
73 {
74 }
75
76
77
78
openInAutomationEditor()79 void AutomationPatternView::openInAutomationEditor()
80 {
81 if(gui) gui->automationEditor()->open(m_pat);
82 }
83
84
85
86
resetName()87 void AutomationPatternView::resetName()
88 {
89 m_pat->setName( QString::null );
90 }
91
92
93
94
changeName()95 void AutomationPatternView::changeName()
96 {
97 QString s = m_pat->name();
98 RenameDialog rename_dlg( s );
99 rename_dlg.exec();
100 m_pat->setName( s );
101 update();
102 }
103
104
105
106
disconnectObject(QAction * _a)107 void AutomationPatternView::disconnectObject( QAction * _a )
108 {
109 JournallingObject * j = Engine::projectJournal()->
110 journallingObject( _a->data().toInt() );
111 if( j && dynamic_cast<AutomatableModel *>( j ) )
112 {
113 float oldMin = m_pat->getMin();
114 float oldMax = m_pat->getMax();
115
116 m_pat->m_objects.erase( qFind( m_pat->m_objects.begin(),
117 m_pat->m_objects.end(),
118 dynamic_cast<AutomatableModel *>( j ) ) );
119 update();
120
121 //If automation editor is opened, update its display after disconnection
122 if( gui->automationEditor() )
123 {
124 gui->automationEditor()->m_editor->updateAfterPatternChange();
125 }
126
127 //if there is no more connection connected to the AutomationPattern
128 if( m_pat->m_objects.size() == 0 )
129 {
130 //scale the points to fit the new min. and max. value
131 this->scaleTimemapToFit( oldMin, oldMax );
132 }
133 }
134 }
135
136
toggleRecording()137 void AutomationPatternView::toggleRecording()
138 {
139 m_pat->setRecording( ! m_pat->isRecording() );
140 update();
141 }
142
143
144
145
flipY()146 void AutomationPatternView::flipY()
147 {
148 m_pat->flipY( m_pat->getMin(), m_pat->getMax() );
149 update();
150 }
151
152
153
154
flipX()155 void AutomationPatternView::flipX()
156 {
157 m_pat->flipX( m_pat->length() );
158 update();
159 }
160
161
162
163
constructContextMenu(QMenu * _cm)164 void AutomationPatternView::constructContextMenu( QMenu * _cm )
165 {
166 QAction * a = new QAction( embed::getIconPixmap( "automation" ),
167 tr( "Open in Automation editor" ), _cm );
168 _cm->insertAction( _cm->actions()[0], a );
169 connect(a, SIGNAL(triggered()), this, SLOT(openInAutomationEditor()));
170 _cm->insertSeparator( _cm->actions()[1] );
171
172 _cm->addSeparator();
173
174 _cm->addAction( embed::getIconPixmap( "edit_erase" ),
175 tr( "Clear" ), m_pat, SLOT( clear() ) );
176 _cm->addSeparator();
177
178 _cm->addAction( embed::getIconPixmap( "reload" ), tr( "Reset name" ),
179 this, SLOT( resetName() ) );
180 _cm->addAction( embed::getIconPixmap( "edit_rename" ),
181 tr( "Change name" ),
182 this, SLOT( changeName() ) );
183 _cm->addAction( embed::getIconPixmap( "record" ),
184 tr( "Set/clear record" ),
185 this, SLOT( toggleRecording() ) );
186 _cm->addAction( embed::getIconPixmap( "flip_y" ),
187 tr( "Flip Vertically (Visible)" ),
188 this, SLOT( flipY() ) );
189 _cm->addAction( embed::getIconPixmap( "flip_x" ),
190 tr( "Flip Horizontally (Visible)" ),
191 this, SLOT( flipX() ) );
192 if( !m_pat->m_objects.isEmpty() )
193 {
194 _cm->addSeparator();
195 QMenu * m = new QMenu( tr( "%1 Connections" ).
196 arg( m_pat->m_objects.count() ), _cm );
197 for( AutomationPattern::objectVector::iterator it =
198 m_pat->m_objects.begin();
199 it != m_pat->m_objects.end(); ++it )
200 {
201 if( *it )
202 {
203 a = new QAction( tr( "Disconnect \"%1\"" ).
204 arg( ( *it )->fullDisplayName() ), m );
205 a->setData( ( *it )->id() );
206 m->addAction( a );
207 }
208 }
209 connect( m, SIGNAL( triggered( QAction * ) ),
210 this, SLOT( disconnectObject( QAction * ) ) );
211 _cm->addMenu( m );
212 }
213
214 _cm->addSeparator();
215 }
216
217
218
219
mouseDoubleClickEvent(QMouseEvent * me)220 void AutomationPatternView::mouseDoubleClickEvent( QMouseEvent * me )
221 {
222 if(me->button() != Qt::LeftButton)
223 {
224 me->ignore();
225 return;
226 }
227 openInAutomationEditor();
228 }
229
230
231
232
paintEvent(QPaintEvent *)233 void AutomationPatternView::paintEvent( QPaintEvent * )
234 {
235 QPainter painter( this );
236
237 if( !needsUpdate() )
238 {
239 painter.drawPixmap( 0, 0, m_paintPixmap );
240 return;
241 }
242
243 setNeedsUpdate( false );
244
245 m_paintPixmap = m_paintPixmap.isNull() == true || m_paintPixmap.size() != size()
246 ? QPixmap( size() ) : m_paintPixmap;
247
248 QPainter p( &m_paintPixmap );
249
250 QLinearGradient lingrad( 0, 0, 0, height() );
251 QColor c;
252 bool muted = m_pat->getTrack()->isMuted() || m_pat->isMuted();
253 bool current = gui->automationEditor()->currentPattern() == m_pat;
254
255 // state: selected, muted, normal
256 c = isSelected() ? selectedColor() : ( muted ? mutedBackgroundColor()
257 : painter.background().color() );
258
259 lingrad.setColorAt( 1, c.darker( 300 ) );
260 lingrad.setColorAt( 0, c );
261
262 // paint a black rectangle under the pattern to prevent glitches with transparent backgrounds
263 p.fillRect( rect(), QColor( 0, 0, 0 ) );
264
265 if( gradient() )
266 {
267 p.fillRect( rect(), lingrad );
268 }
269 else
270 {
271 p.fillRect( rect(), c );
272 }
273
274 const float ppt = fixedTCOs() ?
275 ( parentWidget()->width() - 2 * TCO_BORDER_WIDTH )
276 / (float) m_pat->timeMapLength().getTact() :
277 pixelsPerTact();
278
279 const int x_base = TCO_BORDER_WIDTH;
280
281 const float min = m_pat->firstObject()->minValue<float>();
282 const float max = m_pat->firstObject()->maxValue<float>();
283
284 const float y_scale = max - min;
285 const float h = ( height() - 2 * TCO_BORDER_WIDTH ) / y_scale;
286 const float ppTick = ppt / MidiTime::ticksPerTact();
287
288 p.translate( 0.0f, max * height() / y_scale - TCO_BORDER_WIDTH );
289 p.scale( 1.0f, -h );
290
291 QLinearGradient lin2grad( 0, min, 0, max );
292 QColor col;
293
294 col = !muted ? painter.pen().brush().color() : mutedColor();
295
296 lin2grad.setColorAt( 1, col.lighter( 150 ) );
297 lin2grad.setColorAt( 0.5, col );
298 lin2grad.setColorAt( 0, col.darker( 150 ) );
299
300 p.setRenderHints( QPainter::Antialiasing, true );
301 for( AutomationPattern::timeMap::const_iterator it =
302 m_pat->getTimeMap().begin();
303 it != m_pat->getTimeMap().end(); ++it )
304 {
305 if( it+1 == m_pat->getTimeMap().end() )
306 {
307 const float x1 = x_base + it.key() * ppTick;
308 const float x2 = (float)( width() - TCO_BORDER_WIDTH );
309 if( x1 > ( width() - TCO_BORDER_WIDTH ) ) break;
310 if( gradient() )
311 {
312 p.fillRect( QRectF( x1, 0.0f, x2 - x1, it.value() ), lin2grad );
313 }
314 else
315 {
316 p.fillRect( QRectF( x1, 0.0f, x2 - x1, it.value() ), col );
317 }
318 break;
319 }
320
321 float *values = m_pat->valuesAfter( it.key() );
322
323 float nextValue;
324 if( m_pat->progressionType() == AutomationPattern::DiscreteProgression )
325 {
326 nextValue = it.value();
327 }
328 else
329 {
330 nextValue = ( it + 1 ).value();
331 }
332
333 QPainterPath path;
334 QPointF origin = QPointF( x_base + it.key() * ppTick, 0.0f );
335 path.moveTo( origin );
336 path.moveTo( QPointF( x_base + it.key() * ppTick,values[0] ) );
337 float x;
338 for( int i = it.key() + 1; i < ( it + 1 ).key(); i++ )
339 {
340 x = x_base + i * ppTick;
341 if( x > ( width() - TCO_BORDER_WIDTH ) ) break;
342 float value = values[ i - it.key() ];
343 path.lineTo( QPointF( x, value ) );
344
345 }
346 path.lineTo( x_base + ( ( it + 1 ).key() ) * ppTick, nextValue );
347 path.lineTo( x_base + ( ( it + 1 ).key() ) * ppTick, 0.0f );
348 path.lineTo( origin );
349
350 if( gradient() )
351 {
352 p.fillPath( path, lin2grad );
353 }
354 else
355 {
356 p.fillPath( path, col );
357 }
358 delete [] values;
359 }
360
361 p.setRenderHints( QPainter::Antialiasing, false );
362 p.resetMatrix();
363
364 // bar lines
365 const int lineSize = 3;
366 p.setPen( c.darker( 300 ) );
367
368 for( tact_t t = 1; t < width() - TCO_BORDER_WIDTH; ++t )
369 {
370 const int tx = x_base + static_cast<int>( ppt * t ) - 2;
371 p.drawLine( tx, TCO_BORDER_WIDTH, tx, TCO_BORDER_WIDTH + lineSize );
372 p.drawLine( tx, rect().bottom() - ( lineSize + TCO_BORDER_WIDTH ),
373 tx, rect().bottom() - TCO_BORDER_WIDTH );
374 }
375
376 // recording icon for when recording automation
377 if( m_pat->isRecording() )
378 {
379 p.drawPixmap( 1, rect().bottom() - s_pat_rec->height(),
380 *s_pat_rec );
381 }
382
383 // pattern name
384 p.setRenderHint( QPainter::TextAntialiasing );
385
386 if( m_staticTextName.text() != m_pat->name() )
387 {
388 m_staticTextName.setText( m_pat->name() );
389 }
390
391 QFont font;
392 font.setHintingPreference( QFont::PreferFullHinting );
393 font.setPointSize( 8 );
394 p.setFont( font );
395
396 const int textTop = TCO_BORDER_WIDTH + 1;
397 const int textLeft = TCO_BORDER_WIDTH + 1;
398
399 p.setPen( textShadowColor() );
400 p.drawStaticText( textLeft + 1, textTop + 1, m_staticTextName );
401 p.setPen( textColor() );
402 p.drawStaticText( textLeft, textTop, m_staticTextName );
403
404 // inner border
405 p.setPen( c.lighter( current ? 160 : 130 ) );
406 p.drawRect( 1, 1, rect().right() - TCO_BORDER_WIDTH,
407 rect().bottom() - TCO_BORDER_WIDTH );
408
409 // outer border
410 p.setPen( current? c.lighter( 130 ) : c.darker( 300 ) );
411 p.drawRect( 0, 0, rect().right(), rect().bottom() );
412
413 // draw the 'muted' pixmap only if the pattern was manualy muted
414 if( m_pat->isMuted() )
415 {
416 const int spacing = TCO_BORDER_WIDTH;
417 const int size = 14;
418 p.drawPixmap( spacing, height() - ( size + spacing ),
419 embed::getIconPixmap( "muted", size, size ) );
420 }
421
422 p.end();
423
424 painter.drawPixmap( 0, 0, m_paintPixmap );
425 }
426
427
428
429
dragEnterEvent(QDragEnterEvent * _dee)430 void AutomationPatternView::dragEnterEvent( QDragEnterEvent * _dee )
431 {
432 StringPairDrag::processDragEnterEvent( _dee, "automatable_model" );
433 if( !_dee->isAccepted() )
434 {
435 TrackContentObjectView::dragEnterEvent( _dee );
436 }
437 }
438
439
440
441
dropEvent(QDropEvent * _de)442 void AutomationPatternView::dropEvent( QDropEvent * _de )
443 {
444 QString type = StringPairDrag::decodeKey( _de );
445 QString val = StringPairDrag::decodeValue( _de );
446 if( type == "automatable_model" )
447 {
448 AutomatableModel * mod = dynamic_cast<AutomatableModel *>(
449 Engine::projectJournal()->
450 journallingObject( val.toInt() ) );
451 if( mod != NULL )
452 {
453 bool added = m_pat->addObject( mod );
454 if ( !added )
455 {
456 TextFloat::displayMessage( mod->displayName(),
457 tr( "Model is already connected "
458 "to this pattern." ),
459 embed::getIconPixmap( "automation" ),
460 2000 );
461 }
462 }
463 update();
464
465 if( gui->automationEditor() &&
466 gui->automationEditor()->currentPattern() == m_pat )
467 {
468 gui->automationEditor()->setCurrentPattern( m_pat );
469 }
470 }
471 else
472 {
473 TrackContentObjectView::dropEvent( _de );
474 }
475 }
476
477
478
479
480 /**
481 * @brief Preserves the auto points over different scale
482 */
scaleTimemapToFit(float oldMin,float oldMax)483 void AutomationPatternView::scaleTimemapToFit( float oldMin, float oldMax )
484 {
485 float newMin = m_pat->getMin();
486 float newMax = m_pat->getMax();
487
488 if( oldMin == newMin && oldMax == newMax )
489 {
490 return;
491 }
492
493 for( AutomationPattern::timeMap::iterator it = m_pat->m_timeMap.begin();
494 it != m_pat->m_timeMap.end(); ++it )
495 {
496 if( *it < oldMin )
497 {
498 *it = oldMin;
499 }
500 else if( *it > oldMax )
501 {
502 *it = oldMax;
503 }
504 *it = (*it-oldMin)*(newMax-newMin)/(oldMax-oldMin)+newMin;
505 }
506
507 m_pat->generateTangents();
508 }
509