File indexing completed on 2024-05-05 04:48:24
0001 /**************************************************************************************** 0002 * Copyright (c) 2003 Scott Wheeler <wheeler@kde.org> * 0003 * Copyright (c) 2004 Max Howell <max.howell@methylblue.com> * 0004 * Copyright (c) 2004-2008 Mark Kretschmann <kretschmann@kde.org> * 0005 * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org> * 0006 * Copyright (c) 2008 Sebastian Trueg <trueg@kde.org> * 0007 * Copyright (c) 2013 Ralf Engels <ralf-engels@gmx.de> * 0008 * * 0009 * This program is free software; you can redistribute it and/or modify it under * 0010 * the terms of the GNU General Public License as published by the Free Software * 0011 * Foundation; either version 2 of the License, or (at your option) any later * 0012 * version. * 0013 * * 0014 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0015 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0016 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0017 * * 0018 * You should have received a copy of the GNU General Public License along with * 0019 * this program. If not, see <http://www.gnu.org/licenses/>. * 0020 ****************************************************************************************/ 0021 0022 #include "CollectionSetup.h" 0023 0024 #include "amarokconfig.h" 0025 #include "core/collections/Collection.h" 0026 #include "core/support/Debug.h" 0027 #include "core-impl/collections/support/CollectionManager.h" 0028 #include "dialogs/DatabaseImporterDialog.h" 0029 0030 #include <KLocalizedString> 0031 #include <QPushButton> 0032 #include <QVBoxLayout> 0033 0034 #include <QAction> 0035 #include <QApplication> 0036 #include <QCheckBox> 0037 #include <QDir> 0038 #include <QFile> 0039 #include <QLabel> 0040 #include <QMenu> 0041 0042 #include <algorithm> 0043 0044 CollectionSetup* CollectionSetup::s_instance; 0045 0046 0047 CollectionSetup::CollectionSetup( QWidget *parent ) 0048 : QWidget( parent ) 0049 , m_rescanDirAction( new QAction( this ) ) 0050 { 0051 m_ui.setupUi(this); 0052 0053 setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); 0054 0055 setObjectName( "CollectionSetup" ); 0056 s_instance = this; 0057 0058 m_ui.view->setAnimated( true ); 0059 connect( m_ui.view, &QTreeView::clicked, 0060 this, &CollectionSetup::changed ); 0061 0062 connect( m_ui.view, &QTreeView::pressed, 0063 this, &CollectionSetup::slotPressed ); 0064 connect( m_rescanDirAction, &QAction::triggered, 0065 this, &CollectionSetup::slotRescanDirTriggered ); 0066 0067 QPushButton *rescan = new QPushButton( QIcon::fromTheme( "collection-rescan-amarok" ), i18n( "Full rescan" ), m_ui.buttonContainer ); 0068 rescan->setToolTip( i18n( "Rescan your entire collection. This will <i>not</i> delete any statistics." ) ); 0069 connect( rescan, &QAbstractButton::clicked, CollectionManager::instance(), &CollectionManager::startFullScan ); 0070 0071 QPushButton *import = new QPushButton( QIcon::fromTheme( "tools-wizard" ), i18n( "Import batch file..." ), m_ui.buttonContainer ); 0072 import->setToolTip( i18n( "Import collection from file produced by amarokcollectionscanner." ) ); 0073 connect( import, &QAbstractButton::clicked, this, &CollectionSetup::importCollection ); 0074 0075 QHBoxLayout *buttonLayout = new QHBoxLayout(); 0076 buttonLayout->addWidget( rescan ); 0077 buttonLayout->addWidget( import ); 0078 m_ui.buttonContainer->setLayout( buttonLayout ); 0079 0080 m_recursive = new QCheckBox( i18n("&Scan folders recursively (requires full rescan if newly checked)"), m_ui.checkboxContainer ); 0081 m_monitor = new QCheckBox( i18n("&Watch folders for changes"), m_ui.checkboxContainer ); 0082 connect( m_recursive, &QCheckBox::toggled, this, &CollectionSetup::changed ); 0083 connect( m_monitor , &QCheckBox::toggled, this, &CollectionSetup::changed ); 0084 0085 QVBoxLayout *checkboxLayout = new QVBoxLayout(); 0086 checkboxLayout->addWidget( m_recursive ); 0087 checkboxLayout->addWidget( m_monitor ); 0088 m_ui.checkboxContainer->setLayout( checkboxLayout ); 0089 0090 m_recursive->setToolTip( i18n( "If selected, Amarok will read all subfolders." ) ); 0091 m_monitor->setToolTip( i18n( "If selected, the collection folders will be watched " 0092 "for changes.\nThe watcher will not notice changes behind symbolic links." ) ); 0093 0094 m_recursive->setChecked( AmarokConfig::scanRecursively() ); 0095 m_monitor->setChecked( AmarokConfig::monitorChanges() ); 0096 0097 // set the model _after_ constructing the checkboxes 0098 m_model = new CollectionFolder::Model( this ); 0099 m_ui.view->setModel( m_model ); 0100 #ifndef Q_OS_WIN 0101 m_ui.view->setRootIndex( m_model->setRootPath( QDir::rootPath() ) ); 0102 #else 0103 m_ui.view->setRootIndex( m_model->setRootPath( m_model->myComputer().toString() ) ); 0104 #endif 0105 0106 Collections::Collection *primaryCollection = CollectionManager::instance()->primaryCollection(); 0107 QStringList dirs = primaryCollection ? primaryCollection->property( "collectionFolders" ).toStringList() : QStringList(); 0108 m_model->setDirectories( dirs ); 0109 0110 // make sure that the tree is expanded to show all selected items 0111 foreach( const QString &dir, dirs ) 0112 { 0113 QModelIndex index = m_model->index( dir ); 0114 m_ui.view->scrollTo( index, QAbstractItemView::EnsureVisible ); 0115 } 0116 } 0117 0118 void 0119 CollectionSetup::writeConfig() 0120 { 0121 DEBUG_BLOCK 0122 0123 AmarokConfig::setScanRecursively( recursive() ); 0124 AmarokConfig::setMonitorChanges( monitor() ); 0125 0126 Collections::Collection *primaryCollection = CollectionManager::instance()->primaryCollection(); 0127 QStringList collectionFolders = primaryCollection ? primaryCollection->property( "collectionFolders" ).toStringList() : QStringList(); 0128 0129 if( m_model->directories() != collectionFolders ) 0130 { 0131 debug() << "Selected collection folders: " << m_model->directories(); 0132 if( primaryCollection ) 0133 primaryCollection->setProperty( "collectionFolders", m_model->directories() ); 0134 0135 debug() << "Old collection folders: " << collectionFolders; 0136 CollectionManager::instance()->startFullScan(); 0137 } 0138 } 0139 0140 bool 0141 CollectionSetup::hasChanged() const 0142 { 0143 Collections::Collection *primaryCollection = CollectionManager::instance()->primaryCollection(); 0144 QStringList collectionFolders = primaryCollection ? primaryCollection->property( "collectionFolders" ).toStringList() : QStringList(); 0145 0146 return 0147 m_model->directories() != collectionFolders || 0148 m_recursive->isChecked() != AmarokConfig::scanRecursively() || 0149 m_monitor->isChecked() != AmarokConfig::monitorChanges(); 0150 } 0151 0152 bool 0153 CollectionSetup::recursive() const 0154 { return m_recursive && m_recursive->isChecked(); } 0155 0156 bool 0157 CollectionSetup::monitor() const 0158 { return m_monitor && m_monitor->isChecked(); } 0159 0160 const QString 0161 CollectionSetup::modelFilePath( const QModelIndex &index ) const 0162 { 0163 return m_model->filePath( index ); 0164 } 0165 0166 0167 void 0168 CollectionSetup::importCollection() 0169 { 0170 DatabaseImporterDialog *dlg = new DatabaseImporterDialog( this ); 0171 dlg->exec(); // be modal to avoid messing about by the user in the application 0172 } 0173 0174 void 0175 CollectionSetup::slotPressed( const QModelIndex &index ) 0176 { 0177 DEBUG_BLOCK 0178 0179 // --- show context menu on right mouse button 0180 if( ( QApplication::mouseButtons() & Qt::RightButton ) ) 0181 { 0182 m_currDir = modelFilePath( index ); 0183 debug() << "Setting current dir to " << m_currDir; 0184 0185 // check if there is an sql collection covering the directory 0186 // it's covered, so we can show the rescan option 0187 if( isDirInCollection( m_currDir ) ) 0188 { 0189 m_rescanDirAction->setText( i18n( "Rescan '%1'", m_currDir ) ); 0190 QMenu menu; 0191 menu.addAction( m_rescanDirAction ); 0192 menu.exec( QCursor::pos() ); 0193 } 0194 } 0195 } 0196 0197 void 0198 CollectionSetup::slotRescanDirTriggered() 0199 { 0200 DEBUG_BLOCK 0201 CollectionManager::instance()->startIncrementalScan( m_currDir ); 0202 } 0203 0204 0205 bool 0206 CollectionSetup::isDirInCollection( const QString& path ) const 0207 { 0208 DEBUG_BLOCK 0209 0210 Collections::Collection *primaryCollection = CollectionManager::instance()->primaryCollection(); 0211 QStringList collectionFolders = primaryCollection ? primaryCollection->property( "collectionFolders" ).toStringList() : QStringList(); 0212 0213 foreach( const QString &dir, collectionFolders ) 0214 { 0215 debug() << "Collection Location: " << dir; 0216 debug() << "path: " << path; 0217 debug() << "scan Recursively: " << AmarokConfig::scanRecursively(); 0218 QUrl parentUrl = QUrl::fromLocalFile( dir ); 0219 if ( !AmarokConfig::scanRecursively() ) 0220 { 0221 if ( ( dir == path ) || ( QString( dir + '/' ) == path ) ) 0222 return true; 0223 } 0224 else //scan recursively 0225 { 0226 if (parentUrl.isParentOf( QUrl::fromLocalFile(path) ) || parentUrl.matches(QUrl::fromLocalFile(path), QUrl::StripTrailingSlash)) 0227 return true; 0228 } 0229 } 0230 return false; 0231 } 0232 0233 ////////////////////////////////////////////////////////////////////////////////////////// 0234 // CLASS Model 0235 ////////////////////////////////////////////////////////////////////////////////////////// 0236 0237 namespace CollectionFolder { 0238 0239 Model::Model( QObject *parent ) 0240 : QFileSystemModel( parent ) 0241 { 0242 setFilter( QDir::AllDirs | QDir::NoDotAndDotDot ); 0243 } 0244 0245 Qt::ItemFlags 0246 Model::flags( const QModelIndex &index ) const 0247 { 0248 Qt::ItemFlags flags = QFileSystemModel::flags( index ); 0249 const QString path = filePath( index ); 0250 if( isForbiddenPath( path ) ) 0251 flags ^= Qt::ItemIsEnabled; //disabled! 0252 0253 flags |= Qt::ItemIsUserCheckable; 0254 0255 return flags; 0256 } 0257 0258 QVariant 0259 Model::data( const QModelIndex& index, int role ) const 0260 { 0261 if( index.isValid() && index.column() == 0 && role == Qt::CheckStateRole ) 0262 { 0263 const QString path = filePath( index ); 0264 if( recursive() && ancestorChecked( path ) ) 0265 return Qt::Checked; // always set children of recursively checked parents to checked 0266 if( isForbiddenPath( path ) ) 0267 return Qt::Unchecked; // forbidden paths can never be checked 0268 if( !m_checked.contains( path ) && descendantChecked( path ) ) 0269 return Qt::PartiallyChecked; 0270 return m_checked.contains( path ) ? Qt::Checked : Qt::Unchecked; 0271 } 0272 return QFileSystemModel::data( index, role ); 0273 } 0274 0275 bool 0276 Model::setData( const QModelIndex& index, const QVariant& value, int role ) 0277 { 0278 if( index.isValid() && index.column() == 0 && role == Qt::CheckStateRole ) 0279 { 0280 const QString path = filePath( index ); 0281 if( value.toInt() == Qt::Checked ) 0282 { 0283 // New path selected 0284 if( recursive() ) 0285 { 0286 // Recursive, so clear any paths in m_checked that are made 0287 // redundant by this new selection 0288 QString _path = normalPath( path ); 0289 foreach( const QString &elem, m_checked ) 0290 { 0291 if( normalPath( elem ).startsWith( _path ) ) 0292 m_checked.remove( elem ); 0293 } 0294 } 0295 m_checked << path; 0296 } 0297 else 0298 { 0299 // Path un-selected 0300 m_checked.remove( path ); 0301 if( recursive() && ancestorChecked( path ) ) 0302 { 0303 // Recursive, so we need to deal with the case of un-selecting 0304 // an implicitly selected path 0305 const QStringList ancestors = allCheckedAncestors( path ); 0306 QString topAncestor; 0307 // Remove all selected ancestor of path, and find shallowest 0308 // ancestor 0309 for ( const QString &elem : ancestors ) 0310 { 0311 m_checked.remove( elem ); 0312 if( elem < topAncestor || topAncestor.isEmpty() ) 0313 topAncestor = elem; 0314 } 0315 // Check all paths reachable from topAncestor, except for 0316 // those that are ancestors of path 0317 checkRecursiveSubfolders( topAncestor, path ); 0318 } 0319 } 0320 // A check or un-check can possibly require the whole view to change, 0321 // so we signal that the root's data is changed 0322 Q_EMIT dataChanged( QModelIndex(), QModelIndex() ); 0323 return true; 0324 } 0325 return QFileSystemModel::setData( index, value, role ); 0326 } 0327 0328 void 0329 Model::setDirectories( const QStringList &dirs ) 0330 { 0331 m_checked.clear(); 0332 foreach( const QString &dir, dirs ) 0333 { 0334 m_checked.insert( dir ); 0335 } 0336 } 0337 0338 QStringList 0339 Model::directories() const 0340 { 0341 QStringList dirs = m_checked.values(); 0342 0343 std::sort( dirs.begin(), dirs.end() ); 0344 0345 // we need to remove any children of selected items as 0346 // they are redundant when recursive mode is chosen 0347 if( recursive() ) 0348 { 0349 foreach( const QString &dir, dirs ) 0350 { 0351 if( ancestorChecked( dir ) ) 0352 dirs.removeAll( dir ); 0353 } 0354 } 0355 0356 return dirs; 0357 } 0358 0359 inline bool 0360 Model::isForbiddenPath( const QString &path ) const 0361 { 0362 // we need the trailing slash otherwise we could forbid "/dev-music" for example 0363 QString _path = normalPath( path ); 0364 return _path.startsWith( "/proc/" ) || _path.startsWith( "/dev/" ) || _path.startsWith( "/sys/" ); 0365 } 0366 0367 bool 0368 Model::ancestorChecked( const QString &path ) const 0369 { 0370 // we need the trailing slash otherwise sibling folders with one as the prefix of the other are seen as parent/child 0371 const QString _path = normalPath( path ); 0372 0373 foreach( const QString &element, m_checked ) 0374 { 0375 const QString _element = normalPath( element ); 0376 if( _path.startsWith( _element ) && _element != _path ) 0377 return true; 0378 } 0379 return false; 0380 } 0381 0382 /** 0383 * Get a list of all checked paths that are an ancestor of 0384 * the given path. 0385 */ 0386 QStringList 0387 Model::allCheckedAncestors( const QString &path ) const 0388 { 0389 const QString _path = normalPath( path ); 0390 QStringList rtn; 0391 foreach( const QString &element, m_checked ) 0392 { 0393 const QString _element = normalPath( element ); 0394 if ( _path.startsWith( _element ) && _element != _path ) 0395 rtn << element; 0396 } 0397 return rtn; 0398 } 0399 0400 bool 0401 Model::descendantChecked( const QString &path ) const 0402 { 0403 // we need the trailing slash otherwise sibling folders with one as the prefix of the other are seen as parent/child 0404 const QString _path = normalPath( path ); 0405 0406 foreach( const QString& element, m_checked ) 0407 { 0408 const QString _element = normalPath( element ); 0409 if( _element.startsWith( _path ) && _element != _path ) 0410 return true; 0411 } 0412 return false; 0413 } 0414 0415 void 0416 Model::checkRecursiveSubfolders( const QString &root, const QString &excludePath ) 0417 { 0418 QString _root = normalPath( root ); 0419 QString _excludePath = normalPath( excludePath ); 0420 if( _root == _excludePath ) 0421 return; 0422 QDirIterator it( _root ); 0423 while( it.hasNext() ) 0424 { 0425 QString nextPath = it.next(); 0426 if( nextPath.endsWith( "/." ) || nextPath.endsWith( "/.." ) ) 0427 continue; 0428 if( !_excludePath.startsWith( nextPath ) ) 0429 m_checked << nextPath; 0430 else 0431 checkRecursiveSubfolders( nextPath, excludePath ); 0432 } 0433 } 0434 0435 } //namespace Collection