![]() |
Mixxx
|
00001 // traktorfeature.cpp 00002 // Created 9/26/2010 by Tobias Rafreider 00003 00004 #include <QtDebug> 00005 #include <QMessageBox> 00006 #include <QXmlStreamReader> 00007 #include <QMap> 00008 #include <QSettings> 00009 #include <QDesktopServices> 00010 00011 #include "library/traktor/traktorfeature.h" 00012 00013 #include "library/librarytablemodel.h" 00014 #include "library/missingtablemodel.h" 00015 #include "library/trackcollection.h" 00016 #include "library/treeitem.h" 00017 00018 00019 TraktorFeature::TraktorFeature(QObject* parent, TrackCollection* pTrackCollection): 00020 LibraryFeature(parent), 00021 m_pTrackCollection(pTrackCollection), 00022 m_cancelImport(false) { 00023 QString tableName = "traktor_library"; 00024 QString idColumn = "id"; 00025 QStringList columns; 00026 columns << "id" 00027 << "artist" 00028 << "title" 00029 << "album" 00030 << "year" 00031 << "genre" 00032 << "tracknumber" 00033 << "location" 00034 << "comment" 00035 << "rating" 00036 << "duration" 00037 << "bitrate" 00038 << "bpm" 00039 << "key"; 00040 pTrackCollection->addTrackSource(QString("traktor"), QSharedPointer<BaseTrackCache>( 00041 new BaseTrackCache(m_pTrackCollection, tableName, idColumn, 00042 columns, false))); 00043 00044 m_isActivated = false; 00045 m_pTraktorTableModel = new TraktorTableModel(this, m_pTrackCollection); 00046 m_pTraktorPlaylistModel = new TraktorPlaylistModel(this, m_pTrackCollection); 00047 m_title = tr("Traktor"); 00048 if (!m_database.isOpen()) { 00049 m_database = QSqlDatabase::addDatabase("QSQLITE", "TRAKTOR_SCANNER"); 00050 m_database.setHostName("localhost"); 00051 m_database.setDatabaseName(MIXXX_DB_PATH); 00052 m_database.setUserName("mixxx"); 00053 m_database.setPassword("mixxx"); 00054 00055 //Open the database connection in this thread. 00056 if (!m_database.open()) { 00057 qDebug() << "Failed to open database for iTunes scanner." << m_database.lastError(); 00058 } 00059 } 00060 connect(&m_future_watcher, SIGNAL(finished()), this, SLOT(onTrackCollectionLoaded())); 00061 } 00062 00063 TraktorFeature::~TraktorFeature() { 00064 m_cancelImport = true; 00065 m_future.waitForFinished(); 00066 if(m_pTraktorTableModel) 00067 delete m_pTraktorTableModel; 00068 if(m_pTraktorPlaylistModel) 00069 delete m_pTraktorPlaylistModel; 00070 } 00071 00072 QVariant TraktorFeature::title() { 00073 return m_title; 00074 } 00075 00076 QIcon TraktorFeature::getIcon() { 00077 return QIcon(":/images/library/ic_library_traktor.png"); 00078 } 00079 00080 bool TraktorFeature::isSupported() { 00081 return (QFile::exists(getTraktorMusicDatabase())); 00082 } 00083 00084 TreeItemModel* TraktorFeature::getChildModel() { 00085 return &m_childModel; 00086 } 00087 00088 void TraktorFeature::refreshLibraryModels() { 00089 } 00090 00091 void TraktorFeature::activate() { 00092 qDebug() << "TraktorFeature::activate()"; 00093 00094 if (!m_isActivated) { 00095 m_isActivated = true; 00096 /* Ususally the maximum number of threads 00097 * is > 2 depending on the CPU cores 00098 * Unfortunately, within VirtualBox 00099 * the maximum number of allowed threads 00100 * is 1 at all times We'll need to increase 00101 * the number to > 1, otherwise importing the music collection 00102 * takes place when the GUI threads terminates, i.e., on 00103 * Mixxx shutdown. 00104 */ 00105 QThreadPool::globalInstance()->setMaxThreadCount(4); //Tobias decided to use 4 00106 // Let a worker thread do the XML parsing 00107 m_future = QtConcurrent::run(this, &TraktorFeature::importLibrary, getTraktorMusicDatabase()); 00108 m_future_watcher.setFuture(m_future); 00109 m_title = tr("(loading) Traktor"); 00110 //calls a slot in the sidebar model such that 'iTunes (isLoading)' is displayed. 00111 emit (featureIsLoading(this)); 00112 } else { 00113 emit(showTrackModel(m_pTraktorTableModel)); 00114 } 00115 } 00116 00117 void TraktorFeature::activateChild(const QModelIndex& index) { 00118 00119 if(!index.isValid()) return; 00120 00121 //access underlying TreeItem object 00122 TreeItem *item = static_cast<TreeItem*>(index.internalPointer()); 00123 00124 if(item->isPlaylist()){ 00125 qDebug() << "Activate Traktor Playlist: " << item->dataPath().toString(); 00126 m_pTraktorPlaylistModel->setPlaylist(item->dataPath().toString()); 00127 emit(showTrackModel(m_pTraktorPlaylistModel)); 00128 } 00129 } 00130 00131 void TraktorFeature::onRightClick(const QPoint& globalPos) { 00132 } 00133 00134 void TraktorFeature::onRightClickChild(const QPoint& globalPos, 00135 QModelIndex index) { 00136 } 00137 00138 bool TraktorFeature::dropAccept(QUrl url) { 00139 return false; 00140 } 00141 00142 bool TraktorFeature::dropAcceptChild(const QModelIndex& index, QUrl url) { 00143 return false; 00144 } 00145 00146 bool TraktorFeature::dragMoveAccept(QUrl url) { 00147 return false; 00148 } 00149 00150 bool TraktorFeature::dragMoveAcceptChild(const QModelIndex& index, 00151 QUrl url) { 00152 return false; 00153 } 00154 00155 TreeItem* TraktorFeature::importLibrary(QString file){ 00156 //Give thread a low priority 00157 QThread* thisThread = QThread::currentThread(); 00158 thisThread->setPriority(QThread::LowestPriority); 00159 //Invisible root item of Traktor's child model 00160 TreeItem* root = NULL; 00161 //Delete all table entries of Traktor feature 00162 m_database.transaction(); 00163 clearTable("traktor_playlist_tracks"); 00164 clearTable("traktor_library"); 00165 clearTable("traktor_playlists"); 00166 m_database.commit(); 00167 00168 m_database.transaction(); 00169 QSqlQuery query(m_database); 00170 query.prepare("INSERT INTO traktor_library (artist, title, album, year, genre,comment," 00171 "tracknumber," 00172 "bpm, bitrate," 00173 "duration, location," 00174 "rating," 00175 "key) " 00176 "VALUES (:artist, :title, :album, :year, :genre,:comment, :tracknumber," 00177 ":bpm, :bitrate," 00178 ":duration, :location," 00179 ":rating," 00180 ":key)"); 00181 00182 //Parse Trakor XML file using SAX (for performance) 00183 QFile traktor_file(file); 00184 if (!traktor_file.open(QIODevice::ReadOnly | QIODevice::Text)) { 00185 qDebug() << "Cannot open Traktor music collection"; 00186 return false; 00187 } 00188 QXmlStreamReader xml(&traktor_file); 00189 bool inCollectionTag = false; 00190 bool inEntryTag = false; 00191 bool inPlaylistsTag = false; 00192 bool isRootFolderParsed = false; 00193 int nAudioFiles = 0; 00194 00195 while (!xml.atEnd() && !m_cancelImport) 00196 { 00197 xml.readNext(); 00198 if(xml.isStartElement()) 00199 { 00200 if(xml.name() == "COLLECTION") 00201 { 00202 inCollectionTag = true; 00203 00204 } 00205 /* 00206 * Each "ENTRY" tag in <COLLECTION> represents a track 00207 */ 00208 if(inCollectionTag && xml.name() == "ENTRY" ) 00209 { 00210 inEntryTag = true; 00211 //parse track 00212 parseTrack(xml, query); 00213 00214 ++nAudioFiles; //increment number of files in the music collection 00215 } 00216 if(xml.name() == "PLAYLISTS") 00217 { 00218 inPlaylistsTag = true; 00219 00220 } 00221 if(inPlaylistsTag && !isRootFolderParsed && xml.name() == "NODE"){ 00222 QXmlStreamAttributes attr = xml.attributes(); 00223 QString nodetype = attr.value("TYPE").toString(); 00224 QString name = attr.value("NAME").toString(); 00225 00226 if(nodetype == "FOLDER" && name == "$ROOT"){ 00227 //process all playlists 00228 root = parsePlaylists(xml); 00229 isRootFolderParsed = true; 00230 } 00231 } 00232 00233 } 00234 if(xml.isEndElement()) 00235 { 00236 if(xml.name() == "COLLECTION") 00237 { 00238 inCollectionTag = false; 00239 00240 } 00241 if(xml.name() == "ENTRY" && inCollectionTag) 00242 { 00243 inEntryTag = false; 00244 00245 } 00246 if(xml.name() == "PLAYLISTS" && inPlaylistsTag) 00247 { 00248 inPlaylistsTag = false; 00249 } 00250 00251 } 00252 } 00253 if (xml.hasError()) { 00254 // do error handling 00255 qDebug() << "Cannot process Traktor music collection"; 00256 if(root) 00257 delete root; 00258 return false; 00259 } 00260 00261 qDebug() << "Found: " << nAudioFiles << " audio files in Traktor"; 00262 //initialize TraktorTableModel 00263 m_database.commit(); 00264 00265 return root; 00266 } 00267 00268 void TraktorFeature::parseTrack(QXmlStreamReader &xml, QSqlQuery &query){ 00269 QString title; 00270 QString artist; 00271 QString album; 00272 QString year; 00273 QString genre; 00274 //drive letter 00275 QString volume; 00276 QString path; 00277 QString filename; 00278 QString location; 00279 float bpm = 0.0; 00280 int bitrate = 0; 00281 QString key; 00282 //duration of a track 00283 int playtime = 0; 00284 int rating = 0; 00285 QString comment; 00286 QString tracknumber; 00287 00288 //get XML attributes of starting ENTRY tag 00289 QXmlStreamAttributes attr = xml.attributes (); 00290 title = attr.value("TITLE").toString(); 00291 artist = attr.value("ARTIST").toString(); 00292 00293 //read all sub tags of ENTRY until we reach the closing ENTRY tag 00294 while(!xml.atEnd()) 00295 { 00296 xml.readNext(); 00297 if(xml.isStartElement()){ 00298 if(xml.name() == "ALBUM") 00299 { 00300 QXmlStreamAttributes attr = xml.attributes (); 00301 album = attr.value("TITLE").toString(); 00302 tracknumber = attr.value("TRACK").toString(); 00303 00304 continue; 00305 } 00306 if(xml.name() == "LOCATION") 00307 { 00308 QXmlStreamAttributes attr = xml.attributes (); 00309 volume = attr.value("VOLUME").toString(); 00310 path = attr.value("DIR").toString(); 00311 filename = attr.value("FILE").toString(); 00312 /*compute the location, i.e, combining all the values 00313 * On Windows the volume holds the drive letter e.g., d: 00314 * On OS X, the volume is supposed to be "Macintosh HD" at all times, 00315 * which is a folder in /Volumes/ 00316 */ 00317 #if defined(__APPLE__) 00318 location = "/Volumes/"+volume; 00319 #else 00320 location = volume; 00321 #endif 00322 location += path.replace(QString(":"), QString("")); 00323 location += filename; 00324 continue; 00325 } 00326 if(xml.name() == "INFO") 00327 { 00328 QXmlStreamAttributes attr = xml.attributes(); 00329 key = attr.value("KEY").toString(); 00330 bitrate = attr.value("BITRATE").toString().toInt() / 1000; 00331 playtime = attr.value("PLAYTIME").toString().toInt(); 00332 genre = attr.value("GENRE").toString(); 00333 year = attr.value("RELEASE_DATE").toString(); 00334 comment = attr.value("COMMENT").toString(); 00335 QString ranking_str = attr.value("RANKING").toString(); 00336 /* A ranking in Traktor has ranges between 0 and 255 internally. 00337 * This is same as the POPULARIMETER tag in IDv2, see http://help.mp3tag.de/main_tags.html 00338 * 00339 * Our rating values range from 1 to 5. The mapping is defined as follow 00340 * ourRatingValue = TraktorRating / 51 00341 */ 00342 if(ranking_str != "" && qVariantCanConvert<int>(ranking_str)){ 00343 rating = ranking_str.toInt()/51; 00344 } 00345 continue; 00346 } 00347 if(xml.name() == "TEMPO") 00348 { 00349 QXmlStreamAttributes attr = xml.attributes (); 00350 bpm = attr.value("BPM").toString().toFloat(); 00351 continue; 00352 } 00353 } 00354 //We leave the infinte loop, if twe have the closing tag "ENTRY" 00355 if(xml.name() == "ENTRY" && xml.isEndElement()){ 00356 break; 00357 } 00358 } 00359 00360 /* If we reach the end of ENTRY within the COLLECTION tag 00361 * Save parsed track to database 00362 */ 00363 query.bindValue(":artist", artist); 00364 query.bindValue(":title", title); 00365 query.bindValue(":album", album); 00366 query.bindValue(":genre", genre); 00367 query.bindValue(":year", year); 00368 query.bindValue(":duration", playtime); 00369 query.bindValue(":location", location); 00370 query.bindValue(":rating", rating); 00371 query.bindValue(":comment", comment); 00372 query.bindValue(":tracknumber", tracknumber); 00373 query.bindValue(":key", key); 00374 query.bindValue(":bpm", bpm); 00375 query.bindValue(":bitrate", bitrate); 00376 00377 00378 bool success = query.exec(); 00379 if(!success){ 00380 qDebug() << "SQL Error in TraktorTableModel.cpp: line" << __LINE__ << " " << query.lastError(); 00381 return; 00382 } 00383 00384 } 00385 00386 /* 00387 * Purpose: Parsing all the folder and playlists of Traktor 00388 * This is a complex operation since Traktor uses the concept of folders and playlist. 00389 * A folder can contain folders and playlists. A playlist contains entries but no folders. 00390 * In other words, Traktor uses a tree structure to organize music. Inner nodes represent folders while 00391 * leaves are playlists. 00392 */ 00393 TreeItem* TraktorFeature::parsePlaylists(QXmlStreamReader &xml){ 00394 00395 qDebug() << "Process RootFolder"; 00396 //Each playlist is unique and can be identified by a path in the tree structure. 00397 QString current_path = ""; 00398 QMap<QString,QString> map; 00399 00400 QString delimiter = "-->"; 00401 00402 TreeItem *rootItem = new TreeItem(); 00403 TreeItem * parent = rootItem; 00404 00405 bool inPlaylistTag = false; 00406 00407 QSqlQuery query_insert_to_playlists(m_database); 00408 query_insert_to_playlists.prepare("INSERT INTO traktor_playlists (name) " 00409 "VALUES (:name)"); 00410 00411 QSqlQuery query_insert_to_playlist_tracks(m_database); 00412 query_insert_to_playlist_tracks.prepare( 00413 "INSERT INTO traktor_playlist_tracks (playlist_id, track_id, position) " 00414 "VALUES (:playlist_id, :track_id, :position)"); 00415 00416 while(!xml.atEnd() && !m_cancelImport) { 00417 //read next XML element 00418 xml.readNext(); 00419 00420 if(xml.isStartElement()) 00421 { 00422 if(xml.name() == "NODE"){ 00423 QXmlStreamAttributes attr = xml.attributes(); 00424 QString name = attr.value("NAME").toString(); 00425 QString type = attr.value("TYPE").toString(); 00426 00427 //TODO: What happens if the folder node is a leaf (empty folder) 00428 // Idea: Hide empty folders :-) 00429 if(type == "FOLDER") 00430 { 00431 00432 current_path += delimiter; 00433 current_path += name; 00434 //qDebug() << "Folder: " +current_path << " has parent " << parent->data().toString(); 00435 map.insert(current_path, "FOLDER"); 00436 00437 TreeItem * item = new TreeItem(name,current_path, this, parent); 00438 parent->appendChild(item); 00439 parent = item; 00440 } 00441 if(type == "PLAYLIST") 00442 { 00443 current_path += delimiter; 00444 current_path += name; 00445 //qDebug() << "Playlist: " +current_path << " has parent " << parent->data().toString(); 00446 map.insert(current_path, "PLAYLIST"); 00447 00448 TreeItem * item = new TreeItem(name,current_path, this, parent); 00449 parent->appendChild(item); 00450 // process all the entries within the playlist 'name' having path 'current_path' 00451 parsePlaylistEntries(xml,current_path, query_insert_to_playlists, query_insert_to_playlist_tracks); 00452 } 00453 } 00454 if(xml.name() == "ENTRY" && inPlaylistTag){ 00455 } 00456 } 00457 00458 if(xml.isEndElement()) 00459 { 00460 if(xml.name() == "NODE") 00461 { 00462 if(map.value(current_path) == "FOLDER"){ 00463 parent = parent->parent(); 00464 } 00465 00466 //Whenever we find a closing NODE, remove the last component of the path 00467 int lastSlash = current_path.lastIndexOf(delimiter); 00468 int path_length = current_path.size(); 00469 00470 current_path.remove(lastSlash, path_length - lastSlash); 00471 } 00472 if(xml.name() == "PLAYLIST") 00473 { 00474 inPlaylistTag = false; 00475 } 00476 //We leave the infinte loop, if twe have the closing "PLAYLIST" tag 00477 if(xml.name() == "PLAYLISTS") 00478 { 00479 break; 00480 } 00481 } 00482 } 00483 return rootItem; 00484 } 00485 00486 void TraktorFeature::parsePlaylistEntries(QXmlStreamReader &xml,QString playlist_path, QSqlQuery query_insert_into_playlist, QSqlQuery query_insert_into_playlisttracks) 00487 { 00488 // In the database, the name of a playlist is specified by the unique path, e.g., /someFolderA/someFolderB/playlistA" 00489 query_insert_into_playlist.bindValue(":name", playlist_path); 00490 bool success = query_insert_into_playlist.exec(); 00491 if(!success){ 00492 qDebug() << "SQL Error in TraktorTableModel.cpp: line" << __LINE__ << " " << query_insert_into_playlist.lastError(); 00493 return; 00494 } 00495 //Get playlist id 00496 QSqlQuery id_query(m_database); 00497 id_query.prepare("select id from traktor_playlists where name=:path"); 00498 id_query.bindValue(":path", playlist_path); 00499 success = id_query.exec(); 00500 00501 int playlist_id = -1; 00502 int playlist_position = 1; 00503 if(success){ 00504 //playlist_id = id_query.lastInsertId().toInt(); 00505 while (id_query.next()) { 00506 playlist_id = id_query.value(id_query.record().indexOf("id")).toInt(); 00507 } 00508 } 00509 else 00510 qDebug() << "SQL Error in TraktorTableModel.cpp: line" << __LINE__ << " " << id_query.lastError(); 00511 00512 while(!xml.atEnd() && !m_cancelImport) { 00513 //read next XML element 00514 xml.readNext(); 00515 if(xml.isStartElement()) 00516 { 00517 if(xml.name() == "PRIMARYKEY"){ 00518 QXmlStreamAttributes attr = xml.attributes(); 00519 QString key = attr.value("KEY").toString(); 00520 QString type = attr.value("TYPE").toString(); 00521 if(type == "TRACK") 00522 { 00523 key.replace(QString(":"), QString("")); 00524 //TODO: IFDEF 00525 #if defined(__WINDOWS__) 00526 key.insert(1,":"); 00527 #else 00528 key.prepend("/Volumes/"); 00529 #endif 00530 00531 //insert to database 00532 int track_id = -1; 00533 QSqlQuery finder_query(m_database); 00534 finder_query.prepare("select id from traktor_library where location=:path"); 00535 finder_query.bindValue(":path", key); 00536 success = finder_query.exec(); 00537 00538 if(success){ 00539 while (finder_query.next()) { 00540 track_id = finder_query.value(finder_query.record().indexOf("id")).toInt(); 00541 } 00542 } 00543 else 00544 qDebug() << "SQL Error in TraktorTableModel.cpp: line" << __LINE__ << " " << finder_query.lastError(); 00545 00546 query_insert_into_playlisttracks.bindValue(":playlist_id", playlist_id); 00547 query_insert_into_playlisttracks.bindValue(":track_id", track_id); 00548 query_insert_into_playlisttracks.bindValue(":position", playlist_position++); 00549 success = query_insert_into_playlisttracks.exec(); 00550 if(!success){ 00551 qDebug() << "SQL Error in TraktorFeature.cpp: line" << __LINE__ << " " << query_insert_into_playlisttracks.lastError(); 00552 qDebug() << "trackid" << track_id << " with path " << key; 00553 qDebug() << "playlistname; " << playlist_path <<" with ID " << playlist_id; 00554 qDebug() << "-----------------"; 00555 } 00556 } 00557 } 00558 } 00559 if(xml.isEndElement()){ 00560 //We leave the infinte loop, if twe have the closing "PLAYLIST" tag 00561 if(xml.name() == "PLAYLIST") 00562 { 00563 break; 00564 } 00565 } 00566 } 00567 } 00568 00569 void TraktorFeature::clearTable(QString table_name) 00570 { 00571 QSqlQuery query(m_database); 00572 query.prepare("delete from "+table_name); 00573 bool success = query.exec(); 00574 00575 if(!success) 00576 qDebug() << "Could not delete remove old entries from table " << table_name << " : " << query.lastError(); 00577 else 00578 qDebug() << "Traktor table entries of '" << table_name <<"' have been cleared."; 00579 } 00580 00581 QString TraktorFeature::getTraktorMusicDatabase() 00582 { 00583 QString musicFolder = ""; 00584 00585 /* 00586 * As of version 2, Traktor has changed the path of the collection.nml 00587 * In general, the path is <Home>/Documents/Native Instruments/Traktor 2.x.y/collection.nml 00588 * where x and y denote the bug fix release numbers. For example, Traktor 2.0.3 has the 00589 * following path: <Home>/Documents/Native Instruments/Traktor 2.0.3/collection.nml 00590 */ 00591 00592 //Let's try to detect the latest Traktor version and its collection.nml 00593 QString myDocuments = QDesktopServices::storageLocation(QDesktopServices::DocumentsLocation); 00594 QDir ni_directory(myDocuments +"/Native Instruments/"); 00595 ni_directory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks) ; 00596 00597 //Iterate over the subfolders 00598 QFileInfoList list = ni_directory.entryInfoList(); 00599 QMap<int, QString> installed_ts_map; 00600 00601 for (int i = 0; i < list.size(); ++i) { 00602 QFileInfo fileInfo = list.at(i); 00603 QString folder_name = fileInfo.fileName(); 00604 00605 if(folder_name == "Traktor"){ 00606 //We found a Traktor 1 installation 00607 installed_ts_map.insert(1, fileInfo.absoluteFilePath()); 00608 continue; 00609 } 00610 if(folder_name.contains("Traktor")) 00611 { 00612 qDebug() << "Found " << folder_name; 00613 QVariant sVersion = folder_name.right(5).remove("."); 00614 if(sVersion.canConvert<int>()) 00615 { 00616 installed_ts_map.insert(sVersion.toInt(), fileInfo.absoluteFilePath()); 00617 } 00618 } 00619 } 00620 //If no Traktor installation has been found, return some default string 00621 if(installed_ts_map.isEmpty()){ 00622 musicFolder = QDir::homePath() + "/collection.nml"; 00623 } 00624 else //Select the folder with the highest version as default Traktor folder 00625 { 00626 QList<int> versions = installed_ts_map.keys(); 00627 qSort(versions); 00628 musicFolder = installed_ts_map.value(versions.last()) + "/collection.nml"; 00629 00630 } 00631 qDebug() << "Traktor Library Location=[" << musicFolder << "]"; 00632 return musicFolder; 00633 } 00634 00635 void TraktorFeature::onTrackCollectionLoaded() { 00636 TreeItem* root = m_future.result(); 00637 if (root) { 00638 m_childModel.setRootItem(root); 00639 00640 // Tell the rhythmbox track source that it should re-build its index. 00641 m_pTrackCollection->getTrackSource("traktor")->buildIndex(); 00642 00643 //m_pTraktorTableModel->select(); 00644 emit(showTrackModel(m_pTraktorTableModel)); 00645 qDebug() << "Traktor library loaded successfully"; 00646 } else { 00647 QMessageBox::warning( 00648 NULL, 00649 tr("Error Loading Traktor Library"), 00650 tr("There was an error loading your Traktor library. Some of " 00651 "your Traktor tracks or playlists may not have loaded.")); 00652 } 00653 00654 // calls a slot in the sidebarmodel such that 'isLoading' is removed from the feature title. 00655 m_title = tr("Traktor"); 00656 emit(featureLoadingFinished(this)); 00657 activate(); 00658 } 00659 00660 void TraktorFeature::onLazyChildExpandation(const QModelIndex &index) { 00661 // Nothing to do because the childmodel is not of lazy nature. 00662 }