]> git.sesse.net Git - vlc/blob - modules/gui/qt4/components/interface_widgets.cpp
aec0812c7e9c7360ecdfab3e4e33dd2be94eb615
[vlc] / modules / gui / qt4 / components / interface_widgets.cpp
1 /*****************************************************************************
2  * interface_widgets.cpp : Custom widgets for the main interface
3  ****************************************************************************
4  * Copyright (C) 2006-2010 the VideoLAN team
5  * $Id$
6  *
7  * Authors: Clément Stenac <zorglub@videolan.org>
8  *          Jean-Baptiste Kempf <jb@videolan.org>
9  *          Rafaël Carré <funman@videolanorg>
10  *          Ilkka Ollakka <ileoo@videolan.org>
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * ( at your option ) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25  *****************************************************************************/
26
27 #ifdef HAVE_CONFIG_H
28 # include "config.h"
29 #endif
30
31 #include "components/interface_widgets.hpp"
32 #include "dialogs_provider.hpp"
33 #include "util/customwidgets.hpp"               // qtEventToVLCKey, QVLCStackedWidget
34
35 #include "menus.hpp"             /* Popup menu on bgWidget */
36
37 #include <vlc_vout.h>
38
39 #include <QLabel>
40 #include <QToolButton>
41 #include <QPalette>
42 #include <QEvent>
43 #include <QResizeEvent>
44 #include <QDate>
45 #include <QMenu>
46 #include <QWidgetAction>
47 #include <QDesktopWidget>
48 #include <QPainter>
49 #include <QTimer>
50 #include <QSlider>
51 #include <QBitmap>
52
53 #ifdef Q_WS_X11
54 # include <X11/Xlib.h>
55 # include <qx11info_x11.h>
56 static void videoSync( void )
57 {
58     /* Make sure the X server has processed all requests.
59      * This protects other threads using distinct connections from getting
60      * the video widget window in an inconsistent states. */
61     XSync( QX11Info::display(), False );
62 }
63 #else
64 # define videoSync() (void)0
65 #endif
66
67 #include <math.h>
68 #include <assert.h>
69
70 class ReparentableWidget : public QWidget
71 {
72 private:
73     VideoWidget *owner;
74 public:
75     ReparentableWidget( VideoWidget *owner ) : owner( owner )
76     {}
77 };
78
79 /**********************************************************************
80  * Video Widget. A simple frame on which video is drawn
81  * This class handles resize issues
82  **********************************************************************/
83
84 VideoWidget::VideoWidget( intf_thread_t *_p_i )
85     : QFrame( NULL )
86       , p_intf( _p_i )
87       , reparentable( NULL )
88 {
89     /* Set the policy to expand in both directions */
90     // setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
91
92     layout = new QHBoxLayout( this );
93     layout->setContentsMargins( 0, 0, 0, 0 );
94     setLayout( layout );
95 }
96
97 VideoWidget::~VideoWidget()
98 {
99     /* Ensure we are not leaking the video output. This would crash. */
100     assert( reparentable == NULL );
101 }
102
103 /**
104  * Request the video to avoid the conflicts
105  **/
106 WId VideoWidget::request( int *pi_x, int *pi_y,
107                           unsigned int *pi_width, unsigned int *pi_height,
108                           bool b_keep_size )
109 {
110     msg_Dbg( p_intf, "Video was requested %i, %i", *pi_x, *pi_y );
111
112     if( reparentable != NULL )
113     {
114         msg_Dbg( p_intf, "embedded video already in use" );
115         return NULL;
116     }
117     if( b_keep_size )
118     {
119         *pi_width  = size().width();
120         *pi_height = size().height();
121     }
122
123     /* The Qt4 UI needs a fixed a widget ("this"), so that the parent layout is
124      * not messed up when we the video is reparented. Hence, we create an extra
125      * reparentable widget, that will be within the VideoWidget in windowed
126      * mode, and within the root window (NULL parent) in full-screen mode.
127      */
128     reparentable = new ReparentableWidget( this );
129     reparentable->installEventFilter(this );
130     QLayout *innerLayout = new QHBoxLayout( reparentable );
131     innerLayout->setContentsMargins( 0, 0, 0, 0 );
132
133     /* The owner of the video window needs a stable handle (WinId). Reparenting
134      * in Qt4-X11 changes the WinId of the widget, so we need to create another
135      * dummy widget that stays within the reparentable widget. */
136     QWidget *stable = new QWidget();
137     QPalette plt = palette();
138     plt.setColor( QPalette::Window, Qt::black );
139     stable->setPalette( plt );
140     stable->setAutoFillBackground(true);
141     /* Indicates that the widget wants to draw directly onto the screen.
142        Widgets with this attribute set do not participate in composition
143        management */
144     stable->setAttribute( Qt::WA_PaintOnScreen, true );
145
146     innerLayout->addWidget( stable );
147
148     layout->addWidget( reparentable );
149
150 #ifdef Q_WS_X11
151     /* HACK: Only one X11 client can subscribe to mouse button press events.
152      * VLC currently handles those in the video display.
153      * Force Qt4 to unsubscribe from mouse press and release events. */
154     Display *dpy = QX11Info::display();
155     Window w = stable->winId();
156     XWindowAttributes attr;
157
158     XGetWindowAttributes( dpy, w, &attr );
159     attr.your_event_mask &= ~(ButtonPressMask|ButtonReleaseMask);
160     XSelectInput( dpy, w, attr.your_event_mask );
161 #endif
162     videoSync();
163 #ifndef NDEBUG
164     msg_Dbg( p_intf, "embedded video ready (handle %p)",
165              (void *)stable->winId() );
166 #endif
167     return stable->winId();
168 }
169
170 /* Set the Widget to the correct Size */
171 /* Function has to be called by the parent
172    Parent has to care about resizing itself */
173 void VideoWidget::SetSizing( unsigned int w, unsigned int h )
174 {
175     if (reparentable->windowState() & Qt::WindowFullScreen )
176         return;
177     if( !isVisible() ) show();
178     resize( w, h );
179     emit sizeChanged( w, h );
180     /* Work-around a bug?misconception? that would happen when vout core resize
181        twice to the same size and would make the vout not centered.
182        This cause a small flicker.
183        See #3621
184      */
185     if( size().width() == w && size().height() == h )
186         updateGeometry();
187     videoSync();
188 }
189
190 void VideoWidget::SetFullScreen( bool b_fs )
191 {
192     const Qt::WindowStates curstate = reparentable->windowState();
193     Qt::WindowStates newstate = curstate;
194     Qt::WindowFlags  newflags = reparentable->windowFlags();
195
196
197     if( b_fs )
198     {
199         newstate |= Qt::WindowFullScreen;
200         newflags |= Qt::WindowStaysOnTopHint;
201     }
202     else
203     {
204         newstate &= ~Qt::WindowFullScreen;
205         newflags &= ~Qt::WindowStaysOnTopHint;
206     }
207     if( newstate == curstate )
208         return; /* no changes needed */
209
210     if( b_fs )
211     {   /* Go full-screen */
212         int numscreen = var_InheritInteger( p_intf, "qt-fullscreen-screennumber" );
213         /* if user hasn't defined screennumber, or screennumber that is bigger
214          * than current number of screens, take screennumber where current interface
215          * is
216          */
217         if( numscreen == -1 || numscreen > QApplication::desktop()->numScreens() )
218             numscreen = QApplication::desktop()->screenNumber( p_intf->p_sys->p_mi );
219
220         QRect screenres = QApplication::desktop()->screenGeometry( numscreen );
221
222         /* To be sure window is on proper-screen in xinerama */
223         if( !screenres.contains( reparentable->pos() ) )
224         {
225             msg_Dbg( p_intf, "Moving video to correct screen");
226             reparentable->move( QPoint( screenres.x(), screenres.y() ) );
227         }
228         reparentable->setParent( NULL, newflags );
229         reparentable->setWindowState( newstate );
230
231         /* FIXME: inherit from the vout window, not the interface */
232         char *title = var_InheritString( p_intf, "video-title" );
233         reparentable->setWindowTitle( qfu(title ? title : _("Video")) );
234         free( title );
235
236         reparentable->show();
237     }
238     else
239     {   /* Go windowed */
240         reparentable->setWindowFlags( newflags );
241         reparentable->setWindowState( newstate );
242         layout->addWidget( reparentable );
243     }
244     videoSync();
245 }
246
247 void VideoWidget::release( void )
248 {
249     msg_Dbg( p_intf, "Video is not needed anymore" );
250     //layout->removeWidget( reparentable );
251
252     reparentable->deleteLater();
253     reparentable = NULL;
254     updateGeometry();
255     hide();
256 }
257
258 #undef KeyPress
259 bool VideoWidget::eventFilter(QObject *obj, QEvent *event)
260 {
261     if( obj == reparentable )
262     {
263         if (event->type() == QEvent::Close)
264         {
265             THEDP->quit();
266             return true;
267         }
268         else if( event->type() == QEvent::KeyPress )
269         {
270             emit keyPressed( static_cast<QKeyEvent *>(event) );
271             return true;
272         }
273         else if( event->type() == QEvent::Wheel )
274         {
275             int i_vlckey = qtWheelEventToVLCKey( static_cast<QWheelEvent *>(event) );
276             var_SetInteger( p_intf->p_libvlc, "key-pressed", i_vlckey );
277             return true;
278         }
279     }
280     return false;
281 }
282
283 /**********************************************************************
284  * Background Widget. Show a simple image background. Currently,
285  * it's album art if present or cone.
286  **********************************************************************/
287
288 BackgroundWidget::BackgroundWidget( intf_thread_t *_p_i )
289                  :QWidget( NULL ), p_intf( _p_i ), b_expandPixmap( false )
290 {
291     /* A dark background */
292     setAutoFillBackground( true );
293     QPalette plt = palette();
294     plt.setColor( QPalette::Active, QPalette::Window , Qt::black );
295     plt.setColor( QPalette::Inactive, QPalette::Window , Qt::black );
296     setPalette( plt );
297
298     /* Init the cone art */
299     updateArt( "" );
300
301     CONNECT( THEMIM->getIM(), artChanged( QString ),
302              this, updateArt( const QString& ) );
303 }
304
305 void BackgroundWidget::updateArt( const QString& url )
306 {
307     if ( !url.isEmpty() )
308     {
309         pixmapUrl = url;
310     }
311     else
312     {   /* Xmas joke */
313         if( QDate::currentDate().dayOfYear() >= 354 )
314             pixmapUrl = QString( ":/logo/vlc128-christmas.png" );
315         else
316             pixmapUrl = QString( ":/logo/vlc128.png" );
317     }
318     update();
319 }
320
321 void BackgroundWidget::paintEvent( QPaintEvent *e )
322 {
323     int i_maxwidth, i_maxheight;
324     QPixmap pixmap = QPixmap( pixmapUrl );
325     QPainter painter(this);
326     QBitmap pMask;
327     float f_alpha = 1.0;
328
329     i_maxwidth = std::min( maximumWidth(), width() ) - MARGIN * 2;
330     i_maxheight = std::min( maximumHeight(), height() ) - MARGIN * 2;
331
332     if ( height() > MARGIN * 2 )
333     {
334         /* Scale down the pixmap if the widget is too small */
335         if( pixmap.width() > i_maxwidth || pixmap.height() > i_maxheight )
336         {
337             pixmap = pixmap.scaled( i_maxwidth, i_maxheight,
338                             Qt::KeepAspectRatio, Qt::SmoothTransformation );
339         }
340         else
341         if ( b_expandPixmap &&
342              pixmap.width() < width() && pixmap.height() < height() )
343         {
344             /* Scale up the pixmap to fill widget's size */
345             f_alpha = ( (float) pixmap.height() / (float) height() );
346             pixmap = pixmap.scaled(
347                     width() - MARGIN * 2,
348                     height() - MARGIN * 2,
349                     Qt::KeepAspectRatio,
350                     ( f_alpha < .2 )? /* Don't waste cpu when not visible */
351                         Qt::SmoothTransformation:
352                         Qt::FastTransformation
353                     );
354             /* Non agressive alpha compositing when sizing up */
355             pMask = QBitmap( pixmap.width(), pixmap.height() );
356             pMask.fill( QColor::fromRgbF( 1.0, 1.0, 1.0, f_alpha ) );
357             pixmap.setMask( pMask );
358         }
359
360         painter.drawPixmap(
361                 MARGIN + ( i_maxwidth - pixmap.width() ) /2,
362                 MARGIN + ( i_maxheight - pixmap.height() ) /2,
363                 pixmap);
364     }
365     QWidget::paintEvent( e );
366 }
367
368 void BackgroundWidget::contextMenuEvent( QContextMenuEvent *event )
369 {
370     QVLCMenu::PopupMenu( p_intf, true );
371     event->accept();
372 }
373
374 #if 0
375 #include <QPushButton>
376 #include <QHBoxLayout>
377
378 /**********************************************************************
379  * Visualization selector panel
380  **********************************************************************/
381 VisualSelector::VisualSelector( intf_thread_t *_p_i ) :
382                                 QFrame( NULL ), p_intf( _p_i )
383 {
384     QHBoxLayout *layout = new QHBoxLayout( this );
385     layout->setMargin( 0 );
386     QPushButton *prevButton = new QPushButton( "Prev" );
387     QPushButton *nextButton = new QPushButton( "Next" );
388     layout->addWidget( prevButton );
389     layout->addWidget( nextButton );
390
391     layout->addStretch( 10 );
392     layout->addWidget( new QLabel( qtr( "Current visualization" ) ) );
393
394     current = new QLabel( qtr( "None" ) );
395     layout->addWidget( current );
396
397     BUTTONACT( prevButton, prev() );
398     BUTTONACT( nextButton, next() );
399
400     setLayout( layout );
401     setMaximumHeight( 35 );
402 }
403
404 VisualSelector::~VisualSelector()
405 {}
406
407 void VisualSelector::prev()
408 {
409     char *psz_new = aout_VisualPrev( p_intf );
410     if( psz_new )
411     {
412         current->setText( qfu( psz_new ) );
413         free( psz_new );
414     }
415 }
416
417 void VisualSelector::next()
418 {
419     char *psz_new = aout_VisualNext( p_intf );
420     if( psz_new )
421     {
422         current->setText( qfu( psz_new ) );
423         free( psz_new );
424     }
425 }
426 #endif
427
428 SpeedLabel::SpeedLabel( intf_thread_t *_p_intf, QWidget *parent )
429            : QLabel( parent ), p_intf( _p_intf )
430 {
431     tooltipStringPattern = qtr( "Current playback speed: %1\nClick to adjust" );
432
433     /* Create the Speed Control Widget */
434     speedControl = new SpeedControlWidget( p_intf, this );
435     speedControlMenu = new QMenu( this );
436
437     QWidgetAction *widgetAction = new QWidgetAction( speedControl );
438     widgetAction->setDefaultWidget( speedControl );
439     speedControlMenu->addAction( widgetAction );
440
441     /* Change the SpeedRate in the Status Bar */
442     CONNECT( THEMIM->getIM(), rateChanged( float ), this, setRate( float ) );
443
444     DCONNECT( THEMIM, inputChanged( input_thread_t * ),
445               speedControl, activateOnState() );
446     setRate( var_InheritFloat( p_intf, "rate" ) );
447 }
448
449 SpeedLabel::~SpeedLabel()
450 {
451     delete speedControl;
452     delete speedControlMenu;
453 }
454
455 /****************************************************************************
456  * Small right-click menu for rate control
457  ****************************************************************************/
458
459 void SpeedLabel::showSpeedMenu( QPoint pos )
460 {
461     speedControlMenu->exec( QCursor::pos() - pos
462                           + QPoint( 0, height() ) );
463 }
464
465 void SpeedLabel::setRate( float rate )
466 {
467     QString str;
468     str.setNum( rate, 'f', 2 );
469     str.append( "x" );
470     setText( str );
471     setToolTip( tooltipStringPattern.arg( str ) );
472     speedControl->updateControls( rate );
473 }
474
475 /**********************************************************************
476  * Speed control widget
477  **********************************************************************/
478 SpeedControlWidget::SpeedControlWidget( intf_thread_t *_p_i, QWidget *_parent )
479                     : QFrame( _parent ), p_intf( _p_i )
480 {
481     QSizePolicy sizePolicy( QSizePolicy::Maximum, QSizePolicy::Fixed );
482     sizePolicy.setHorizontalStretch( 0 );
483     sizePolicy.setVerticalStretch( 0 );
484
485     speedSlider = new QSlider( this );
486     speedSlider->setSizePolicy( sizePolicy );
487     speedSlider->setMaximumSize( QSize( 80, 200 ) );
488     speedSlider->setOrientation( Qt::Vertical );
489     speedSlider->setTickPosition( QSlider::TicksRight );
490
491     speedSlider->setRange( -34, 34 );
492     speedSlider->setSingleStep( 1 );
493     speedSlider->setPageStep( 1 );
494     speedSlider->setTickInterval( 17 );
495
496     CONNECT( speedSlider, valueChanged( int ), this, updateRate( int ) );
497
498     QToolButton *normalSpeedButton = new QToolButton( this );
499     normalSpeedButton->setMaximumSize( QSize( 26, 20 ) );
500     normalSpeedButton->setAutoRaise( true );
501     normalSpeedButton->setText( "1x" );
502     normalSpeedButton->setToolTip( qtr( "Revert to normal play speed" ) );
503
504     CONNECT( normalSpeedButton, clicked(), this, resetRate() );
505
506     QVBoxLayout *speedControlLayout = new QVBoxLayout( this );
507     speedControlLayout->setContentsMargins( 4, 4, 4, 4 );
508     speedControlLayout->setSpacing( 4 );
509     speedControlLayout->addWidget( speedSlider );
510     speedControlLayout->addWidget( normalSpeedButton );
511
512     lastValue = 0;
513
514     activateOnState();
515 }
516
517 void SpeedControlWidget::activateOnState()
518 {
519     speedSlider->setEnabled( THEMIM->getIM()->hasInput() );
520 }
521
522 void SpeedControlWidget::updateControls( float rate )
523 {
524     if( speedSlider->isSliderDown() )
525     {
526         //We don't want to change anything if the user is using the slider
527         return;
528     }
529
530     double value = 17 * log( rate ) / log( 2 );
531     int sliderValue = (int) ( ( value > 0 ) ? value + .5 : value - .5 );
532
533     if( sliderValue < speedSlider->minimum() )
534     {
535         sliderValue = speedSlider->minimum();
536     }
537     else if( sliderValue > speedSlider->maximum() )
538     {
539         sliderValue = speedSlider->maximum();
540     }
541     lastValue = sliderValue;
542
543     speedSlider->setValue( sliderValue );
544 }
545
546 void SpeedControlWidget::updateRate( int sliderValue )
547 {
548     if( sliderValue == lastValue )
549         return;
550
551     double speed = pow( 2, (double)sliderValue / 17 );
552     int rate = INPUT_RATE_DEFAULT / speed;
553
554     THEMIM->getIM()->setRate(rate);
555 }
556
557 void SpeedControlWidget::resetRate()
558 {
559     THEMIM->getIM()->setRate( INPUT_RATE_DEFAULT );
560 }
561
562 CoverArtLabel::CoverArtLabel( QWidget *parent, intf_thread_t *_p_i )
563               : QLabel( parent ), p_intf( _p_i )
564 {
565     setContextMenuPolicy( Qt::ActionsContextMenu );
566     CONNECT( this, updateRequested(), this, askForUpdate() );
567
568     setMinimumHeight( 128 );
569     setMinimumWidth( 128 );
570     setMaximumHeight( 128 );
571     setMaximumWidth( 128 );
572     setScaledContents( false );
573     setAlignment( Qt::AlignCenter );
574
575     QList< QAction* > artActions = actions();
576     QAction *action = new QAction( qtr( "Download cover art" ), this );
577     CONNECT( action, triggered(), this, askForUpdate() );
578     addAction( action );
579
580     showArtUpdate( "" );
581 }
582
583 CoverArtLabel::~CoverArtLabel()
584 {
585     QList< QAction* > artActions = actions();
586     foreach( QAction *act, artActions )
587         removeAction( act );
588 }
589
590 void CoverArtLabel::showArtUpdate( const QString& url )
591 {
592     QPixmap pix;
593     if( !url.isEmpty() && pix.load( url ) )
594     {
595         pix = pix.scaled( maximumWidth(), maximumHeight(),
596                           Qt::KeepAspectRatioByExpanding );
597     }
598     else
599     {
600         pix = QPixmap( ":/noart.png" );
601     }
602     setPixmap( pix );
603 }
604
605 void CoverArtLabel::askForUpdate()
606 {
607     THEMIM->getIM()->requestArtUpdate();
608 }
609
610 TimeLabel::TimeLabel( intf_thread_t *_p_intf  )
611     : QLabel(), p_intf( _p_intf ), bufTimer( new QTimer(this) ),
612       buffering( false ), showBuffering(false), bufVal( -1 )
613 {
614     b_remainingTime = false;
615     setText( " --:--/--:-- " );
616     setAlignment( Qt::AlignRight | Qt::AlignVCenter );
617     setToolTip( QString( "- " )
618         + qtr( "Click to toggle between elapsed and remaining time" )
619         + QString( "\n- " )
620         + qtr( "Double click to jump to a chosen time position" ) );
621     bufTimer->setSingleShot( true );
622
623     CONNECT( THEMIM->getIM(), positionUpdated( float, int64_t, int ),
624               this, setDisplayPosition( float, int64_t, int ) );
625     CONNECT( THEMIM->getIM(), cachingChanged( float ),
626               this, updateBuffering( float ) );
627     CONNECT( bufTimer, timeout(), this, updateBuffering() );
628 }
629
630 void TimeLabel::setDisplayPosition( float pos, int64_t t, int length )
631 {
632     showBuffering = false;
633     bufTimer->stop();
634
635     if( pos == -1.f )
636     {
637         setText( " --:--/--:-- " );
638         return;
639     }
640
641     int time = t / 1000000;
642
643     secstotimestr( psz_length, length );
644     secstotimestr( psz_time, ( b_remainingTime && length ) ? length - time
645                                                            : time );
646
647     QString timestr = QString( " %1%2/%3 " )
648             .arg( QString( (b_remainingTime && length) ? "-" : "" ) )
649             .arg( QString( psz_time ) )
650             .arg( QString( ( !length && time ) ? "--:--" : psz_length ) );
651
652     setText( timestr );
653
654     cachedLength = length;
655 }
656
657 void TimeLabel::setDisplayPosition( float pos )
658 {
659     if( pos == -1.f || cachedLength == 0 )
660     {
661         setText( " --:--/--:-- " );
662         return;
663     }
664
665     int time = pos * cachedLength;
666     secstotimestr( psz_time,
667                    ( b_remainingTime && cachedLength ?
668                    cachedLength - time : time ) );
669     QString timestr = QString( " %1%2/%3 " )
670         .arg( QString( (b_remainingTime && cachedLength) ? "-" : "" ) )
671         .arg( QString( psz_time ) )
672         .arg( QString( ( !cachedLength && time ) ? "--:--" : psz_length ) );
673
674     setText( timestr );
675 }
676
677
678 void TimeLabel::toggleTimeDisplay()
679 {
680     b_remainingTime = !b_remainingTime;
681 }
682
683
684 void TimeLabel::updateBuffering( float _buffered )
685 {
686     bufVal = _buffered;
687     if( !buffering || bufVal == 0 )
688     {
689         showBuffering = false;
690         buffering = true;
691         bufTimer->start(200);
692     }
693     else if( bufVal == 1 )
694     {
695         showBuffering = buffering = false;
696         bufTimer->stop();
697     }
698     update();
699 }
700
701 void TimeLabel::updateBuffering()
702 {
703     showBuffering = true;
704     update();
705 }
706
707 void TimeLabel::paintEvent( QPaintEvent* event )
708 {
709     if( showBuffering )
710     {
711         QRect r( rect() );
712         r.setLeft( r.width() * bufVal );
713         QPainter p( this );
714         p.setOpacity( 0.4 );
715         p.fillRect( r, palette().color( QPalette::Highlight ) );
716     }
717     QLabel::paintEvent( event );
718 }