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