]> git.sesse.net Git - vlc/blob - modules/gui/qt4/components/playlist/playlist.cpp
Qt: Playlist, code simplification and factorisation
[vlc] / modules / gui / qt4 / components / playlist / playlist.cpp
1 /*****************************************************************************
2  * playlist.cpp : Custom widgets for the playlist
3  ****************************************************************************
4  * Copyright © 2007-2010 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 "components/playlist/playlist.hpp"
30 #include "components/playlist/standardpanel.hpp"  /* MainView */
31 #include "components/playlist/selector.hpp"       /* PLSelector */
32 #include "components/playlist/playlist_model.hpp" /* PLModel */
33 #include "components/interface_widgets.hpp"       /* CoverArtLabel */
34
35 #include "input_manager.hpp"                      /* art signal */
36 #include "main_interface.hpp"                     /* DropEvent TODO remove this*/
37
38 #include <QMenu>
39 #include <QSignalMapper>
40
41 /**********************************************************************
42  * Playlist Widget. The embedded playlist
43  **********************************************************************/
44
45 PlaylistWidget::PlaylistWidget( intf_thread_t *_p_i, QWidget *_par )
46                : QSplitter( _par ), p_intf ( _p_i )
47 {
48     setContentsMargins( 3, 3, 3, 3 );
49
50     /*******************
51      * Left            *
52      *******************/
53     /* We use a QSplitter for the left part */
54     leftSplitter = new QSplitter( Qt::Vertical, this );
55
56     /* Source Selector */
57     PLSelector *selector = new PLSelector( this, p_intf );
58     leftSplitter->addWidget( selector);
59
60     /* Create a Container for the Art Label
61        in order to have a beautiful resizing for the selector above it */
62     QWidget *artContainer = new QWidget;
63     QHBoxLayout *artContLay = new QHBoxLayout( artContainer );
64     artContLay->setMargin( 0 );
65     artContLay->setSpacing( 0 );
66     artContainer->setMaximumHeight( 128 );
67
68     /* Art label */
69     CoverArtLabel *art = new CoverArtLabel( artContainer, p_intf );
70     art->setToolTip( qtr( "Double click to get media information" ) );
71     artContLay->addWidget( art, 1 );
72
73     CONNECT( THEMIM->getIM(), artChanged( QString ),
74              art, showArtUpdate( const QString& ) );
75
76     leftSplitter->addWidget( artContainer );
77
78     /*******************
79      * Right           *
80      *******************/
81     /* Initialisation of the playlist */
82     playlist_t * p_playlist = THEPL;
83     PL_LOCK;
84     playlist_item_t *p_root = THEPL->p_playing;
85     PL_UNLOCK;
86
87     QWidget *rightPanel = new QWidget( this );
88     QGridLayout *layout = new QGridLayout( rightPanel );
89     layout->setSpacing( 0 ); layout->setMargin( 0 );
90     setMinimumWidth( 300 );
91
92     PLModel *model = new PLModel( p_playlist, p_intf, p_root, this );
93     mainView = new StandardPLPanel( this, p_intf, THEPL, p_root, selector, model );
94
95     /* Location Bar */
96     locationBar = new LocationBar( model );
97     locationBar->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Preferred );
98     layout->addWidget( locationBar, 0, 0 );
99     layout->setColumnStretch( 0, 5 );
100     CONNECT( locationBar, invoked( const QModelIndex & ),
101              mainView, browseInto( const QModelIndex & ) );
102
103     /* Button to switch views */
104     QToolButton *viewButton = new QToolButton( this );
105     viewButton->setIcon( style()->standardIcon( QStyle::SP_FileDialogDetailedView ) );
106     viewButton->setToolTip( qtr("Change playlistview") );
107     layout->addWidget( viewButton, 0, 1 );
108
109     /* View selection menu */
110     QSignalMapper *viewSelectionMapper = new QSignalMapper( this );
111     CONNECT( viewSelectionMapper, mapped( int ), mainView, showView( int ) );
112
113     QActionGroup *actionGroup = new QActionGroup( this );
114     QAction *viewActions[StandardPLPanel::VIEW_COUNT];
115     for( int i = 0; i < StandardPLPanel::VIEW_COUNT; i++ )
116     {
117         viewActions[i] = actionGroup->addAction( viewNames[i] );
118         viewActions[i]->setCheckable( true );
119         viewSelectionMapper->setMapping( viewActions[i], i );
120         CONNECT( viewActions[i], triggered(), viewSelectionMapper, map() );
121     }
122
123     QMenu *viewMenu = new QMenu( viewButton );
124     viewMenu->addActions( actionGroup->actions() );
125     viewButton->setMenu( viewMenu );
126     CONNECT( viewButton, clicked(), mainView, cycleViews() );
127
128     /* Search */
129     searchEdit = new SearchLineEdit( this );
130     searchEdit->setMaximumWidth( 250 );
131     searchEdit->setMinimumWidth( 80 );
132     layout->addWidget( searchEdit, 0, 2 );
133     CONNECT( searchEdit, textEdited( const QString& ),
134              mainView, search( const QString& ) );
135     CONNECT( searchEdit, searchDelayedChanged( const QString& ),
136              mainView, searchDelayed( const QString & ) );
137     CONNECT( mainView, viewChanged( const QModelIndex& ),
138              this, changeView( const QModelIndex &) );
139     layout->setColumnStretch( 2, 3 );
140
141     /* Connect the activation of the selector to a redefining of the PL */
142     DCONNECT( selector, activated( playlist_item_t * ),
143               mainView, setRoot( playlist_item_t * ) );
144
145     layout->addWidget( mainView, 1, 0, 1, -1 );
146
147     /* Add the two sides of the QSplitter */
148     addWidget( leftSplitter );
149     addWidget( rightPanel );
150
151     QList<int> sizeList;
152     sizeList << 180 << 420 ;
153     setSizes( sizeList );
154     //setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
155     setStretchFactor( 0, 0 );
156     setStretchFactor( 1, 3 );
157     leftSplitter->setMaximumWidth( 250 );
158     setCollapsible( 1, false );
159
160     /* In case we want to keep the splitter information */
161     // components shall never write there setting to a fixed location, may infer
162     // with other uses of the same component...
163     getSettings()->beginGroup("Playlist");
164     restoreState( getSettings()->value("splitterSizes").toByteArray());
165     leftSplitter->restoreState( getSettings()->value("leftSplitterGeometry").toByteArray() );
166     getSettings()->endGroup();
167
168     setAcceptDrops( true );
169     setWindowTitle( qtr( "Playlist" ) );
170     setWindowRole( "vlc-playlist" );
171     setWindowIcon( QApplication::windowIcon() );
172 }
173
174 PlaylistWidget::~PlaylistWidget()
175 {
176     getSettings()->beginGroup("Playlist");
177     getSettings()->setValue( "splitterSizes", saveState() );
178     getSettings()->setValue( "leftSplitterGeometry", leftSplitter->saveState() );
179     getSettings()->endGroup();
180     msg_Dbg( p_intf, "Playlist Destroyed" );
181 }
182
183 void PlaylistWidget::dropEvent( QDropEvent *event )
184 {
185     if( p_intf->p_sys->p_mi )
186         p_intf->p_sys->p_mi->dropEventPlay( event, false );
187 }
188 void PlaylistWidget::dragEnterEvent( QDragEnterEvent *event )
189 {
190     event->acceptProposedAction();
191 }
192
193 void PlaylistWidget::closeEvent( QCloseEvent *event )
194 {
195     if( THEDP->isDying() )
196     {
197         p_intf->p_sys->p_mi->playlistVisible = true;
198         event->accept();
199     }
200     else
201     {
202         p_intf->p_sys->p_mi->playlistVisible = false;
203         hide();
204         event->ignore();
205     }
206 }
207
208 void PlaylistWidget::forceHide()
209 {
210     leftSplitter->hide();
211     mainView->hide();
212     updateGeometry();
213 }
214
215 void PlaylistWidget::forceShow()
216 {
217     leftSplitter->show();
218     mainView->show();
219     updateGeometry();
220 }
221
222 void PlaylistWidget::changeView( const QModelIndex& index )
223 {
224     searchEdit->clear();
225     locationBar->setIndex( index );
226 }
227
228
229 #include <QSignalMapper>
230 #include <QMenu>
231 #include <QPainter>
232 LocationBar::LocationBar( PLModel *m )
233 {
234     model = m;
235     mapper = new QSignalMapper( this );
236     CONNECT( mapper, mapped( int ), this, invoke( int ) );
237
238     btnMore = new LocationButton( "...", false, true, this );
239     menuMore = new QMenu( this );
240     btnMore->setMenu( menuMore );
241 }
242
243 void LocationBar::setIndex( const QModelIndex &index )
244 {
245     qDeleteAll( buttons );
246     buttons.clear();
247     qDeleteAll( actions );
248     actions.clear();
249
250     QModelIndex i = index;
251     bool first = true;
252
253     while( i.isValid() )
254     {
255         PLItem *item = model->getItem( i );
256
257         char *fb_name = input_item_GetTitleFbName( item->inputItem() );
258         QString text = qfu(fb_name);
259         free(fb_name);
260
261         QAbstractButton *btn = new LocationButton( text, first, !first, this );
262         btn->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Fixed );
263         buttons.append( btn );
264
265         QAction *action = new QAction( text, this );
266         actions.append( action );
267         CONNECT( btn, clicked(), action, trigger() );
268
269         mapper->setMapping( action, item->id() );
270         CONNECT( action, triggered(), mapper, map() );
271
272         first = false;
273
274         i = i.parent();
275     }
276
277     QString prefix;
278     for( int a = actions.count() - 1; a >= 0 ; a-- )
279     {
280         actions[a]->setText( prefix + actions[a]->text() );
281         prefix += QString("  ");
282     }
283
284     if( isVisible() ) layOut( size() );
285 }
286
287 void LocationBar::setRootIndex()
288 {
289     setIndex( QModelIndex() );
290 }
291
292 void LocationBar::invoke( int i_id )
293 {
294     QModelIndex index = model->index( i_id, 0 );
295     emit invoked ( index );
296 }
297
298 void LocationBar::layOut( const QSize& size )
299 {
300     menuMore->clear();
301     widths.clear();
302
303     int count = buttons.count();
304     int totalWidth = 0;
305     for( int i = 0; i < count; i++ )
306     {
307         int w = buttons[i]->sizeHint().width();
308         widths.append( w );
309         totalWidth += w;
310         if( totalWidth > size.width() ) break;
311     }
312
313     int x = 0;
314     int shown = widths.count();
315
316     if( totalWidth > size.width() && count > 1 )
317     {
318         QSize sz = btnMore->sizeHint();
319         btnMore->setGeometry( 0, 0, sz.width(), size.height() );
320         btnMore->show();
321         x = sz.width();
322         totalWidth += x;
323     }
324     else
325     {
326         btnMore->hide();
327     }
328     for( int i = count - 1; i >= 0; i-- )
329     {
330         if( totalWidth <= size.width() || i == 0)
331         {
332             buttons[i]->setGeometry( x, 0, qMin( size.width() - x, widths[i] ), size.height() );
333             buttons[i]->show();
334             x += widths[i];
335             totalWidth -= widths[i];
336         }
337         else
338         {
339             menuMore->addAction( actions[i] );
340             buttons[i]->hide();
341             if( i < shown ) totalWidth -= widths[i];
342         }
343     }
344 }
345
346 void LocationBar::resizeEvent ( QResizeEvent * event )
347 {
348     layOut( event->size() );
349 }
350
351 QSize LocationBar::sizeHint() const
352 {
353     return btnMore->sizeHint();
354 }
355
356 LocationButton::LocationButton( const QString &text, bool bold,
357                                 bool arrow, QWidget * parent )
358   : b_arrow( arrow ), QPushButton( parent )
359 {
360     QFont font;
361     font.setBold( bold );
362     setFont( font );
363     setText( text );
364 }
365
366 #define PADDING 4
367
368 void LocationButton::paintEvent ( QPaintEvent * event )
369 {
370     QStyleOptionButton option;
371     option.initFrom( this );
372     option.state |= QStyle::State_Enabled;
373     QPainter p( this );
374
375     if( underMouse() )
376     {
377         p.save();
378         p.setRenderHint( QPainter::Antialiasing, true );
379         QColor c = palette().color( QPalette::Highlight );
380         p.setPen( c );
381         p.setBrush( c.lighter( 150 ) );
382         p.setOpacity( 0.2 );
383         p.drawRoundedRect( option.rect.adjusted( 0, 2, 0, -2 ), 5, 5 );
384         p.restore();
385     }
386
387     QRect r = option.rect.adjusted( PADDING, 0, -PADDING - (b_arrow ? 10 : 0), 0 );
388
389     QString str( text() );
390     /* This check is absurd, but either it is not done properly inside elidedText(),
391        or boundingRect() is wrong */
392     if( r.width() < fontMetrics().boundingRect( text() ).width() )
393         str = fontMetrics().elidedText( text(), Qt::ElideRight, r.width() );
394     p.drawText( r, Qt::AlignVCenter | Qt::AlignLeft, str );
395
396     if( b_arrow )
397     {
398         option.rect.setWidth( 10 );
399         option.rect.moveRight( rect().right() );
400         style()->drawPrimitive( QStyle::PE_IndicatorArrowRight, &option, &p );
401     }
402 }
403
404 QSize LocationButton::sizeHint() const
405 {
406     QSize s( fontMetrics().boundingRect( text() ).size() );
407     /* Add two pixels to width: font metrics are buggy, if you pass text through elidation
408        with exactly the width of its bounding rect, sometimes it still elides */
409     s.setWidth( s.width() + ( 2 * PADDING ) + ( b_arrow ? 10 : 0 ) + 2 );
410     s.setHeight( s.height() + 2 * PADDING );
411     return s;
412 }
413
414 #undef PADDING