1 /*****************************************************************************
2 * selector.cpp : Playlist source selector
3 ****************************************************************************
4 * Copyright (C) 2006-2009 the VideoLAN team
7 * Authors: Clément Stenac <zorglub@videolan.org>
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.
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.
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 *****************************************************************************/
30 #include "components/playlist/selector.hpp"
31 #include "playlist_model.hpp" /* plMimeData */
32 #include "input_manager.hpp" /* MainInputManager, for podcast */
34 #include <QApplication>
35 #include <QInputDialog>
36 #include <QMessageBox>
38 #include <QDragMoveEvent>
39 #include <QTreeWidgetItem>
40 #include <QHBoxLayout>
47 #include <vlc_playlist.h>
48 #include <vlc_services_discovery.h>
50 void SelectorActionButton::paintEvent( QPaintEvent
*event
)
53 QColor color
= palette().color( QPalette::HighlightedText
);
56 p
.fillRect( rect(), color
);
58 int frame
= style()->pixelMetric( QStyle::PM_DefaultFrameWidth
, 0, this );
59 p
.drawLine( rect().topLeft() + QPoint( 0, frame
),
60 rect().bottomLeft() - QPoint( 0, frame
) );
61 QFramelessButton::paintEvent( event
);
64 PLSelItem::PLSelItem ( QTreeWidgetItem
*i
, const QString
& text
)
65 : qitem(i
), lblAction( NULL
)
67 layout
= new QHBoxLayout( this );
68 layout
->setContentsMargins(0,0,0,0);
69 layout
->addSpacing( 3 );
71 lbl
= new QElidingLabel( text
);
72 layout
->addWidget(lbl
, 1);
74 int height
= qMax( 22, fontMetrics().height() + 8 );
75 setMinimumHeight( height
);
78 void PLSelItem::addAction( ItemAction act
, const QString
& tooltip
)
80 if( lblAction
) return; //might change later
87 icon
= QIcon( ":/buttons/playlist/playlist_add" ); break;
89 icon
= QIcon( ":/buttons/playlist/playlist_remove" ); break;
94 lblAction
= new SelectorActionButton();
95 lblAction
->setIcon( icon
);
96 int icon_size
= fontMetrics().height();
97 lblAction
->setIconSize( QSize( icon_size
, icon_size
) );
98 lblAction
->setMinimumWidth( lblAction
->sizeHint().width() + icon_size
);
100 if( !tooltip
.isEmpty() ) lblAction
->setToolTip( tooltip
);
102 layout
->addWidget( lblAction
, 0 );
105 CONNECT( lblAction
, clicked(), this, triggerAction() );
109 PLSelector::PLSelector( QWidget
*p
, intf_thread_t
*_p_intf
)
110 : QTreeWidget( p
), p_intf(_p_intf
)
113 setFrameStyle( QFrame::NoFrame
);
114 setAttribute( Qt::WA_MacShowFocusRect
, false );
115 viewport()->setAutoFillBackground( false );
116 setIconSize( QSize( 24,24 ) );
117 setIndentation( 12 );
118 setHeaderHidden( true );
119 setRootIsDecorated( true );
120 setAlternatingRowColors( false );
123 viewport()->setAcceptDrops(true);
124 setDropIndicatorShown(true);
125 invisibleRootItem()->setFlags( invisibleRootItem()->flags() & ~Qt::ItemIsDropEnabled
);
128 setAutoFillBackground( true );
130 palette
.setColor( QPalette::Window
, QColor(209,215,226) );
131 setPalette( palette
);
133 setMinimumHeight( 120 );
136 podcastsParent
= NULL
;
137 podcastsParentId
= -1;
139 /* Podcast connects */
140 CONNECT( THEMIM
, playlistItemAppended( int, int ),
141 this, plItemAdded( int, int ) );
142 CONNECT( THEMIM
, playlistItemRemoved( int ),
143 this, plItemRemoved( int ) );
144 DCONNECT( THEMIM
->getIM(), metaChanged( input_item_t
*),
145 this, inputItemUpdate( input_item_t
* ) );
149 setRootIsDecorated( false );
151 /* Expand at least to show level 2 */
152 for ( int i
= 0; i
< topLevelItemCount(); i
++ )
153 expandItem( topLevelItem( i
) );
156 * We need to react to both clicks and activation (enter-key) here.
157 * We use curItem to avoid rebuilding twice.
158 * See QStyle::SH_ItemView_ActivateItemOnSingleClick
161 CONNECT( this, itemActivated( QTreeWidgetItem
*, int ),
162 this, setSource( QTreeWidgetItem
*) );
163 CONNECT( this, itemClicked( QTreeWidgetItem
*, int ),
164 this, setSource( QTreeWidgetItem
*) );
167 PLSelector::~PLSelector()
171 int c
= podcastsParent
->childCount();
172 for( int i
= 0; i
< c
; i
++ )
174 QTreeWidgetItem
*item
= podcastsParent
->child(i
);
175 input_item_t
*p_input
= item
->data( 0, IN_ITEM_ROLE
).value
<input_item_t
*>();
176 input_item_Release( p_input
);
181 PLSelItem
* putSDData( PLSelItem
* item
, const char* name
, const char* longname
)
183 item
->treeItem()->setData( 0, NAME_ROLE
, qfu( name
) );
184 item
->treeItem()->setData( 0, LONGNAME_ROLE
, qfu( longname
) );
188 PLSelItem
* putPLData( PLSelItem
* item
, playlist_item_t
* plItem
)
190 item
->treeItem()->setData( 0, PL_ITEM_ROLE
, QVariant::fromValue( plItem
) );
191 /* item->setData( 0, PL_ITEM_ID_ROLE, plItem->i_id );
192 item->setData( 0, IN_ITEM_ROLE, QVariant::fromValue( (void*) plItem->p_input ) ); );*/
197 * Reads and updates the playlist's duration as [xx:xx] after the label in the tree
198 * item - the treeview item to get the duration for
199 * prefix - the string to use before the time (should be the category name)
201 void PLSelector::updateTotalDuration( PLSelItem
* item
, const char* prefix
)
203 /* Getting the playlist */
204 QVariant playlistVariant
= item
->treeItem()->data( 0, PL_ITEM_ROLE
);
205 playlist_item_t
* node
= playlistVariant
.value
<playlist_item_t
*>();
207 /* Get the duration of the playlist item */
208 playlist_Lock( THEPL
);
209 mtime_t mt_duration
= playlist_GetNodeDuration( node
);
210 playlist_Unlock( THEPL
);
212 /* Formatting time */
213 QString
qs_timeLabel( prefix
);
215 int i_seconds
= mt_duration
/ 1000000;
216 int i_minutes
= i_seconds
/ 60;
217 i_seconds
= i_seconds
% 60;
218 if( i_minutes
>= 60 )
220 int i_hours
= i_minutes
/ 60;
221 i_minutes
= i_minutes
% 60;
222 qs_timeLabel
+= QString(" [%1:%2:%3]").arg( i_hours
).arg( i_minutes
, 2, 10, QChar('0') ).arg( i_seconds
, 2, 10, QChar('0') );
225 qs_timeLabel
+= QString( " [%1:%2]").arg( i_minutes
, 2, 10, QChar('0') ).arg( i_seconds
, 2, 10, QChar('0') );
227 item
->setText( qs_timeLabel
);
230 void PLSelector::createItems()
233 playlistItem
= putPLData( addItem( PL_ITEM_TYPE
, N_("Playlist"), true ),
235 playlistItem
->treeItem()->setData( 0, SPECIAL_ROLE
, QVariant( IS_PL
) );
236 playlistItem
->treeItem()->setData( 0, Qt::DecorationRole
, QIcon( ":/sidebar/playlist" ) );
237 setCurrentItem( playlistItem
->treeItem() );
240 if( THEPL
->p_media_library
)
242 PLSelItem
*ml
= putPLData( addItem( PL_ITEM_TYPE
, N_("Media Library"), true ),
243 THEPL
->p_media_library
);
244 ml
->treeItem()->setData( 0, SPECIAL_ROLE
, QVariant( IS_ML
) );
245 ml
->treeItem()->setData( 0, Qt::DecorationRole
, QIcon( ":/sidebar/library" ) );
249 QTreeWidgetItem
*mycomp
= addItem( CATEGORY_TYPE
, N_("My Computer"), false, true )->treeItem();
250 QTreeWidgetItem
*devices
= addItem( CATEGORY_TYPE
, N_("Devices"), false, true )->treeItem();
251 QTreeWidgetItem
*lan
= addItem( CATEGORY_TYPE
, N_("Local Network"), false, true )->treeItem();
252 QTreeWidgetItem
*internet
= addItem( CATEGORY_TYPE
, N_("Internet"), false, true )->treeItem();
254 #define NOT_SELECTABLE(w) w->setFlags( w->flags() ^ Qt::ItemIsSelectable );
255 NOT_SELECTABLE( mycomp
);
256 NOT_SELECTABLE( devices
);
257 NOT_SELECTABLE( lan
);
258 NOT_SELECTABLE( internet
);
259 #undef NOT_SELECTABLE
262 char **ppsz_longnames
;
264 char **ppsz_names
= vlc_sd_GetNames( THEPL
, &ppsz_longnames
, &p_categories
);
268 char **ppsz_name
= ppsz_names
, **ppsz_longname
= ppsz_longnames
;
269 int *p_category
= p_categories
;
270 for( ; *ppsz_name
; ppsz_name
++, ppsz_longname
++, p_category
++ )
272 //msg_Dbg( p_intf, "Adding a SD item: %s", *ppsz_longname );
276 QString
name( *ppsz_name
);
277 switch( *p_category
)
279 case SD_CAT_INTERNET
:
281 selItem
= addItem( SD_TYPE
, *ppsz_longname
, false, false, internet
);
282 if( name
.startsWith( "podcast" ) )
284 selItem
->treeItem()->setData( 0, SPECIAL_ROLE
, QVariant( IS_PODCAST
) );
285 selItem
->addAction( ADD_ACTION
, qtr( "Subscribe to a podcast" ) );
286 CONNECT( selItem
, action( PLSelItem
* ), this, podcastAdd( PLSelItem
* ) );
287 podcastsParent
= selItem
->treeItem();
288 icon
= QIcon( ":/sidebar/podcast" );
290 else if ( name
.startsWith( "lua{" ) )
292 int i_head
= name
.indexOf( "sd='" ) + 4;
293 int i_tail
= name
.indexOf( '\'', i_head
);
294 QString iconname
= QString( ":/sidebar/sd/%1" ).arg( name
.mid( i_head
, i_tail
- i_head
) );
295 QResource
resource( iconname
);
296 if ( !resource
.isValid() )
297 icon
= QIcon( ":/sidebar/network" );
299 icon
= QIcon( iconname
);
304 name
= name
.mid( 0, name
.indexOf( '{' ) );
305 selItem
= addItem( SD_TYPE
, *ppsz_longname
, false, false, devices
);
306 if ( name
== "xcb_apps" )
307 icon
= QIcon( ":/sidebar/screen" );
308 else if ( name
== "mtp" )
309 icon
= QIcon( ":/sidebar/mtp" );
310 else if ( name
== "disc" )
311 icon
= QIcon( ":/sidebar/disc" );
313 icon
= QIcon( ":/sidebar/capture" );
316 selItem
= addItem( SD_TYPE
, *ppsz_longname
, false, false, lan
);
317 icon
= QIcon( ":/sidebar/lan" );
319 case SD_CAT_MYCOMPUTER
:
320 name
= name
.mid( 0, name
.indexOf( '{' ) );
321 selItem
= addItem( SD_TYPE
, *ppsz_longname
, false, false, mycomp
);
322 if ( name
== "video_dir" )
323 icon
= QIcon( ":/sidebar/movie" );
324 else if ( name
== "audio_dir" )
325 icon
= QIcon( ":/sidebar/music" );
326 else if ( name
== "picture_dir" )
327 icon
= QIcon( ":/sidebar/pictures" );
329 icon
= QIcon( ":/sidebar/movie" );
332 selItem
= addItem( SD_TYPE
, *ppsz_longname
);
335 selItem
->treeItem()->setData( 0, SD_CATEGORY_ROLE
, *p_category
);
336 putSDData( selItem
, *ppsz_name
, *ppsz_longname
);
337 if ( ! icon
.isNull() )
338 selItem
->treeItem()->setData( 0, Qt::DecorationRole
, icon
);
341 free( *ppsz_longname
);
344 free( ppsz_longnames
);
345 free( p_categories
);
347 if( mycomp
->childCount() == 0 ) delete mycomp
;
348 if( devices
->childCount() == 0 ) delete devices
;
349 if( lan
->childCount() == 0 ) delete lan
;
350 if( internet
->childCount() == 0 ) delete internet
;
353 void PLSelector::setSource( QTreeWidgetItem
*item
)
355 if( !item
|| item
== curItem
)
359 int i_type
= item
->data( 0, TYPE_ROLE
).toInt( &b_ok
);
360 if( !b_ok
|| i_type
== CATEGORY_TYPE
)
364 if( i_type
== SD_TYPE
)
366 QString qs
= item
->data( 0, NAME_ROLE
).toString();
367 sd_loaded
= playlist_IsServicesDiscoveryLoaded( THEPL
, qtu( qs
) );
370 if ( playlist_ServicesDiscoveryAdd( THEPL
, qtu( qs
) ) != VLC_SUCCESS
)
373 services_discovery_descriptor_t test
;
375 if ( playlist_ServicesDiscoveryControl( THEPL
, qtu( qs
),
376 SD_CMD_DESCRIPTOR
, &test
) == VLC_SUCCESS
)
378 item
->setData( 0, CAP_SEARCH_ROLE
, (test
.i_capabilities
& SD_CAP_SEARCH
) );
386 playlist_Lock( THEPL
);
387 playlist_item_t
*pl_item
= NULL
;
389 /* Special case for podcast */
391 if( i_type
== SD_TYPE
)
393 /* Find the right item for the SD */
394 /* FIXME: searching by name - what could possibly go wrong? */
395 pl_item
= playlist_ChildSearchName( &(THEPL
->root
),
396 vlc_gettext(qtu(item
->data(0, LONGNAME_ROLE
).toString())) );
399 if( item
->data( 0, SPECIAL_ROLE
).toInt() == IS_PODCAST
)
401 if( pl_item
&& !sd_loaded
)
403 podcastsParentId
= pl_item
->i_id
;
404 for( int i
=0; i
< pl_item
->i_children
; i
++ )
405 addPodcastItem( pl_item
->pp_children
[i
] );
407 pl_item
= NULL
; //to prevent activating it
411 pl_item
= item
->data( 0, PL_ITEM_ROLE
).value
<playlist_item_t
*>();
413 playlist_Unlock( THEPL
);
418 emit
categoryActivated( pl_item
, false );
419 int i_cat
= item
->data( 0, SD_CATEGORY_ROLE
).toInt();
420 emit
SDCategorySelected( i_cat
== SD_CAT_INTERNET
421 || i_cat
== SD_CAT_LAN
);
425 PLSelItem
* PLSelector::addItem (
426 SelectorItemType type
, const char* str
, bool drop
, bool bold
,
427 QTreeWidgetItem
* parentItem
)
429 QTreeWidgetItem
*item
= parentItem
?
430 new QTreeWidgetItem( parentItem
) : new QTreeWidgetItem( this );
432 PLSelItem
*selItem
= new PLSelItem( item
, qtr( str
) );
433 if ( bold
) selItem
->setStyleSheet( "font-weight: bold;" );
434 setItemWidget( item
, 0, selItem
);
435 item
->setData( 0, TYPE_ROLE
, (int)type
);
436 if( !drop
) item
->setFlags( item
->flags() & ~Qt::ItemIsDropEnabled
);
441 PLSelItem
*PLSelector::addPodcastItem( playlist_item_t
*p_item
)
443 input_item_Hold( p_item
->p_input
);
445 char *psz_name
= input_item_GetName( p_item
->p_input
);
446 PLSelItem
*item
= addItem( PL_ITEM_TYPE
, psz_name
, false, false, podcastsParent
);
449 item
->addAction( RM_ACTION
, qtr( "Remove this podcast subscription" ) );
450 item
->treeItem()->setData( 0, PL_ITEM_ROLE
, QVariant::fromValue( p_item
) );
451 item
->treeItem()->setData( 0, PL_ITEM_ID_ROLE
, QVariant(p_item
->i_id
) );
452 item
->treeItem()->setData( 0, IN_ITEM_ROLE
, QVariant::fromValue( p_item
->p_input
) );
453 CONNECT( item
, action( PLSelItem
* ), this, podcastRemove( PLSelItem
* ) );
457 QStringList
PLSelector::mimeTypes() const
460 types
<< "vlc/qt-input-items";
464 bool PLSelector::dropMimeData ( QTreeWidgetItem
* parent
, int,
465 const QMimeData
* data
, Qt::DropAction
)
467 if( !parent
) return false;
469 QVariant type
= parent
->data( 0, TYPE_ROLE
);
470 if( type
== QVariant() ) return false;
472 int i_truth
= parent
->data( 0, SPECIAL_ROLE
).toInt();
473 if( i_truth
!= IS_PL
&& i_truth
!= IS_ML
) return false;
475 bool to_pl
= ( i_truth
== IS_PL
);
477 const PlMimeData
*plMimeData
= qobject_cast
<const PlMimeData
*>( data
);
478 if( !plMimeData
) return false;
480 QList
<input_item_t
*> inputItems
= plMimeData
->inputItems();
482 playlist_Lock( THEPL
);
484 foreach( input_item_t
*p_input
, inputItems
)
486 playlist_item_t
*p_item
= playlist_ItemGetByInput( THEPL
, p_input
);
487 if( !p_item
) continue;
489 playlist_NodeAddCopy( THEPL
, p_item
,
490 to_pl
? THEPL
->p_playing
: THEPL
->p_media_library
,
494 playlist_Unlock( THEPL
);
499 void PLSelector::dragMoveEvent ( QDragMoveEvent
* event
)
501 event
->setDropAction( Qt::CopyAction
);
502 QAbstractItemView::dragMoveEvent( event
);
505 void PLSelector::plItemAdded( int item
, int parent
)
507 updateTotalDuration(playlistItem
, "Playlist");
508 if( parent
!= podcastsParentId
|| podcastsParent
== NULL
) return;
510 playlist_Lock( THEPL
);
512 playlist_item_t
*p_item
= playlist_ItemGetById( THEPL
, item
);
514 playlist_Unlock( THEPL
);
518 int c
= podcastsParent
->childCount();
519 for( int i
= 0; i
< c
; i
++ )
521 QTreeWidgetItem
*podItem
= podcastsParent
->child(i
);
522 if( podItem
->data( 0, PL_ITEM_ID_ROLE
).toInt() == item
)
524 //msg_Dbg( p_intf, "Podcast already in: (%d) %s", item, p_item->p_input->psz_uri);
525 playlist_Unlock( THEPL
);
530 //msg_Dbg( p_intf, "Adding podcast: (%d) %s", item, p_item->p_input->psz_uri );
531 addPodcastItem( p_item
);
533 playlist_Unlock( THEPL
);
535 podcastsParent
->setExpanded( true );
538 void PLSelector::plItemRemoved( int id
)
540 updateTotalDuration(playlistItem
, "Playlist");
541 if( !podcastsParent
) return;
543 int c
= podcastsParent
->childCount();
544 for( int i
= 0; i
< c
; i
++ )
546 QTreeWidgetItem
*item
= podcastsParent
->child(i
);
547 if( item
->data( 0, PL_ITEM_ID_ROLE
).toInt() == id
)
549 input_item_t
*p_input
= item
->data( 0, IN_ITEM_ROLE
).value
<input_item_t
*>();
550 //msg_Dbg( p_intf, "Removing podcast: (%d) %s", id, p_input->psz_uri );
551 input_item_Release( p_input
);
558 void PLSelector::inputItemUpdate( input_item_t
*arg
)
560 updateTotalDuration(playlistItem
, "Playlist");
562 if( podcastsParent
== NULL
)
565 int c
= podcastsParent
->childCount();
566 for( int i
= 0; i
< c
; i
++ )
568 QTreeWidgetItem
*item
= podcastsParent
->child(i
);
569 input_item_t
*p_input
= item
->data( 0, IN_ITEM_ROLE
).value
<input_item_t
*>();
572 PLSelItem
*si
= itemWidget( item
);
573 char *psz_name
= input_item_GetName( p_input
);
574 si
->setText( qfu( psz_name
) );
581 void PLSelector::podcastAdd( PLSelItem
* )
583 assert( podcastsParent
);
586 QString url
= QInputDialog::getText( this, qtr( "Subscribe" ),
587 qtr( "Enter URL of the podcast to subscribe to:" ),
588 QLineEdit::Normal
, QString(), &ok
);
589 if( !ok
|| url
.isEmpty() ) return;
591 setSource( podcastsParent
); //to load the SD in case it's not loaded
593 QString
request("ADD:");
594 request
+= url
.trimmed();
595 var_SetString( THEPL
, "podcast-request", qtu( request
) );
598 void PLSelector::podcastRemove( PLSelItem
* item
)
600 QString
question ( qtr( "Do you really want to unsubscribe from %1?" ) );
601 question
= question
.arg( item
->text() );
602 QMessageBox::StandardButton res
=
603 QMessageBox::question( this, qtr( "Unsubscribe" ), question
,
604 QMessageBox::Yes
| QMessageBox::No
,
606 if( res
== QMessageBox::No
) return;
608 input_item_t
*input
= item
->treeItem()->data( 0, IN_ITEM_ROLE
).value
<input_item_t
*>();
611 QString
request("RM:");
612 char *psz_uri
= input_item_GetURI( input
);
613 request
+= qfu( psz_uri
);
614 var_SetString( THEPL
, "podcast-request", qtu( request
) );
618 PLSelItem
* PLSelector::itemWidget( QTreeWidgetItem
*item
)
620 return ( static_cast<PLSelItem
*>( QTreeWidget::itemWidget( item
, 0 ) ) );
623 void PLSelector::drawBranches ( QPainter
* painter
, const QRect
& rect
, const QModelIndex
& index
) const
625 if( !model()->hasChildren( index
) ) return;
627 option
.initFrom( this );
628 option
.rect
= rect
.adjusted( rect
.width() - indentation(), 0, 0, 0 );
629 style()->drawPrimitive( isExpanded( index
) ?
630 QStyle::PE_IndicatorArrowDown
:
631 QStyle::PE_IndicatorArrowRight
, &option
, painter
);
634 void PLSelector::getCurrentItemInfos( int* type
, bool* can_delay_search
, QString
*string
)
636 *type
= currentItem()->data( 0, TYPE_ROLE
).toInt();
637 *string
= currentItem()->data( 0, NAME_ROLE
).toString();
638 *can_delay_search
= currentItem()->data( 0, CAP_SEARCH_ROLE
).toBool();
641 int PLSelector::getCurrentItemCategory()
643 return currentItem()->data( 0, SPECIAL_ROLE
).toInt();
646 void PLSelector::wheelEvent( QWheelEvent
*e
)
648 if( verticalScrollBar()->isVisible() && (
649 (verticalScrollBar()->value() != verticalScrollBar()->minimum() && e
->delta() >= 0 ) ||
650 (verticalScrollBar()->value() != verticalScrollBar()->maximum() && e
->delta() < 0 )
652 QApplication::sendEvent(verticalScrollBar(), e
);
654 // Accept this event in order to prevent unwanted volume up/down changes