1 /*
2  * TimeLineWidget.cpp - class timeLine, representing a time-line with position marker
3  *
4  * Copyright (c) 2004-2014 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 
25 
26 #include <QDomElement>
27 #include <QTimer>
28 #include <QApplication>
29 #include <QLayout>
30 #include <QMouseEvent>
31 #include <QPainter>
32 #include <QToolBar>
33 
34 
35 #include "TimeLineWidget.h"
36 #include "embed.h"
37 #include "NStateButton.h"
38 #include "GuiApplication.h"
39 #include "TextFloat.h"
40 #include "SongEditor.h"
41 
42 
43 #if QT_VERSION < 0x040800
44 #define MiddleButton MidButton
45 #endif
46 
47 
48 QPixmap * TimeLineWidget::s_posMarkerPixmap = NULL;
49 
TimeLineWidget(const int xoff,const int yoff,const float ppt,Song::PlayPos & pos,const MidiTime & begin,QWidget * parent)50 TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppt,
51 			Song::PlayPos & pos, const MidiTime & begin,
52 							QWidget * parent ) :
53 	QWidget( parent ),
54 	m_inactiveLoopColor( 52, 63, 53, 64 ),
55 	m_inactiveLoopBrush( QColor( 255, 255, 255, 32 ) ),
56 	m_inactiveLoopInnerColor( 255, 255, 255, 32 ),
57 	m_activeLoopColor( 52, 63, 53, 255 ),
58 	m_activeLoopBrush( QColor( 55, 141, 89 ) ),
59 	m_activeLoopInnerColor( 74, 155, 100, 255 ),
60 	m_loopRectangleVerticalPadding( 1 ),
61 	m_barLineColor( 192, 192, 192 ),
62 	m_barNumberColor( m_barLineColor.darker( 120 ) ),
63 	m_autoScroll( AutoScrollEnabled ),
64 	m_loopPoints( LoopPointsDisabled ),
65 	m_behaviourAtStop( BackToZero ),
66 	m_changedPosition( true ),
67 	m_xOffset( xoff ),
68 	m_posMarkerX( 0 ),
69 	m_ppt( ppt ),
70 	m_pos( pos ),
71 	m_begin( begin ),
72 	m_savedPos( -1 ),
73 	m_hint( NULL ),
74 	m_action( NoAction ),
75 	m_moveXOff( 0 )
76 {
77 	m_loopPos[0] = 0;
78 	m_loopPos[1] = DefaultTicksPerTact;
79 
80 	if( s_posMarkerPixmap == NULL )
81 	{
82 		s_posMarkerPixmap = new QPixmap( embed::getIconPixmap(
83 							"playpos_marker" ) );
84 	}
85 
86 	setAttribute( Qt::WA_OpaquePaintEvent, true );
87 	move( 0, yoff );
88 
89 	m_xOffset -= s_posMarkerPixmap->width() / 2;
90 
91 	setMouseTracking(true);
92 	m_pos.m_timeLine = this;
93 
94 	QTimer * updateTimer = new QTimer( this );
95 	connect( updateTimer, SIGNAL( timeout() ),
96 					this, SLOT( updatePosition() ) );
97 	updateTimer->start( 1000 / 60 );  // 60 fps
98 	connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ),
99 					this, SLOT( update() ) );
100 }
101 
102 
103 
104 
~TimeLineWidget()105 TimeLineWidget::~TimeLineWidget()
106 {
107 	if( gui->songEditor() )
108 	{
109 		m_pos.m_timeLine = NULL;
110 	}
111 	delete m_hint;
112 }
113 
114 
115 
116 
addToolButtons(QToolBar * _tool_bar)117 void TimeLineWidget::addToolButtons( QToolBar * _tool_bar )
118 {
119 	NStateButton * autoScroll = new NStateButton( _tool_bar );
120 	autoScroll->setGeneralToolTip( tr( "Enable/disable auto-scrolling" ) );
121 	autoScroll->addState( embed::getIconPixmap( "autoscroll_on" ) );
122 	autoScroll->addState( embed::getIconPixmap( "autoscroll_off" ) );
123 	connect( autoScroll, SIGNAL( changedState( int ) ), this,
124 					SLOT( toggleAutoScroll( int ) ) );
125 
126 	NStateButton * loopPoints = new NStateButton( _tool_bar );
127 	loopPoints->setGeneralToolTip( tr( "Enable/disable loop-points" ) );
128 	loopPoints->addState( embed::getIconPixmap( "loop_points_off" ) );
129 	loopPoints->addState( embed::getIconPixmap( "loop_points_on" ) );
130 	connect( loopPoints, SIGNAL( changedState( int ) ), this,
131 					SLOT( toggleLoopPoints( int ) ) );
132 	connect( this, SIGNAL( loopPointStateLoaded( int ) ), loopPoints,
133 					SLOT( changeState( int ) ) );
134 
135 	NStateButton * behaviourAtStop = new NStateButton( _tool_bar );
136 	behaviourAtStop->addState( embed::getIconPixmap( "back_to_zero" ),
137 					tr( "After stopping go back to begin" )
138 									);
139 	behaviourAtStop->addState( embed::getIconPixmap( "back_to_start" ),
140 					tr( "After stopping go back to "
141 						"position at which playing was "
142 						"started" ) );
143 	behaviourAtStop->addState( embed::getIconPixmap( "keep_stop_position" ),
144 					tr( "After stopping keep position" ) );
145 	connect( behaviourAtStop, SIGNAL( changedState( int ) ), this,
146 					SLOT( toggleBehaviourAtStop( int ) ) );
147 
148 	_tool_bar->addWidget( autoScroll );
149 	_tool_bar->addWidget( loopPoints );
150 	_tool_bar->addWidget( behaviourAtStop );
151 }
152 
153 
154 
155 
saveSettings(QDomDocument & _doc,QDomElement & _this)156 void TimeLineWidget::saveSettings( QDomDocument & _doc, QDomElement & _this )
157 {
158 	_this.setAttribute( "lp0pos", (int) loopBegin() );
159 	_this.setAttribute( "lp1pos", (int) loopEnd() );
160 	_this.setAttribute( "lpstate", m_loopPoints );
161 }
162 
163 
164 
165 
loadSettings(const QDomElement & _this)166 void TimeLineWidget::loadSettings( const QDomElement & _this )
167 {
168 	m_loopPos[0] = _this.attribute( "lp0pos" ).toInt();
169 	m_loopPos[1] = _this.attribute( "lp1pos" ).toInt();
170 	m_loopPoints = static_cast<LoopPointStates>(
171 					_this.attribute( "lpstate" ).toInt() );
172 	update();
173 	emit loopPointStateLoaded( m_loopPoints );
174 }
175 
176 
177 
178 
updatePosition(const MidiTime &)179 void TimeLineWidget::updatePosition( const MidiTime & )
180 {
181 	const int new_x = markerX( m_pos );
182 
183 	if( new_x != m_posMarkerX )
184 	{
185 		m_posMarkerX = new_x;
186 		m_changedPosition = true;
187 		emit positionChanged( m_pos );
188 		update();
189 	}
190 }
191 
192 
193 
194 
toggleAutoScroll(int _n)195 void TimeLineWidget::toggleAutoScroll( int _n )
196 {
197 	m_autoScroll = static_cast<AutoScrollStates>( _n );
198 }
199 
200 
201 
202 
toggleLoopPoints(int _n)203 void TimeLineWidget::toggleLoopPoints( int _n )
204 {
205 	m_loopPoints = static_cast<LoopPointStates>( _n );
206 	update();
207 }
208 
209 
210 
211 
toggleBehaviourAtStop(int _n)212 void TimeLineWidget::toggleBehaviourAtStop( int _n )
213 {
214 	m_behaviourAtStop = static_cast<BehaviourAtStopStates>( _n );
215 }
216 
217 
218 
219 
paintEvent(QPaintEvent *)220 void TimeLineWidget::paintEvent( QPaintEvent * )
221 {
222 	QPainter p( this );
223 
224 	// Draw background
225 	p.fillRect( 0, 0, width(), height(), p.background() );
226 
227 	// Clip so that we only draw everything starting from the offset
228 	p.setClipRect( m_xOffset, 0, width() - m_xOffset, height() );
229 
230 	// Draw the loop rectangle
231 	int const & loopRectMargin = getLoopRectangleVerticalPadding();
232 	int const loopRectHeight = this->height() - 2 * loopRectMargin;
233 	int const loopStart = markerX( loopBegin() ) + 8;
234 	int const loopEndR = markerX( loopEnd() ) + 9;
235 	int const loopRectWidth = loopEndR - loopStart;
236 
237 	bool const loopPointsActive = loopPointsEnabled();
238 
239 	// Draw the main rectangle (inner fill only)
240 	QRect outerRectangle( loopStart, loopRectMargin, loopRectWidth - 1, loopRectHeight - 1 );
241 	p.fillRect( outerRectangle, loopPointsActive ? getActiveLoopBrush() : getInactiveLoopBrush());
242 
243 	// Draw the bar lines and numbers
244 	// Activate hinting on the font
245 	QFont font = p.font();
246 	font.setHintingPreference( QFont::PreferFullHinting );
247 	p.setFont(font);
248 	int const fontAscent = p.fontMetrics().ascent();
249 	int const fontHeight = p.fontMetrics().height();
250 
251 	QColor const & barLineColor = getBarLineColor();
252 	QColor const & barNumberColor = getBarNumberColor();
253 
254 	tact_t barNumber = m_begin.getTact();
255 	int const x = m_xOffset + s_posMarkerPixmap->width() / 2 -
256 			( ( static_cast<int>( m_begin * m_ppt ) / MidiTime::ticksPerTact() ) % static_cast<int>( m_ppt ) );
257 
258 	for( int i = 0; x + i * m_ppt < width(); ++i )
259 	{
260 		++barNumber;
261 		if( ( barNumber - 1 ) %
262 			qMax( 1, qRound( 1.0f / 3.0f *
263 				MidiTime::ticksPerTact() / m_ppt ) ) == 0 )
264 		{
265 			const int cx = x + qRound( i * m_ppt );
266 			p.setPen( barLineColor );
267 			p.drawLine( cx, 5, cx, height() - 6 );
268 
269 			const QString s = QString::number( barNumber );
270 			p.setPen( barNumberColor );
271 			p.drawText( cx + 5, ((height() - fontHeight) / 2) + fontAscent, s );
272 		}
273 	}
274 
275 	// Draw the main rectangle (outer border)
276 	p.setPen( loopPointsActive ? getActiveLoopColor() : getInactiveLoopColor() );
277 	p.setBrush( Qt::NoBrush );
278 	p.drawRect( outerRectangle );
279 
280 	// Draw the inner border outline (no fill)
281 	QRect innerRectangle = outerRectangle.adjusted( 1, 1, -1, -1 );
282 	p.setPen( loopPointsActive ? getActiveLoopInnerColor() : getInactiveLoopInnerColor() );
283 	p.setBrush( Qt::NoBrush );
284 	p.drawRect( innerRectangle );
285 
286 	// Draw the position marker
287 	p.setOpacity( 0.6 );
288 	p.drawPixmap( m_posMarkerX, height() - s_posMarkerPixmap->height(), *s_posMarkerPixmap );
289 }
290 
291 
292 
293 
mousePressEvent(QMouseEvent * event)294 void TimeLineWidget::mousePressEvent( QMouseEvent* event )
295 {
296 	if( event->x() < m_xOffset )
297 	{
298 		return;
299 	}
300 	if( event->button() == Qt::LeftButton  && !(event->modifiers() & Qt::ShiftModifier) )
301 	{
302 		m_action = MovePositionMarker;
303 		if( event->x() - m_xOffset < s_posMarkerPixmap->width() )
304 		{
305 			m_moveXOff = event->x() - m_xOffset;
306 		}
307 		else
308 		{
309 			m_moveXOff = s_posMarkerPixmap->width() / 2;
310 		}
311 	}
312 	else if( event->button() == Qt::LeftButton  && (event->modifiers() & Qt::ShiftModifier) )
313 	{
314 		m_action = SelectSongTCO;
315 		m_initalXSelect = event->x();
316 	}
317 	else if( event->button() == Qt::RightButton || event->button() == Qt::MiddleButton )
318 	{
319         	m_moveXOff = s_posMarkerPixmap->width() / 2;
320 		const MidiTime t = m_begin + static_cast<int>( event->x() * MidiTime::ticksPerTact() / m_ppt );
321 		if( m_loopPos[0] > m_loopPos[1]  )
322 		{
323 			qSwap( m_loopPos[0], m_loopPos[1] );
324 		}
325 		if( ( event->modifiers() & Qt::ShiftModifier ) || event->button() == Qt::MiddleButton )
326 		{
327 			m_action = MoveLoopBegin;
328 		}
329 		else
330 		{
331 			m_action = MoveLoopEnd;
332 		}
333 		m_loopPos[( m_action == MoveLoopBegin ) ? 0 : 1] = t;
334 	}
335 
336 	if( m_action == MoveLoopBegin )
337 	{
338 		delete m_hint;
339 		m_hint = TextFloat::displayMessage( tr( "Hint" ),
340 					tr( "Press <%1> to disable magnetic loop points." ).arg(
341 						#ifdef LMMS_BUILD_APPLE
342 						"⌘"),
343 						#else
344 						"Ctrl"),
345 						#endif
346 					embed::getIconPixmap( "hint" ), 0 );
347 	}
348 	else if( m_action == MoveLoopEnd )
349 	{
350 		delete m_hint;
351 		m_hint = TextFloat::displayMessage( tr( "Hint" ),
352 					tr( "Hold <Shift> to move the begin loop point; Press <%1> to disable magnetic loop points." ).arg(
353 						#ifdef LMMS_BUILD_APPLE
354 						"⌘"),
355 						#else
356 						"Ctrl"),
357 						#endif
358 					embed::getIconPixmap( "hint" ), 0 );
359 	}
360 
361 	mouseMoveEvent( event );
362 }
363 
364 
365 
366 
mouseMoveEvent(QMouseEvent * event)367 void TimeLineWidget::mouseMoveEvent( QMouseEvent* event )
368 {
369 	parentWidget()->update(); // essential for widgets that this timeline had taken their mouse move event from.
370 	const MidiTime t = m_begin + static_cast<int>( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * MidiTime::ticksPerTact() / m_ppt );
371 
372 	switch( m_action )
373 	{
374 		case MovePositionMarker:
375 			m_pos.setTicks( t.getTicks() );
376 			Engine::getSong()->setMilliSeconds( ( t.getTicks() *
377 					( 60 * 1000 / 48 ) ) /
378 						Engine::getSong()->getTempo() );
379 			m_pos.setCurrentFrame( 0 );
380 			m_pos.setJumped( true );
381 			updatePosition();
382 			positionMarkerMoved();
383 			break;
384 
385 		case MoveLoopBegin:
386 		case MoveLoopEnd:
387 		{
388 			const int i = m_action - MoveLoopBegin;
389 			if( event->modifiers() & Qt::ControlModifier )
390 			{
391 				// no ctrl-press-hint when having ctrl pressed
392 				delete m_hint;
393 				m_hint = NULL;
394 				m_loopPos[i] = t;
395 			}
396 			else
397 			{
398 				m_loopPos[i] = t.toNearestTact();
399 			}
400 			// Catch begin == end
401 			if( m_loopPos[0] == m_loopPos[1] )
402 			{
403 				// Note, swap 1 and 0 below and the behavior "skips" the other
404 				// marking instead of pushing it.
405 				if( m_action == MoveLoopBegin )
406 					m_loopPos[0] -= MidiTime::ticksPerTact();
407 				else
408 					m_loopPos[1] += MidiTime::ticksPerTact();
409 			}
410 			update();
411 			break;
412 		}
413 	case SelectSongTCO:
414 			emit regionSelectedFromPixels( m_initalXSelect , event->x() );
415 		break;
416 
417 		default:
418 			break;
419 	}
420 }
421 
422 
423 
424 
mouseReleaseEvent(QMouseEvent * event)425 void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event )
426 {
427 	delete m_hint;
428 	m_hint = NULL;
429 	if ( m_action == SelectSongTCO ) { emit selectionFinished(); }
430 	m_action = NoAction;
431 }
432