From b6c1d06165f3f04ba75df8bca5b009a89475c6fd Mon Sep 17 00:00:00 2001
From: Leo Franchi <lfranchi@kde.org>
Date: Sat, 17 Sep 2011 12:23:31 -0400
Subject: [PATCH] Add spotify support to jobsview

---
 src/CMakeLists.txt                            | 10 ----
 src/libtomahawk/CMakeLists.txt                | 11 ++++
 src/libtomahawk/dropjob.cpp                   | 22 ++++---
 src/libtomahawk/dropjob.h                     |  2 +-
 .../jobview/JobStatusDelegate.cpp             |  0
 .../jobview/JobStatusDelegate.h               |  0
 src/{ => libtomahawk}/jobview/JobStatusItem.h |  0
 .../jobview/JobStatusModel.cpp                | 33 +++++++++--
 .../jobview/JobStatusModel.h                  |  4 +-
 .../jobview/JobStatusView.cpp                 |  8 ++-
 src/{ => libtomahawk}/jobview/JobStatusView.h | 14 ++++-
 .../jobview/PipelineStatusItem.cpp            |  3 +-
 .../jobview/PipelineStatusItem.h              |  0
 src/libtomahawk/utils/spotifyparser.cpp       | 59 +++++++++++++++----
 src/libtomahawk/utils/spotifyparser.h         | 52 +++++++++-------
 src/tomahawkwindow.h                          |  1 -
 16 files changed, 158 insertions(+), 61 deletions(-)
 rename src/{ => libtomahawk}/jobview/JobStatusDelegate.cpp (100%)
 rename src/{ => libtomahawk}/jobview/JobStatusDelegate.h (100%)
 rename src/{ => libtomahawk}/jobview/JobStatusItem.h (100%)
 rename src/{ => libtomahawk}/jobview/JobStatusModel.cpp (72%)
 rename src/{ => libtomahawk}/jobview/JobStatusModel.h (95%)
 rename src/{ => libtomahawk}/jobview/JobStatusView.cpp (94%)
 rename src/{ => libtomahawk}/jobview/JobStatusView.h (81%)
 rename src/{ => libtomahawk}/jobview/PipelineStatusItem.cpp (96%)
 rename src/{ => libtomahawk}/jobview/PipelineStatusItem.h (100%)

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8fb41dad9..a95e6b622 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -63,10 +63,6 @@ SET( tomahawkSourcesGui ${tomahawkSourcesGui}
      sourcetree/items/genericpageitems.cpp
      sourcetree/items/temporarypageitem.cpp
 
-     jobview/JobStatusView.cpp
-     jobview/JobStatusModel.cpp
-     jobview/JobStatusDelegate.cpp
-     jobview/PipelineStatusItem.cpp
 #     breakpad/BreakPad.cpp
 
      transferview.cpp
@@ -118,12 +114,6 @@ SET( tomahawkHeadersGui ${tomahawkHeadersGui}
      sourcetree/items/genericpageitems.h
      sourcetree/items/temporarypageitem.h
 
-     jobview/JobStatusView.h
-     jobview/JobStatusModel.h
-     jobview/JobStatusDelegate.h
-     jobview/JobStatusItem.h
-     jobview/PipelineStatusItem.h
-
      transferview.h
      tomahawktrayicon.h
      audiocontrols.h
diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt
index eae4c4dac..4cf92d3de 100644
--- a/src/libtomahawk/CMakeLists.txt
+++ b/src/libtomahawk/CMakeLists.txt
@@ -215,6 +215,11 @@ set( libSources
     widgets/headerbreadcrumb.cpp
     widgets/siblingcrumbbutton.cpp
 
+    jobview/JobStatusView.cpp
+    jobview/JobStatusModel.cpp
+    jobview/JobStatusDelegate.cpp
+    jobview/PipelineStatusItem.cpp
+
     thirdparty/kdsingleapplicationguard/kdsingleapplicationguard.cpp
     thirdparty/kdsingleapplicationguard/kdsharedmemorylocker.cpp
     thirdparty/kdsingleapplicationguard/kdtoolsglobal.cpp
@@ -421,6 +426,12 @@ set( libHeaders
     widgets/headerbreadcrumb.h
     widgets/siblingcrumbbutton.h
 
+    jobview/JobStatusView.h
+    jobview/JobStatusModel.h
+    jobview/JobStatusDelegate.h
+    jobview/JobStatusItem.h
+    jobview/PipelineStatusItem.h
+
     thirdparty/kdsingleapplicationguard/kdsingleapplicationguard.h
     thirdparty/Qocoa/qsearchfield.h
 )
diff --git a/src/libtomahawk/dropjob.cpp b/src/libtomahawk/dropjob.cpp
index f65656a6d..c285723ec 100644
--- a/src/libtomahawk/dropjob.cpp
+++ b/src/libtomahawk/dropjob.cpp
@@ -178,13 +178,7 @@ DropJob::parseMimeData( const QMimeData *data )
     else if ( data->hasFormat( "text/plain" ) )
     {
         const QString plainData = QString::fromUtf8( data->data( "text/plain" ) );
-
-        if ( plainData.contains( "xspf" ) )
-            handleXspf( data->data( "text/plain" ).trimmed() );
-        else if ( plainData.contains( "spotify" ) && plainData.contains( "playlist" ) && s_canParseSpotifyPlaylists )
-            handleSpPlaylist( plainData );
-        else
-            handleTrackUrls ( plainData );
+        handleAllUrls( plainData );
     }
 
     m_resultList.append( results );
@@ -415,6 +409,18 @@ DropJob::handleSpPlaylist( const QString& url )
     m_queryCount++;
 }
 
+void
+DropJob::handleAllUrls( const QString& urls )
+{
+    if ( urls.contains( "xspf" ) )
+        handleXspf( urls );
+    else if ( urls.contains( "spotify" ) && urls.contains( "playlist" ) && s_canParseSpotifyPlaylists )
+        handleSpPlaylist( urls );
+    else
+        handleTrackUrls ( urls );
+}
+
+
 void
 DropJob::handleTrackUrls( const QString& urls )
 {
@@ -471,7 +477,7 @@ void
 DropJob::expandedUrls( QStringList urls )
 {
     m_queryCount--;
-    handleTrackUrls( urls.join( "\n" ) );
+    handleAllUrls( urls.join( "\n" ) );
 }
 
 void
diff --git a/src/libtomahawk/dropjob.h b/src/libtomahawk/dropjob.h
index e45b0ff17..4bc81a2bd 100644
--- a/src/libtomahawk/dropjob.h
+++ b/src/libtomahawk/dropjob.h
@@ -105,7 +105,7 @@ private slots:
 private:
     /// handle parsing mime data
 
-
+    void handleAllUrls( const QString& urls );
     void handleTrackUrls( const QString& urls );
     QList< Tomahawk::query_ptr > tracksFromQueryList( const QMimeData* d );
     QList< Tomahawk::query_ptr > tracksFromResultList( const QMimeData* d );
diff --git a/src/jobview/JobStatusDelegate.cpp b/src/libtomahawk/jobview/JobStatusDelegate.cpp
similarity index 100%
rename from src/jobview/JobStatusDelegate.cpp
rename to src/libtomahawk/jobview/JobStatusDelegate.cpp
diff --git a/src/jobview/JobStatusDelegate.h b/src/libtomahawk/jobview/JobStatusDelegate.h
similarity index 100%
rename from src/jobview/JobStatusDelegate.h
rename to src/libtomahawk/jobview/JobStatusDelegate.h
diff --git a/src/jobview/JobStatusItem.h b/src/libtomahawk/jobview/JobStatusItem.h
similarity index 100%
rename from src/jobview/JobStatusItem.h
rename to src/libtomahawk/jobview/JobStatusItem.h
diff --git a/src/jobview/JobStatusModel.cpp b/src/libtomahawk/jobview/JobStatusModel.cpp
similarity index 72%
rename from src/jobview/JobStatusModel.cpp
rename to src/libtomahawk/jobview/JobStatusModel.cpp
index 9d9966be2..fdaf8bc2e 100644
--- a/src/jobview/JobStatusModel.cpp
+++ b/src/libtomahawk/jobview/JobStatusModel.cpp
@@ -17,7 +17,9 @@
  */
 
 #include "JobStatusModel.h"
+
 #include "JobStatusItem.h"
+#include "utils/logger.h"
 
 JobStatusModel::JobStatusModel( QObject* parent )
     : QAbstractListModel ( parent )
@@ -34,11 +36,15 @@ JobStatusModel::~JobStatusModel()
 void
 JobStatusModel::addJob( JobStatusItem* item )
 {
+    connect( item, SIGNAL( statusChanged() ), this, SLOT( itemUpdated() ) );
+    connect( item, SIGNAL( finished() ), this, SLOT( itemFinished() ) );
+
     if ( item->collapseItem() )
     {
         if ( m_collapseCount.contains( item->type() ) )
         {
             m_collapseCount[ item->type() ].append( item );
+            qDebug() << "Adding item:" << item << "TO COLLAPSE ONLY";
             return; // we're done, no new rows
         }
         else
@@ -47,9 +53,7 @@ JobStatusModel::addJob( JobStatusItem* item )
         }
 
     }
-
-    connect( item, SIGNAL( statusChanged() ), this, SLOT( itemUpdated() ) );
-    connect( item, SIGNAL( finished() ), this, SLOT( itemFinished() ) );
+    qDebug() << "Adding item:" << item;
 
     beginInsertRows( QModelIndex(), 0, 0 );
     m_items.prepend( item );
@@ -108,15 +112,36 @@ JobStatusModel::itemFinished()
     JobStatusItem* item = qobject_cast< JobStatusItem* >( sender() );
     Q_ASSERT( item );
 
+//     tDebug() << "Got item finished:" << item->type() << item->mainText() << item;
+//     foreach( JobStatusItem* item, m_items )
+//     {
+//         qDebug() << "ITEM #:" << item;
+//     }
+//     foreach( const QString& str, m_collapseCount.keys() )
+//     {
+//         tDebug() << "\t" << str;
+//         foreach( JobStatusItem* chain, m_collapseCount[ str ] )
+//             qDebug() << "\t\t" << chain;
+//     }
     if ( m_collapseCount.contains( item->type() ) )
     {
+        const int indexOf = m_items.indexOf( m_collapseCount[ item->type() ].first() );
+//         tDebug() << "index in main list of collapsed irst item:" << indexOf;
+        if ( m_collapseCount[ item->type() ].first() == item &&
+             m_items.contains( m_collapseCount[ item->type() ].first() ) && m_collapseCount[ item->type() ].size() > 1 )
+        {
+            // the placeholder we use that links m_items and m_collapsecount is done, so choose another one
+            m_items.replace( m_items.indexOf( m_collapseCount[ item->type() ].first() ), m_collapseCount[ item->type() ][ 1 ] );
+//             qDebug() << "Replaced" << m_collapseCount[ item->type() ].first() << "with:" << m_collapseCount[ item->type() ][ 1 ] << m_items;
+        }
         m_collapseCount[ item->type() ].removeAll( item );
+//         tDebug() << "New collapse count list:" << m_collapseCount[ item->type() ];
         if ( m_collapseCount[ item->type() ].isEmpty() )
             m_collapseCount.remove( item->type() );
         else
         {
             // One less to count, but item is still there
-            const QModelIndex idx = index( m_items.indexOf( m_collapseCount[ item->type() ].first() ), 0, QModelIndex() );
+            const QModelIndex idx = index( indexOf, 0, QModelIndex() );
             emit dataChanged( idx, idx );
             return;
         }
diff --git a/src/jobview/JobStatusModel.h b/src/libtomahawk/jobview/JobStatusModel.h
similarity index 95%
rename from src/jobview/JobStatusModel.h
rename to src/libtomahawk/jobview/JobStatusModel.h
index 7204d26d6..c3611cfd3 100644
--- a/src/jobview/JobStatusModel.h
+++ b/src/libtomahawk/jobview/JobStatusModel.h
@@ -19,10 +19,12 @@
 #ifndef JOBSTATUSMODEL_H
 #define JOBSTATUSMODEL_H
 
+#include "dllmacro.h"
+
 #include <QModelIndex>
 
 class JobStatusItem;
-class JobStatusModel : public QAbstractListModel
+class DLLEXPORT JobStatusModel : public QAbstractListModel
 {
     Q_OBJECT
 public:
diff --git a/src/jobview/JobStatusView.cpp b/src/libtomahawk/jobview/JobStatusView.cpp
similarity index 94%
rename from src/jobview/JobStatusView.cpp
rename to src/libtomahawk/jobview/JobStatusView.cpp
index 7a799f1c2..732875377 100644
--- a/src/jobview/JobStatusView.cpp
+++ b/src/libtomahawk/jobview/JobStatusView.cpp
@@ -34,11 +34,14 @@
 
 using namespace Tomahawk;
 
+JobStatusView* JobStatusView::s_instance = 0;
 
 JobStatusView::JobStatusView( AnimatedSplitter* parent )
     : AnimatedWidget( parent )
     , m_parent( parent )
 {
+    s_instance = this;
+
     setHiddenSize( QSize( 0, 0 ) );
     setLayout( new QVBoxLayout() );
     m_view = new QListView( this );
@@ -73,8 +76,9 @@ JobStatusView::JobStatusView( AnimatedSplitter* parent )
 }
 
 void
-JobStatusView::setModel( QAbstractItemModel* m )
+JobStatusView::setModel( JobStatusModel* m )
 {
+    m_model = m;
     m_view->setModel( m );
     m_view->setItemDelegate( new JobStatusDelegate( m_view ) );
 
@@ -90,6 +94,8 @@ JobStatusView::checkCount()
     else if ( isHidden() && m_view->model()->rowCount() > 0 )
         emit showWidget();
 
+    emit sizeChanged( sizeHint() );
+
 }
 
 
diff --git a/src/jobview/JobStatusView.h b/src/libtomahawk/jobview/JobStatusView.h
similarity index 81%
rename from src/jobview/JobStatusView.h
rename to src/libtomahawk/jobview/JobStatusView.h
index bc6ac3462..1cbc5a7c2 100644
--- a/src/jobview/JobStatusView.h
+++ b/src/libtomahawk/jobview/JobStatusView.h
@@ -22,17 +22,22 @@
 
 #include "typedefs.h"
 #include "widgets/animatedsplitter.h"
+#include "dllmacro.h"
 
 class QAbstractItemModel;
 class QListView;
 class JobStatusModel;
 class StreamConnection;
 
-class JobStatusView : public AnimatedWidget
+class DLLEXPORT JobStatusView : public AnimatedWidget
 {
 Q_OBJECT
 
 public:
+    static JobStatusView* instance() {
+        return s_instance;
+    }
+
     explicit JobStatusView( AnimatedSplitter* parent );
     virtual ~JobStatusView()
     {
@@ -40,14 +45,19 @@ public:
 
     QSize sizeHint() const;
 
-    void setModel( QAbstractItemModel* model );
+    void setModel( JobStatusModel* model );
+
+    JobStatusModel* model() { return m_model; }
 
 private slots:
     void checkCount();
 
 private:
     QListView* m_view;
+    JobStatusModel* m_model;
     AnimatedSplitter* m_parent;
+
+    static JobStatusView* s_instance;
 };
 
 #endif // JOBSTATUSVIEW_H
diff --git a/src/jobview/PipelineStatusItem.cpp b/src/libtomahawk/jobview/PipelineStatusItem.cpp
similarity index 96%
rename from src/jobview/PipelineStatusItem.cpp
rename to src/libtomahawk/jobview/PipelineStatusItem.cpp
index 2e47ae876..283ef509e 100644
--- a/src/jobview/PipelineStatusItem.cpp
+++ b/src/libtomahawk/jobview/PipelineStatusItem.cpp
@@ -22,6 +22,7 @@
 #include "pipeline.h"
 #include "tomahawkapp.h"
 #include "JobStatusModel.h"
+#include "JobStatusView.h"
 
 PipelineStatusItem::PipelineStatusItem()
     : JobStatusItem()
@@ -76,6 +77,6 @@ PipelineStatusManager::resolving( const Tomahawk::query_ptr& p )
     {
         // No current query item and we're resolving something, so show it
         m_curItem = QWeakPointer< PipelineStatusItem >( new PipelineStatusItem );
-        APP->mainWindow()->jobsModel()->addJob( m_curItem.data() );
+        JobStatusView::instance()->model()->addJob( m_curItem.data() );
     }
 }
diff --git a/src/jobview/PipelineStatusItem.h b/src/libtomahawk/jobview/PipelineStatusItem.h
similarity index 100%
rename from src/jobview/PipelineStatusItem.h
rename to src/libtomahawk/jobview/PipelineStatusItem.h
diff --git a/src/libtomahawk/utils/spotifyparser.cpp b/src/libtomahawk/utils/spotifyparser.cpp
index 4851e8edb..6d1971eb2 100644
--- a/src/libtomahawk/utils/spotifyparser.cpp
+++ b/src/libtomahawk/utils/spotifyparser.cpp
@@ -22,6 +22,9 @@
 #include "utils/tomahawkutils.h"
 #include "query.h"
 #include "sourcelist.h"
+#include "jobview/JobStatusView.h"
+#include "jobview/JobStatusModel.h"
+
 #include <qjson/parser.h>
 
 #include <QtNetwork/QNetworkAccessManager>
@@ -29,30 +32,51 @@
 
 using namespace Tomahawk;
 
-QPixmap SpotifyParser::s_pixmap = QPixmap();
+QPixmap* SpotifyParser::s_pixmap = 0;
 
-SpotifyJobNotifier::SpotifyJobNotifier( const QString &type, const QPixmap& pixmap )
+SpotifyJobNotifier::SpotifyJobNotifier( QNetworkReply* job )
     : JobStatusItem()
-    , m_type( type )
-    , m_icon( pixmap )
+    , m_type( "track" )
+    , m_job( job )
+{
+    connect( job, SIGNAL( finished() ), this, SLOT( setFinished()) );
+}
+
+SpotifyJobNotifier::SpotifyJobNotifier()
+    : JobStatusItem()
+    , m_type( "playlist" )
+    , m_job( 0 )
 {
 }
 
+
 SpotifyJobNotifier::~SpotifyJobNotifier()
 {}
 
 QString
 SpotifyJobNotifier::rightColumnText() const
 {
-
+    return QString();
 }
 
+QPixmap
+SpotifyJobNotifier::icon() const
+{
+    return SpotifyParser::pixmap();
+}
+
+
 QString
 SpotifyJobNotifier::mainText() const
 {
-
+    return tr( "Parsing Spotify %1" ).arg( m_type );
 }
 
+void
+SpotifyJobNotifier::setFinished()
+{
+    emit finished();
+}
 
 
 SpotifyParser::SpotifyParser( const QStringList& Urls, bool createNewPlaylist, QObject* parent )
@@ -60,6 +84,7 @@ SpotifyParser::SpotifyParser( const QStringList& Urls, bool createNewPlaylist, Q
     , m_single( false )
     , m_trackMode( true )
     , m_createNewPlaylist( createNewPlaylist )
+    , m_playlistJob( 0 )
 
 {
     foreach ( const QString& url, Urls )
@@ -71,16 +96,13 @@ SpotifyParser::SpotifyParser( const QString& Url, bool createNewPlaylist, QObjec
     , m_single( true )
     , m_trackMode( true )
     , m_createNewPlaylist( createNewPlaylist )
+    , m_playlistJob( 0 )
 {
-    if ( s_pixmap.isNull() )
-        s_pixmap.load( RESPATH "images/spotify-logo.jpg" );
-
     lookupUrl( Url );
 }
 
 SpotifyParser::~SpotifyParser()
 {
-
 }
 
 
@@ -116,6 +138,9 @@ SpotifyParser::lookupPlaylist( const QString& link )
     QNetworkReply* reply = TomahawkUtils::nam()->get( QNetworkRequest( url ) );
     connect( reply, SIGNAL( finished() ), this, SLOT( spotifyPlaylistLookupFinished() ) );
 
+    m_playlistJob = new SpotifyJobNotifier();
+    JobStatusView::instance()->model()->addJob( m_playlistJob );
+
     m_queries.insert( reply );
 }
 
@@ -142,6 +167,9 @@ SpotifyParser::lookupTrack( const QString& link )
     QNetworkReply* reply = TomahawkUtils::nam()->get( QNetworkRequest( url ) );
     connect( reply, SIGNAL( finished() ), this, SLOT( spotifyTrackLookupFinished() ) );
 
+    SpotifyJobNotifier* j = new SpotifyJobNotifier( reply );
+    JobStatusView::instance()->model()->addJob( j );
+
     m_queries.insert( reply );
 
 }
@@ -262,6 +290,8 @@ SpotifyParser::checkPlaylistFinished()
     tDebug() << "Checking for spotify batch playlist job finished" << m_queries.isEmpty() << m_createNewPlaylist;
     if ( m_queries.isEmpty() ) // we're done
     {
+        if ( m_playlistJob )
+            m_playlistJob->setFinished();
         if( m_createNewPlaylist )
             m_playlist = Playlist::create( SourceList::instance()->getLocal(),
                                        uuid(),
@@ -294,3 +324,12 @@ SpotifyParser::checkTrackFinished()
     }
 
 }
+
+QPixmap
+SpotifyParser::pixmap()
+{
+    if ( !s_pixmap )
+        s_pixmap = new QPixmap( RESPATH "images/spotify-logo.jpg" );
+
+    return *s_pixmap;
+}
diff --git a/src/libtomahawk/utils/spotifyparser.h b/src/libtomahawk/utils/spotifyparser.h
index ea629545c..654258962 100644
--- a/src/libtomahawk/utils/spotifyparser.h
+++ b/src/libtomahawk/utils/spotifyparser.h
@@ -34,6 +34,31 @@ class QNetworkReply;
 namespace Tomahawk
 {
 
+class DLLEXPORT SpotifyJobNotifier : public JobStatusItem
+{
+    Q_OBJECT
+public:
+    // track
+    SpotifyJobNotifier( QNetworkReply* job );
+    // playlist
+    SpotifyJobNotifier();
+    virtual ~SpotifyJobNotifier();
+
+    virtual QString rightColumnText() const;
+    virtual QString mainText() const;
+    virtual QPixmap icon() const;
+    virtual QString type() const { return m_type; }
+    virtual bool collapseItem() const { return true; }
+
+public slots:
+    void setFinished();
+
+private:
+    QString m_type;
+    QNetworkReply* m_job;
+};
+
+
 /**
  * Small class to parse spotify links into query_ptrs
  *
@@ -43,6 +68,7 @@ class DLLEXPORT SpotifyParser : public QObject
 {
     Q_OBJECT
 public:
+    friend class SpotifyJobNotifier;
     explicit SpotifyParser( const QString& trackUrl, bool createNewPlaylist = false, QObject* parent = 0 );
     explicit SpotifyParser( const QStringList& trackUrls, bool createNewPlaylist = false, QObject* parent = 0 );
     virtual ~SpotifyParser();
@@ -57,6 +83,8 @@ private slots:
     void spotifyPlaylistLookupFinished();
 
 private:
+    static QPixmap pixmap();
+
     void lookupUrl( const QString& url );
     void lookupTrack( const QString& track );
     void lookupPlaylist( const QString& playlist );
@@ -70,29 +98,9 @@ private:
     QSet< QNetworkReply* > m_queries;
     QString m_title, m_info, m_creator;
     Tomahawk::playlist_ptr m_playlist;
+    SpotifyJobNotifier* m_playlistJob;
 
-    static QPixmap s_pixmap;
-};
-
-class DLLEXPORT SpotifyJobNotifier : public JobStatusItem
-{
-    Q_OBJECT
-
-    friend class SpotifyParser;
-public:
-    SpotifyJobNotifier( const QString& type, const QPixmap& pixmap );
-    virtual ~SpotifyJobNotifier();
-
-    virtual QString rightColumnText() const;
-    virtual QString mainText() const;
-    virtual QPixmap icon() const { return m_icon; }
-    virtual QString type() const { return m_type; }
-    virtual bool collapseItem() const { return true; }
-
-private:
-    void set
-    QPixmap m_icon;
-    QString m_type;
+    static QPixmap* s_pixmap;
 };
 
 }
diff --git a/src/tomahawkwindow.h b/src/tomahawkwindow.h
index 4d490ffd1..ee62cb161 100644
--- a/src/tomahawkwindow.h
+++ b/src/tomahawkwindow.h
@@ -57,7 +57,6 @@ public:
 
     AudioControls* audioControls() { return m_audioControls; }
     SourceTreeView* sourceTreeView() const { return m_sourcetree; }
-    JobStatusModel* jobsModel() const { return m_jobsModel; }
 
     void setWindowTitle( const QString& title );