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