]> git.sesse.net Git - vlc/blob - modules/gui/qt4/util/input_slider.cpp
Qt: SoundSlider: make 100% value green
[vlc] / modules / gui / qt4 / util / input_slider.cpp
1 /*****************************************************************************
2  * input_slider.cpp : VolumeSlider and SeekSlider
3  ****************************************************************************
4  * Copyright (C) 2006-2011 the VideoLAN team
5  * $Id$
6  *
7  * Authors: Clément Stenac <zorglub@videolan.org>
8  *          Jean-Baptiste Kempf <jb@videolan.org>
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23  *****************************************************************************/
24
25 #ifdef HAVE_CONFIG_H
26 # include "config.h"
27 #endif
28
29 #include "qt4.hpp"
30
31 #include "util/input_slider.hpp"
32 #include "adapters/seekpoints.hpp"
33
34 #include <stdlib.h>
35
36 #include <QPaintEvent>
37 #include <QPainter>
38 #include <QBitmap>
39 #include <QPainter>
40 #include <QStyleOptionSlider>
41 #include <QLinearGradient>
42 #include <QTimer>
43 #include <QRadialGradient>
44
45 #define MINIMUM 0
46 #define MAXIMUM 1000
47 #define CHAPTERSSPOTSIZE 3
48
49 SeekSlider::SeekSlider( QWidget *_parent ) : QSlider( _parent )
50 {
51     SeekSlider( Qt::Horizontal, _parent );
52 }
53
54 SeekSlider::SeekSlider( Qt::Orientation q, QWidget *_parent )
55           : QSlider( q, _parent )
56 {
57     b_isSliding = false;
58     f_buffering = 1.0;
59     chapters = NULL;
60
61     /* Timer used to fire intermediate updatePos() when sliding */
62     seekLimitTimer = new QTimer( this );
63     seekLimitTimer->setSingleShot( true );
64
65     /* Tooltip bubble */
66     mTimeTooltip = new TimeTooltip( this );
67     mTimeTooltip->setMouseTracking( true );
68
69     /* Properties */
70     setRange( MINIMUM, MAXIMUM );
71     setSingleStep( 2 );
72     setPageStep( 10 );
73     setMouseTracking( true );
74     setTracking( true );
75     setFocusPolicy( Qt::NoFocus );
76
77     /* Init to 0 */
78     setPosition( -1.0, 0, 0 );
79     secstotimestr( psz_length, 0 );
80
81     CONNECT( this, sliderMoved( int ), this, startSeekTimer() );
82     CONNECT( seekLimitTimer, timeout(), this, updatePos() );
83     mTimeTooltip->installEventFilter( this );
84 }
85
86 SeekSlider::~SeekSlider()
87 {
88     delete chapters;
89 }
90
91 /***
92  * \brief Sets the chapters seekpoints adapter
93  *
94  * \params SeekPoints initilized with current intf thread
95 ***/
96 void SeekSlider::setChapters( SeekPoints *chapters_ )
97 {
98     delete chapters;
99     chapters = chapters_;
100     chapters->setParent( this );
101 }
102
103 /***
104  * \brief Main public method, superseeding setValue. Disabling the slider when neeeded
105  *
106  * \param pos Position, between 0 and 1. -1 disables the slider
107  * \param time Elapsed time. Unused
108  * \param legnth Duration time.
109  ***/
110 void SeekSlider::setPosition( float pos, int64_t time, int length )
111 {
112     VLC_UNUSED(time);
113     if( pos == -1.0 )
114     {
115         setEnabled( false );
116         b_isSliding = false;
117     }
118     else
119         setEnabled( true );
120
121     if( !b_isSliding )
122         setValue( (int)( pos * 1000.0 ) );
123
124     inputLength = length;
125 }
126
127 void SeekSlider::startSeekTimer()
128 {
129     /* Only fire one update, when sliding, every 150ms */
130     if( b_isSliding && !seekLimitTimer->isActive() )
131         seekLimitTimer->start( 150 );
132 }
133
134 void SeekSlider::updatePos()
135 {
136     float f_pos = (float)( value() ) / 1000.0;
137     emit sliderDragged( f_pos ); /* Send new position to VLC's core */
138 }
139
140 void SeekSlider::updateBuffering( float f_buffering_ )
141 {
142     f_buffering = f_buffering_;
143     repaint();
144 }
145
146 void SeekSlider::mouseReleaseEvent( QMouseEvent *event )
147 {
148     event->accept();
149     b_isSliding = false;
150     seekLimitTimer->stop(); /* We're not sliding anymore: only last seek on release */
151     if ( b_is_jumping )
152     {
153         b_is_jumping = false;
154         return;
155     }
156     QSlider::mouseReleaseEvent( event );
157     updatePos();
158 }
159
160 void SeekSlider::mousePressEvent( QMouseEvent* event )
161 {
162     /* Right-click */
163     if( event->button() != Qt::LeftButton &&
164         event->button() != Qt::MidButton )
165     {
166         QSlider::mousePressEvent( event );
167         return;
168     }
169
170     b_is_jumping = false;
171     /* handle chapter clicks */
172     int i_width = size().width();
173     if ( chapters && inputLength && i_width)
174     {
175         if ( orientation() == Qt::Horizontal ) /* TODO: vertical */
176         {
177              /* only on chapters zone */
178             if ( event->y() < CHAPTERSSPOTSIZE ||
179                  event->y() > ( size().height() - CHAPTERSSPOTSIZE ) )
180             {
181                 QList<SeekPoint> points = chapters->getPoints();
182                 int i_selected = -1;
183                 int i_min_diff = i_width + 1;
184                 for( int i = 0 ; i < points.count() ; i++ )
185                 {
186                     int x = points.at(i).time / 1000000.0 / inputLength * i_width;
187                     int diff_x = abs( x - event->x() );
188                     if ( diff_x < i_min_diff )
189                     {
190                         i_min_diff = diff_x;
191                         i_selected = i;
192                     } else break;
193                 }
194                 if ( i_selected && i_min_diff < 4 ) // max 4px around mark
195                 {
196                     chapters->jumpTo( i_selected );
197                     event->accept();
198                     b_is_jumping = true;
199                     return;
200                 }
201             }
202         }
203     }
204
205     b_isSliding = true ;
206     setValue( QStyle::sliderValueFromPosition( MINIMUM, MAXIMUM, event->x(), width(), false ) );
207     event->accept();
208 }
209
210 void SeekSlider::mouseMoveEvent( QMouseEvent *event )
211 {
212     if( b_isSliding )
213     {
214         setValue( QStyle::sliderValueFromPosition( MINIMUM, MAXIMUM, event->x(), width(), false) );
215         emit sliderMoved( value() );
216     }
217
218     /* Tooltip */
219     if ( inputLength > 0 )
220     {
221         int posX = qMax( rect().left(), qMin( rect().right(), event->x() ) );
222
223         QPoint p( event->globalX() - ( event->x() - posX ) - ( mTimeTooltip->width() / 2 ),
224                   QWidget::mapToGlobal( pos() ).y() - ( mTimeTooltip->height() + 2 ) );
225
226
227         secstotimestr( psz_length, ( posX * inputLength ) / size().width() );
228         mTimeTooltip->setTime( psz_length );
229         mTimeTooltip->move( p );
230     }
231     event->accept();
232 }
233
234 void SeekSlider::wheelEvent( QWheelEvent *event )
235 {
236     /* Don't do anything if we are for somehow reason sliding */
237     if( !b_isSliding )
238     {
239         setValue( value() + event->delta() / 12 ); /* 12 = 8 * 15 / 10
240          Since delta is in 1/8 of ° and mouse have steps of 15 °
241          and that our slider is in 0.1% and we want one step to be a 1%
242          increment of position */
243         emit sliderDragged( value() / 1000.0 );
244     }
245     event->accept();
246 }
247
248 void SeekSlider::enterEvent( QEvent * )
249 {
250     /* Don't show the tooltip if the slider is disabled */
251     if( isEnabled() && inputLength > 0 )
252         mTimeTooltip->show();
253 }
254
255 void SeekSlider::leaveEvent( QEvent * )
256 {
257     if( !rect().contains( mapFromGlobal( QCursor::pos() ) ) )
258         mTimeTooltip->hide();
259 }
260
261 void SeekSlider::hideEvent( QHideEvent * )
262 {
263     mTimeTooltip->hide();
264 }
265
266 bool SeekSlider::eventFilter( QObject *obj, QEvent *event )
267 {
268     if( obj == mTimeTooltip )
269     {
270         if( event->type() == QEvent::Leave ||
271             event->type() == QEvent::MouseMove )
272         {
273             QMouseEvent *e = static_cast<QMouseEvent*>( event );
274             if( !rect().contains( mapFromGlobal( e->globalPos() ) ) )
275                 mTimeTooltip->hide();
276         }
277         return false;
278     }
279     else
280         return QSlider::eventFilter( obj, event );
281 }
282
283 QSize SeekSlider::sizeHint() const
284 {
285     return ( orientation() == Qt::Horizontal ) ? QSize( 100, 18 )
286                                                : QSize( 18, 100 );
287 }
288
289 QSize SeekSlider::handleSize() const
290 {
291     const int size = ( orientation() == Qt::Horizontal ? height() : width() );
292     return QSize( size, size );
293 }
294
295 void SeekSlider::paintEvent( QPaintEvent *event )
296 {
297     Q_UNUSED( event );
298
299     QStyleOptionSlider option;
300     initStyleOption( &option );
301
302     /* */
303     QPainter painter( this );
304     painter.setRenderHints( QPainter::Antialiasing );
305
306     // draw bar
307     const int barCorner = 3;
308     qreal sliderPos     = -1;
309     int range           = MAXIMUM;
310     QRect barRect       = rect();
311
312     // adjust positions based on the current orientation
313     if ( option.sliderPosition != 0 )
314     {
315         switch ( orientation() )
316         {
317             case Qt::Horizontal:
318                 sliderPos = ( ( (qreal)width() ) / (qreal)range )
319                         * (qreal)option.sliderPosition;
320                 break;
321             case Qt::Vertical:
322                 sliderPos = ( ( (qreal)height() ) / (qreal)range )
323                         * (qreal)option.sliderPosition;
324                 break;
325         }
326     }
327
328     switch ( orientation() )
329     {
330         case Qt::Horizontal:
331             barRect.setHeight( handleSize().height() /2 );
332             break;
333         case Qt::Vertical:
334             barRect.setWidth( handleSize().width() /2 );
335             break;
336     }
337
338     barRect.moveCenter( rect().center() );
339
340     // set the background color and gradient
341     QColor backgroundBase( 135, 135, 135 );
342     QLinearGradient backgroundGradient( 0, 0, 0, height() );
343     backgroundGradient.setColorAt( 0.0, backgroundBase );
344     backgroundGradient.setColorAt( 1.0, backgroundBase.lighter( 150 ) );
345
346     // set the foreground color and gradient
347     QColor foregroundBase( 50, 156, 255 );
348     QLinearGradient foregroundGradient( 0, 0, 0, height() );
349     foregroundGradient.setColorAt( 0.0,  foregroundBase );
350     foregroundGradient.setColorAt( 1.0,  foregroundBase.darker( 140 ) );
351
352     // draw a slight 3d effect on the bottom
353     painter.setPen( QColor( 230, 230, 230 ) );
354     painter.setBrush( Qt::NoBrush );
355     painter.drawRoundedRect( barRect.adjusted( 0, 2, 0, 0 ), barCorner, barCorner );
356
357     // draw background
358     painter.setPen( Qt::NoPen );
359     painter.setBrush( backgroundGradient );
360     painter.drawRoundedRect( barRect, barCorner, barCorner );
361
362     // adjusted foreground rectangle
363     QRect valueRect = barRect.adjusted( 1, 1, -1, 0 );
364
365     switch ( orientation() )
366     {
367         case Qt::Horizontal:
368             valueRect.setWidth( qMin( width(), int( sliderPos ) ) );
369             break;
370         case Qt::Vertical:
371             valueRect.setHeight( qMin( height(), int( sliderPos ) ) );
372             valueRect.moveBottom( rect().bottom() );
373             break;
374     }
375
376     if ( option.sliderPosition > minimum() && option.sliderPosition <= maximum() )
377     {
378         // draw foreground
379         painter.setPen( Qt::NoPen );
380         painter.setBrush( foregroundGradient );
381         painter.drawRoundedRect( valueRect, barCorner, barCorner );
382     }
383
384     // draw buffering overlay
385     if ( f_buffering < 1.0 )
386     {
387         QRect innerRect = barRect.adjusted( 1, 1,
388                             barRect.width() * ( -1.0 + f_buffering ) - 1, 0 );
389         QColor overlayColor = QColor( "Orange" );
390         overlayColor.setAlpha( 128 );
391         painter.setBrush( overlayColor );
392         painter.drawRoundedRect( innerRect, barCorner, barCorner );
393     }
394
395     if ( option.state & QStyle::State_MouseOver )
396     {
397         /* draw chapters tickpoints */
398         if ( chapters && inputLength && size().width() )
399         {
400             if ( orientation() == Qt::Horizontal ) /* TODO: vertical */
401             {
402                 QList<SeekPoint> points = chapters->getPoints();
403                 foreach( SeekPoint point, points )
404                 {
405                     int x = point.time / 1000000.0 / inputLength * size().width();
406                     painter.setPen( QColor( 80, 80, 80 ) );
407                     painter.setBrush( Qt::NoBrush );
408                     painter.drawLine( x, 0, x, CHAPTERSSPOTSIZE );
409                     painter.drawLine( x, height(), x, height() - CHAPTERSSPOTSIZE );
410                 }
411             }
412         }
413
414         // draw handle
415         if ( sliderPos != -1 )
416         {
417             const int margin = 0;
418             QSize hs = handleSize() - QSize( 5, 5 );
419             QPoint pos;
420
421             switch ( orientation() )
422             {
423                 case Qt::Horizontal:
424                     pos = QPoint( sliderPos - ( hs.width() / 2 ), 2 );
425                     pos.rx() = qMax( margin, pos.x() );
426                     pos.rx() = qMin( width() - hs.width() - margin, pos.x() );
427                     break;
428                 case Qt::Vertical:
429                     pos = QPoint( 2, height() - ( sliderPos + ( hs.height() / 2 ) ) );
430                     pos.ry() = qMax( margin, pos.y() );
431                     pos.ry() = qMin( height() - hs.height() - margin, pos.y() );
432                     break;
433             }
434
435             QRadialGradient buttonGradient( pos.x() + ( hs.width() / 2 ) - 2,
436                                             pos.y() + ( hs.height() / 2 ) - 2,
437                                             qMax( hs.width(), hs.height() ) );
438             buttonGradient.setColorAt( 0.0, QColor(  0,  0,  0 ) );
439             buttonGradient.setColorAt( 1.0, QColor( 80, 80, 80 ) );
440
441             painter.setPen( Qt::NoPen );
442             painter.setBrush( buttonGradient );
443             painter.drawEllipse( pos.x(), pos.y(), hs.width(), hs.height() );
444         }
445     }
446 }
447
448
449 /* This work is derived from Amarok's work under GPLv2+
450     - Mark Kretschmann
451     - Gábor Lehel
452    */
453 #define WLENGTH   80 // px
454 #define WHEIGHT   22  // px
455 #define SOUNDMIN  0   // %
456 #define SOUNDMAX  200 // % OR 400 ?
457
458 SoundSlider::SoundSlider( QWidget *_parent, int _i_step, bool b_hard,
459                           char *psz_colors )
460                         : QAbstractSlider( _parent )
461 {
462     f_step = ( _i_step * 100 ) / AOUT_VOLUME_MAX ;
463     setRange( SOUNDMIN, b_hard ? (2 * SOUNDMAX) : SOUNDMAX  );
464     setMouseTracking( true );
465     b_isSliding = false;
466     b_mouseOutside = true;
467     b_isMuted = false;
468
469     pixOutside = QPixmap( ":/toolbar/volslide-outside" );
470
471     const QPixmap temp( ":/toolbar/volslide-inside" );
472     const QBitmap mask( temp.createHeuristicMask() );
473
474     setFixedSize( pixOutside.size() );
475
476     pixGradient = QPixmap( mask.size() );
477     pixGradient2 = QPixmap( mask.size() );
478
479     /* Gradient building from the preferences */
480     QLinearGradient gradient( paddingL, 2, WLENGTH + paddingL , 2 );
481     QLinearGradient gradient2( paddingL, 2, WLENGTH + paddingL , 2 );
482
483     QStringList colorList = qfu( psz_colors ).split( ";" );
484     free( psz_colors );
485
486     /* Fill with 255 if the list is too short */
487     if( colorList.size() < 12 )
488         for( int i = colorList.size(); i < 12; i++)
489             colorList.append( "255" );
490
491     /* Regular colors */
492 #define c(i) colorList.at(i).toInt()
493 #define add_color(gradient, range, c1, c2, c3) \
494     gradient.setColorAt( range, QColor( c(c1), c(c2), c(c3) ) );
495
496     /* Desaturated colors */
497 #define desaturate(c) c->setHsvF( c->hueF(), 0.2 , 0.5, 1.0 )
498 #define add_desaturated_color(gradient, range, c1, c2, c3) \
499     foo = new QColor( c(c1), c(c2), c(c3) );\
500     desaturate( foo ); gradient.setColorAt( range, *foo );\
501     delete foo;
502
503     /* combine the two helpers */
504 #define add_colors( gradient1, gradient2, range, c1, c2, c3 )\
505     add_color( gradient1, range, c1, c2, c3 ); \
506     add_desaturated_color( gradient2, range, c1, c2, c3 );
507
508     QColor * foo;
509     add_colors( gradient, gradient2, 0.0, 0, 1, 2 );
510     add_colors( gradient, gradient2, 0.45, 3, 4, 5 );
511     add_colors( gradient, gradient2, 0.55, 6, 7, 8 );
512     add_colors( gradient, gradient2, 1.0, 9, 10, 11 );
513
514     QPainter painter( &pixGradient );
515     painter.setPen( Qt::NoPen );
516     painter.setBrush( gradient );
517     painter.drawRect( pixGradient.rect() );
518     painter.end();
519
520     painter.begin( &pixGradient2 );
521     painter.setPen( Qt::NoPen );
522     painter.setBrush( gradient2 );
523     painter.drawRect( pixGradient2.rect() );
524     painter.end();
525
526     pixGradient.setMask( mask );
527     pixGradient2.setMask( mask );
528 }
529
530 void SoundSlider::wheelEvent( QWheelEvent *event )
531 {
532     int newvalue = value() + event->delta() / ( 8 * 15 ) * f_step;
533     setValue( __MIN( __MAX( minimum(), newvalue ), maximum() ) );
534
535     emit sliderReleased();
536     emit sliderMoved( value() );
537 }
538
539 void SoundSlider::mousePressEvent( QMouseEvent *event )
540 {
541     if( event->button() != Qt::RightButton )
542     {
543         /* We enter the sliding mode */
544         b_isSliding = true;
545         i_oldvalue = value();
546         emit sliderPressed();
547         changeValue( event->x() - paddingL );
548         emit sliderMoved( value() );
549     }
550 }
551
552 void SoundSlider::mouseReleaseEvent( QMouseEvent *event )
553 {
554     if( event->button() != Qt::RightButton )
555     {
556         if( !b_mouseOutside && value() != i_oldvalue )
557         {
558             emit sliderReleased();
559             setValue( value() );
560             emit sliderMoved( value() );
561         }
562         b_isSliding = false;
563         b_mouseOutside = false;
564     }
565 }
566
567 void SoundSlider::mouseMoveEvent( QMouseEvent *event )
568 {
569     if( b_isSliding )
570     {
571         QRect rect( paddingL - 15,    -1,
572                     WLENGTH + 15 * 2 , WHEIGHT + 5 );
573         if( !rect.contains( event->pos() ) )
574         { /* We are outside */
575             if ( !b_mouseOutside )
576                 setValue( i_oldvalue );
577             b_mouseOutside = true;
578         }
579         else
580         { /* We are inside */
581             b_mouseOutside = false;
582             changeValue( event->x() - paddingL );
583             emit sliderMoved( value() );
584         }
585     }
586     else
587     {
588         int i = ( ( event->x() - paddingL ) * maximum() + 40 ) / WLENGTH;
589         i = __MIN( __MAX( 0, i ), maximum() );
590         setToolTip( QString("%1  \%" ).arg( i ) );
591     }
592 }
593
594 void SoundSlider::changeValue( int x )
595 {
596     setValue( (x * maximum() + 40 ) / WLENGTH );
597 }
598
599 void SoundSlider::setMuted( bool m )
600 {
601     b_isMuted = m;
602     update();
603 }
604
605 void SoundSlider::paintEvent( QPaintEvent *e )
606 {
607     QPainter painter( this );
608     QPixmap *pixGradient;
609     if (b_isMuted)
610         pixGradient = &this->pixGradient2;
611     else
612         pixGradient = &this->pixGradient;
613
614     const int offset = int( ( WLENGTH * value() + 100 ) / maximum() ) + paddingL;
615
616     const QRectF boundsG( 0, 0, offset , pixGradient->height() );
617     painter.drawPixmap( boundsG, *pixGradient, boundsG );
618
619     const QRectF boundsO( 0, 0, pixOutside.width(), pixOutside.height() );
620     painter.drawPixmap( boundsO, pixOutside, boundsO );
621
622     painter.setPen( palette().color( QPalette::Active, QPalette::Mid ) );
623     QFont font; font.setPixelSize( 9 );
624     painter.setFont( font );
625     const QRect rect( 0, 0, 34, 15 );
626     painter.drawText( rect, Qt::AlignRight | Qt::AlignVCenter,
627                       QString::number( value() ) + '%' );
628
629     painter.end();
630     e->accept();
631 }
632