interface_widgets.cpp 18 KB
Newer Older
1
/*****************************************************************************
2
 * interface_widgets.cpp : Custom widgets for the main interface
3
 ****************************************************************************
4
 * Copyright (C) 2006-2008 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 13 14
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
15
 * ( at your option ) any later version.
16 17 18 19 20 21 22 23 24 25
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/
26

27 28 29
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
30

31
#include "components/interface_widgets.hpp"
32 33 34 35

#include "menus.hpp"             /* Popup menu on bgWidget */

#include <vlc_vout.h>
36

Clément Stenac's avatar
Clément Stenac committed
37
#include <QLabel>
38
#include <QToolButton>
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
39 40
#include <QPalette>
#include <QResizeEvent>
41
#include <QDate>
42 43
#include <QMenu>
#include <QWidgetAction>
44
#include <QDesktopWidget>
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
45

46 47 48
#ifdef Q_WS_X11
# include <X11/Xlib.h>
# include <qx11info_x11.h>
49 50 51 52 53 54 55 56 57
static void videoSync( void )
{
    /* Make sure the X server has processed all requests.
     * This protects other threads using distinct connections from getting
     * the video widget window in an inconsistent states. */
    XSync( QX11Info::display(), False );
}
#else
# define videoSync() (void)0
58
#endif
59

60 61
#include <math.h>

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
class ReparentableWidget : public QWidget
{
private:
    VideoWidget *owner;
public:
    ReparentableWidget( VideoWidget *owner ) : owner( owner )
    {
    }

protected:
    void keyPressEvent( QKeyEvent *e )
    {
        emit owner->keyPressed( e );
    }
};

78 79 80 81
/**********************************************************************
 * Video Widget. A simple frame on which video is drawn
 * This class handles resize issues
 **********************************************************************/
82

83
VideoWidget::VideoWidget( intf_thread_t *_p_i ) : QFrame( NULL ), p_intf( _p_i )
84
{
85
    /* Init */
86
    reparentable = NULL;
87 88
    videoSize.rwidth() = -1;
    videoSize.rheight() = -1;
89 90 91 92

    hide();

    /* Set the policy to expand in both directions */
93
//    setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
94

95 96 97
    layout = new QHBoxLayout( this );
    layout->setContentsMargins( 0, 0, 0, 0 );
    setLayout( layout );
98 99 100 101
}

VideoWidget::~VideoWidget()
{
102
    /* Ensure we are not leaking the video output. This would crash. */
103
    assert( reparentable == NULL );
104 105
}

106
/**
107
 * Request the video to avoid the conflicts
108
 **/
109
WId VideoWidget::request( int *pi_x, int *pi_y,
110 111
                          unsigned int *pi_width, unsigned int *pi_height,
                          bool b_keep_size )
112
{
113
    msg_Dbg( p_intf, "Video was requested %i, %i", *pi_x, *pi_y );
114

115 116 117 118 119
    if( reparentable != NULL )
    {
        msg_Dbg( p_intf, "embedded video already in use" );
        return NULL;
    }
120 121 122 123 124 125
    if( b_keep_size )
    {
        *pi_width  = size().width();
        *pi_height = size().height();
    }

126 127 128 129 130
    /* The Qt4 UI needs a fixed a widget ("this"), so that the parent layout is
     * not messed up when we the video is reparented. Hence, we create an extra
     * reparentable widget, that will be within the VideoWidget in windowed
     * mode, and within the root window (NULL parent) in full-screen mode.
     */
131
    reparentable = new ReparentableWidget( this );
132 133 134 135 136 137 138 139 140 141 142
    QLayout *innerLayout = new QHBoxLayout( reparentable );
    innerLayout->setContentsMargins( 0, 0, 0, 0 );

    /* The owner of the video window needs a stable handle (WinId). Reparenting
     * in Qt4-X11 changes the WinId of the widget, so we need to create another
     * dummy widget that stays within the reparentable widget. */
    QWidget *stable = new QWidget();
    QPalette plt = palette();
    plt.setColor( QPalette::Window, Qt::black );
    stable->setPalette( plt );
    stable->setAutoFillBackground(true);
143 144 145
    /* Indicates that the widget wants to draw directly onto the screen.
       Widgets with this attribute set do not participate in composition
       management */
146 147 148 149 150 151 152
    stable->setAttribute( Qt::WA_PaintOnScreen, true );

    innerLayout->addWidget( stable );

    reparentable->setLayout( innerLayout );
    layout->addWidget( reparentable );

153
#ifdef Q_WS_X11
154
    /* HACK: Only one X11 client can subscribe to mouse button press events.
155
     * VLC currently handles those in the video display.
156
     * Force Qt4 to unsubscribe from mouse press and release events. */
157 158 159 160 161
    Display *dpy = QX11Info::display();
    Window w = stable->winId();
    XWindowAttributes attr;

    XGetWindowAttributes( dpy, w, &attr );
162 163
    attr.your_event_mask &= ~(ButtonPressMask|ButtonReleaseMask);
    XSelectInput( dpy, w, attr.your_event_mask );
164
#endif
165
    videoSync();
166
#ifndef NDEBUG
167 168
    msg_Dbg( p_intf, "embedded video ready (handle %p)",
             (void *)stable->winId() );
169
#endif
170
    return stable->winId();
171 172
}

173
/* Set the Widget to the correct Size */
174
/* Function has to be called by the parent
Rémi Denis-Courmont's avatar
typo  
Rémi Denis-Courmont committed
175
   Parent has to care about resizing itself */
176
void VideoWidget::SetSizing( unsigned int w, unsigned int h )
177
{
178 179 180
    msg_Dbg( p_intf, "Video is resizing to: %i %i", w, h );
    videoSize.rwidth() = w;
    videoSize.rheight() = h;
181
    if( !isVisible() ) show();
182
    updateGeometry(); // Needed for deinterlace
183
    videoSync();
184 185
}

186 187 188 189
void VideoWidget::SetFullScreen( bool b_fs )
{
    const Qt::WindowStates curstate = reparentable->windowState();
    Qt::WindowStates newstate = curstate;
190
    Qt::WindowFlags  newflags = reparentable->windowFlags();
191

192

193
    if( b_fs )
194
    {
195
        newstate |= Qt::WindowFullScreen;
196 197
        newflags |= Qt::WindowStaysOnTopHint;
    }
198
    else
199
    {
200
        newstate &= ~Qt::WindowFullScreen;
201 202
        newflags &= ~Qt::WindowStaysOnTopHint;
    }
203 204 205 206 207
    if( newstate == curstate )
        return; /* no changes needed */

    if( b_fs )
    {   /* Go full-screen */
208 209 210 211
        int numscreen = QApplication::desktop()->screenNumber( p_intf->p_sys->p_mi );
        QRect screenres = QApplication::desktop()->screenGeometry( numscreen );

        reparentable->setWindowState( newstate );
212
        reparentable->setParent( NULL );
213
        reparentable->setWindowFlags( newflags );
214 215 216 217 218 219
        /* To be sure window is on proper-screen in xinerama */
        if( !screenres.contains( reparentable->pos() ) )
        {
            msg_Dbg( p_intf, "Moving video to correct screen");
            reparentable->move( QPoint( screenres.x(), screenres.y() ) );
        }
220 221 222 223
        reparentable->show();
    }
    else
    {   /* Go windowed */
224
        reparentable->setWindowFlags( newflags );
225
        layout->addWidget( reparentable );
226
        reparentable->setWindowState( newstate );
227
    }
228
    videoSync();
229 230
}

231
void VideoWidget::release( void )
232
{
Rémi Denis-Courmont's avatar
Typo  
Rémi Denis-Courmont committed
233
    msg_Dbg( p_intf, "Video is not needed anymore" );
234 235 236
    //layout->removeWidget( reparentable );
    delete reparentable;
    reparentable = NULL;
237 238
    videoSize.rwidth() = 0;
    videoSize.rheight() = 0;
239
    updateGeometry();
240
    hide();
241
}
242

243 244 245 246
QSize VideoWidget::sizeHint() const
{
    return videoSize;
}
247

248 249
/**********************************************************************
 * Background Widget. Show a simple image background. Currently,
250
 * it's album art if present or cone.
251
 **********************************************************************/
252 253
#define ICON_SIZE 128
#define MAX_BG_SIZE 400
254
#define MIN_BG_SIZE 128
255

256 257
BackgroundWidget::BackgroundWidget( intf_thread_t *_p_i )
                 :QWidget( NULL ), p_intf( _p_i )
258
{
259
    /* We should use that one to take the more size it can */
260
    setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding);
261

262
    /* A dark background */
Clément Stenac's avatar
Clément Stenac committed
263
    setAutoFillBackground( true );
264
    plt = palette();
265 266 267 268
    plt.setColor( QPalette::Active, QPalette::Window , Qt::black );
    plt.setColor( QPalette::Inactive, QPalette::Window , Qt::black );
    setPalette( plt );

269
    /* A cone in the middle */
270
    label = new QLabel;
271
    label->setMargin( 5 );
272 273
    label->setMaximumHeight( MAX_BG_SIZE );
    label->setMaximumWidth( MAX_BG_SIZE );
274 275
    label->setMinimumHeight( MIN_BG_SIZE );
    label->setMinimumWidth( MIN_BG_SIZE );
276
    label->setAlignment( Qt::AlignCenter );
277
    if( QDate::currentDate().dayOfYear() >= 354 )
278
        label->setPixmap( QPixmap( ":/logo/vlc128-christmas.png" ) );
279
    else
280
        label->setPixmap( QPixmap( ":/logo/vlc128.png" ) );
281

282 283 284 285
    QGridLayout *backgroundLayout = new QGridLayout( this );
    backgroundLayout->addWidget( label, 0, 1 );
    backgroundLayout->setColumnStretch( 0, 1 );
    backgroundLayout->setColumnStretch( 2, 1 );
286

287
    CONNECT( THEMIM->getIM(), artChanged( QString ),
288
             this, updateArt( const QString& ) );
289 290
}

Clément Stenac's avatar
Clément Stenac committed
291
BackgroundWidget::~BackgroundWidget()
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
292
{}
Ilkka Ollakka's avatar
 
Ilkka Ollakka committed
293

294 295 296 297 298 299 300 301
void BackgroundWidget::resizeEvent( QResizeEvent * event )
{
    if( event->size().height() <= MIN_BG_SIZE )
        label->hide();
    else
        label->show();
}

302
void BackgroundWidget::updateArt( const QString& url )
303
{
304
    if( url.isEmpty() )
Ilkka Ollakka's avatar
 
Ilkka Ollakka committed
305
    {
306
        if( QDate::currentDate().dayOfYear() >= 354 )
307
            label->setPixmap( QPixmap( ":/logo/vlc128-christmas.png" ) );
308
        else
309
            label->setPixmap( QPixmap( ":/logo/vlc128.png" ) );
Ilkka Ollakka's avatar
 
Ilkka Ollakka committed
310
    }
311
    else
Ilkka Ollakka's avatar
 
Ilkka Ollakka committed
312
    {
313 314 315 316 317 318 319 320 321
        QPixmap pixmap( url );
        if( pixmap.width() > label->maximumWidth() ||
            pixmap.height() > label->maximumHeight() )
        {
            pixmap = pixmap.scaled( label->maximumWidth(),
                          label->maximumHeight(), Qt::KeepAspectRatio );
        }

        label->setPixmap( pixmap );
Ilkka Ollakka's avatar
 
Ilkka Ollakka committed
322
    }
323 324
}

325 326 327
void BackgroundWidget::contextMenuEvent( QContextMenuEvent *event )
{
    QVLCMenu::PopupMenu( p_intf, true );
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
328
    event->accept();
329
}
330

Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
331
#if 0
332 333 334
#include <QPushButton>
#include <QHBoxLayout>

Clément Stenac's avatar
Clément Stenac committed
335 336 337 338
/**********************************************************************
 * Visualization selector panel
 **********************************************************************/
VisualSelector::VisualSelector( intf_thread_t *_p_i ) :
339
                                QFrame( NULL ), p_intf( _p_i )
Clément Stenac's avatar
Clément Stenac committed
340 341
{
    QHBoxLayout *layout = new QHBoxLayout( this );
342
    layout->setMargin( 0 );
Clément Stenac's avatar
Clément Stenac committed
343
    QPushButton *prevButton = new QPushButton( "Prev" );
344
    QPushButton *nextButton = new QPushButton( "Next" );
Clément Stenac's avatar
Clément Stenac committed
345 346
    layout->addWidget( prevButton );
    layout->addWidget( nextButton );
Clément Stenac's avatar
Clément Stenac committed
347

348
    layout->addStretch( 10 );
349
    layout->addWidget( new QLabel( qtr( "Current visualization" ) ) );
Clément Stenac's avatar
Clément Stenac committed
350 351 352 353 354 355 356

    current = new QLabel( qtr( "None" ) );
    layout->addWidget( current );

    BUTTONACT( prevButton, prev() );
    BUTTONACT( nextButton, next() );

Clément Stenac's avatar
Clément Stenac committed
357
    setLayout( layout );
358
    setMaximumHeight( 35 );
Clément Stenac's avatar
Clément Stenac committed
359 360 361
}

VisualSelector::~VisualSelector()
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
362
{}
Clément Stenac's avatar
Clément Stenac committed
363

Clément Stenac's avatar
Clément Stenac committed
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
void VisualSelector::prev()
{
    char *psz_new = aout_VisualPrev( p_intf );
    if( psz_new )
    {
        current->setText( qfu( psz_new ) );
        free( psz_new );
    }
}

void VisualSelector::next()
{
    char *psz_new = aout_VisualNext( p_intf );
    if( psz_new )
    {
        current->setText( qfu( psz_new ) );
        free( psz_new );
    }
}
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
383
#endif
Clément Stenac's avatar
Clément Stenac committed
384

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
385 386 387
SpeedLabel::SpeedLabel( intf_thread_t *_p_intf, const QString& text,
                        QWidget *parent )
           : QLabel( text, parent ), p_intf( _p_intf )
388
{
389
    setToolTip( qtr( "Current playback speed.\nClick to adjust" ) );
390 391

    /* Create the Speed Control Widget */
392
    speedControl = new SpeedControlWidget( p_intf, this );
393 394 395 396 397 398 399 400 401
    speedControlMenu = new QMenu( this );

    QWidgetAction *widgetAction = new QWidgetAction( speedControl );
    widgetAction->setDefaultWidget( speedControl );
    speedControlMenu->addAction( widgetAction );

    /* Change the SpeedRate in the Status Bar */
    CONNECT( THEMIM->getIM(), rateChanged( int ), this, setRate( int ) );

402
    CONNECT( THEMIM, inputChanged( input_thread_t * ),
403
             speedControl, activateOnState() );
404

405
}
406 407 408 409 410
SpeedLabel::~SpeedLabel()
{
        delete speedControl;
        delete speedControlMenu;
}
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
/****************************************************************************
 * Small right-click menu for rate control
 ****************************************************************************/
void SpeedLabel::showSpeedMenu( QPoint pos )
{
    speedControlMenu->exec( QCursor::pos() - pos
                          + QPoint( 0, height() ) );
}

void SpeedLabel::setRate( int rate )
{
    QString str;
    str.setNum( ( 1000 / (double)rate ), 'f', 2 );
    str.append( "x" );
    setText( str );
    setToolTip( str );
    speedControl->updateControls( rate );
}

430 431 432
/**********************************************************************
 * Speed control widget
 **********************************************************************/
433 434
SpeedControlWidget::SpeedControlWidget( intf_thread_t *_p_i, QWidget *_parent )
                    : QFrame( _parent ), p_intf( _p_i )
435
{
436
    QSizePolicy sizePolicy( QSizePolicy::Maximum, QSizePolicy::Fixed );
437 438
    sizePolicy.setHorizontalStretch( 0 );
    sizePolicy.setVerticalStretch( 0 );
439

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
440
    speedSlider = new QSlider( this );
441 442 443 444 445
    speedSlider->setSizePolicy( sizePolicy );
    speedSlider->setMaximumSize( QSize( 80, 200 ) );
    speedSlider->setOrientation( Qt::Vertical );
    speedSlider->setTickPosition( QSlider::TicksRight );

446
    speedSlider->setRange( -34, 34 );
447 448
    speedSlider->setSingleStep( 1 );
    speedSlider->setPageStep( 1 );
449
    speedSlider->setTickInterval( 17 );
450

451
    CONNECT( speedSlider, valueChanged( int ), this, updateRate( int ) );
452

453
    QToolButton *normalSpeedButton = new QToolButton( this );
454
    normalSpeedButton->setMaximumSize( QSize( 26, 20 ) );
455
    normalSpeedButton->setAutoRaise( true );
456
    normalSpeedButton->setText( "1x" );
457
    normalSpeedButton->setToolTip( qtr( "Revert to normal play speed" ) );
458

459
    CONNECT( normalSpeedButton, clicked(), this, resetRate() );
460

Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
461
    QVBoxLayout *speedControlLayout = new QVBoxLayout( this );
462 463
    speedControlLayout->setLayoutMargins( 4, 4, 4, 4, 4 );
    speedControlLayout->setSpacing( 4 );
464 465
    speedControlLayout->addWidget( speedSlider );
    speedControlLayout->addWidget( normalSpeedButton );
466

467 468
    activateOnState();
}
469

470
void SpeedControlWidget::activateOnState()
471
{
472
    speedSlider->setEnabled( THEMIM->getIM()->hasInput() );
473 474
}

475 476
void SpeedControlWidget::updateControls( int rate )
{
477 478 479 480 481
    if( speedSlider->isSliderDown() )
    {
        //We don't want to change anything if the user is using the slider
        return;
    }
482

483
    double value = 17 * log( (double)INPUT_RATE_DEFAULT / rate ) / log( 2 );
484
    int sliderValue = (int) ( ( value > 0 ) ? value + .5 : value - .5 );
485

486
    if( sliderValue < speedSlider->minimum() )
487
    {
488
        sliderValue = speedSlider->minimum();
489
    }
490
    else if( sliderValue > speedSlider->maximum() )
491
    {
492
        sliderValue = speedSlider->maximum();
493
    }
494

495 496 497 498
    //Block signals to avoid feedback loop
    speedSlider->blockSignals( true );
    speedSlider->setValue( sliderValue );
    speedSlider->blockSignals( false );
499 500 501 502
}

void SpeedControlWidget::updateRate( int sliderValue )
{
503
    double speed = pow( 2, (double)sliderValue / 17 );
504
    int rate = INPUT_RATE_DEFAULT / speed;
505

506
    THEMIM->getIM()->setRate(rate);
507 508 509 510
}

void SpeedControlWidget::resetRate()
{
511
    THEMIM->getIM()->setRate( INPUT_RATE_DEFAULT );
512
}
513

514
CoverArtLabel::CoverArtLabel( QWidget *parent, intf_thread_t *_p_i )
515
              : QLabel( parent ), p_intf( _p_i )
516 517
{
    setContextMenuPolicy( Qt::ActionsContextMenu );
518
    CONNECT( this, updateRequested(), this, askForUpdate() );
519 520 521 522 523

    setMinimumHeight( 128 );
    setMinimumWidth( 128 );
    setMaximumHeight( 128 );
    setMaximumWidth( 128 );
524 525
    setScaledContents( false );
    setAlignment( Qt::AlignCenter );
526

527 528
    QList< QAction* > artActions = actions();
    QAction *action = new QAction( qtr( "Download cover art" ), this );
529
    CONNECT( action, triggered(), this, askForUpdate() );
530
    addAction( action );
531

532
    showArtUpdate( "" );
533 534
}

535 536 537 538 539 540 541
CoverArtLabel::~CoverArtLabel()
{
    QList< QAction* > artActions = actions();
    foreach( QAction *act, artActions )
        removeAction( act );
}

542
void CoverArtLabel::showArtUpdate( const QString& url )
543
{
544 545
    QPixmap pix;
    if( !url.isEmpty()  && pix.load( url ) )
546
    {
547 548
        pix = pix.scaled( maximumWidth(), maximumHeight(),
                          Qt::KeepAspectRatioByExpanding );
549 550 551
    }
    else
    {
552
        pix = QPixmap( ":/noart.png" );
553
    }
554
    setPixmap( pix );
555 556
}

557
void CoverArtLabel::askForUpdate()
558
{
559
    THEMIM->getIM()->requestArtUpdate();
560 561
}

562 563 564 565 566 567 568 569
TimeLabel::TimeLabel( intf_thread_t *_p_intf  ) :QLabel(), p_intf( _p_intf )
{
   b_remainingTime = false;
   setText( " --:--/--:-- " );
   setAlignment( Qt::AlignRight | Qt::AlignVCenter );
   setToolTip( qtr( "Toggle between elapsed and remaining time" ) );


570 571
   CONNECT( THEMIM->getIM(), cachingChanged( float ),
            this, setCaching( float ) );
572 573 574 575 576 577
   CONNECT( THEMIM->getIM(), positionUpdated( float, int, int ),
             this, setDisplayPosition( float, int, int ) );
}

void TimeLabel::setDisplayPosition( float pos, int time, int length )
{
578
    if( pos == -1.f )
579 580 581 582
    {
        setText( " --:--/--:-- " );
        return;
    }
Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
583

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
    char psz_length[MSTRTIME_MAX_SIZE], psz_time[MSTRTIME_MAX_SIZE];
    secstotimestr( psz_length, length );
    secstotimestr( psz_time, ( b_remainingTime && length ) ? length - time
                                                           : time );

    QString timestr;
    timestr.sprintf( "%s/%s", psz_time,
                            ( !length && time ) ? "--:--" : psz_length );

    /* Add a minus to remaining time*/
    if( b_remainingTime && length ) setText( " -"+timestr+" " );
    else setText( " "+timestr+" " );
}

void TimeLabel::toggleTimeDisplay()
{
    b_remainingTime = !b_remainingTime;
}
602

603 604
void TimeLabel::setCaching( float f_cache )
{
605 606 607
    QString amount;
    amount.setNum( (int)(100 * f_cache) );
    msg_Dbg( p_intf, "New caching: %d", (int)(100*f_cache));
608
    setText( "Buff: " + amount + "%" );
609 610
}

611