File indexing completed on 2024-05-19 04:48:38

0001 /****************************************************************************************
0002  * Copyright (c) 2010 Nikolaj Hald Nielsen <nhn@kde.org>                                *
0003  * Copyright (c) 2010 Casey Link <unnamedrambler@gmail.com>                             *
0004  * Copyright (c) 2010 Téo Mrnjavac <teo@kde.org>                                        *
0005  *                                                                                      *
0006  * This program is free software; you can redistribute it and/or modify it under        *
0007  * the terms of the GNU General Public License as published by the Free Software        *
0008  * Foundation; either version 2 of the License, or (at your option) any later           *
0009  * version.                                                                             *
0010  *                                                                                      *
0011  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0012  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0013  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0014  *                                                                                      *
0015  * You should have received a copy of the GNU General Public License along with         *
0016  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0017  ****************************************************************************************/
0018 
0019 #define DEBUG_PREFIX "FileBrowser"
0020 
0021 #include "FileBrowser_p.h"
0022 #include "FileBrowser.h"
0023 
0024 #include "amarokconfig.h"
0025 #include "EngineController.h"
0026 #include "core/support/Amarok.h"
0027 #include "core/support/Components.h"
0028 #include "core/support/Debug.h"
0029 #include "core-impl/meta/file/File.h"
0030 #include "browsers/BrowserBreadcrumbItem.h"
0031 #include "browsers/BrowserCategoryList.h"
0032 #include "browsers/filebrowser/DirPlaylistTrackFilterProxyModel.h"
0033 #include "browsers/filebrowser/FileView.h"
0034 #include "playlist/PlaylistController.h"
0035 #include "widgets/SearchWidget.h"
0036 
0037 #include <QAction>
0038 #include <QComboBox>
0039 #include <QHeaderView>
0040 #include <QHBoxLayout>
0041 #include <QStandardPaths>
0042 
0043 #include <KConfigGroup>
0044 #include <KDirLister>
0045 #include <KLocalizedString>
0046 #include <KIO/StatJob>
0047 #include <KStandardAction>
0048 #include <KToolBar>
0049 
0050 static const QString placesString( "places://" );
0051 static const QUrl placesUrl( placesString );
0052 
0053 FileBrowser::Private::Private( FileBrowser *parent )
0054     : placesModel( nullptr )
0055     , q( parent )
0056 {
0057     BoxWidget *topHBox = new BoxWidget( q );
0058 
0059     KToolBar *navigationToolbar = new KToolBar( topHBox );
0060     navigationToolbar->setToolButtonStyle( Qt::ToolButtonIconOnly );
0061     navigationToolbar->setIconDimensions( 16 );
0062 
0063     backAction = KStandardAction::back( q, &FileBrowser::back, topHBox );
0064     forwardAction = KStandardAction::forward( q, &FileBrowser::forward, topHBox );
0065     backAction->setEnabled( false );
0066     forwardAction->setEnabled( false );
0067 
0068     upAction = KStandardAction::up( q, &FileBrowser::up, topHBox );
0069     homeAction = KStandardAction::home( q, &FileBrowser::home, topHBox );
0070     refreshAction = new QAction( QIcon::fromTheme(QStringLiteral("view-refresh")), i18n( "Refresh" ), topHBox );
0071     QObject::connect( refreshAction, &QAction::triggered, q, &FileBrowser::refresh );
0072 
0073     navigationToolbar->addAction( backAction );
0074     navigationToolbar->addAction( forwardAction );
0075     navigationToolbar->addAction( upAction );
0076     navigationToolbar->addAction( homeAction );
0077     navigationToolbar->addAction( refreshAction );
0078 
0079     searchWidget = new SearchWidget( topHBox, false );
0080     searchWidget->setClickMessage( i18n( "Filter Files" ) );
0081 
0082     fileView = new FileView( q );
0083 }
0084 
0085 FileBrowser::Private::~Private()
0086 {
0087     writeConfig();
0088 }
0089 
0090 void
0091 FileBrowser::Private::readConfig()
0092 {
0093     const QUrl homeUrl = QUrl::fromLocalFile( QDir::homePath() );
0094     const QUrl savedUrl = Amarok::config( "File Browser" ).readEntry( "Current Directory", homeUrl );
0095     bool useHome( true );
0096     // fall back to $HOME if the saved dir has since disappeared or is a remote one
0097     if( savedUrl.isLocalFile() )
0098     {
0099         QDir dir( savedUrl.path() );
0100         if( dir.exists() )
0101             useHome = false;
0102     }
0103     else
0104     {
0105         KIO::StatJob *statJob = KIO::statDetails( savedUrl, KIO::StatJob::DestinationSide);
0106         statJob->exec();
0107         if( statJob->statResult().isDir() )
0108         {
0109             useHome = false;
0110         }
0111     }
0112     currentPath = useHome ? homeUrl : savedUrl;
0113 }
0114 
0115 void
0116 FileBrowser::Private::writeConfig()
0117 {
0118     Amarok::config( "File Browser" ).writeEntry( "Current Directory", kdirModel->dirLister()->url() );
0119 }
0120 
0121 BreadcrumbSiblingList
0122 FileBrowser::Private::siblingsForDir( const QUrl &path )
0123 {
0124     BreadcrumbSiblingList siblings;
0125     if( path.scheme() == "places" )
0126     {
0127         for( int i = 0; i < placesModel->rowCount(); i++ )
0128         {
0129             QModelIndex idx = placesModel->index( i, 0 );
0130 
0131             QString name = idx.data( Qt::DisplayRole ).toString();
0132             QString url = idx.data( KFilePlacesModel::UrlRole ).toString();
0133             if( url.isEmpty() )
0134                 // the place perhaps needs mounting, use places url instead
0135                 url = placesString + name;
0136             siblings << BreadcrumbSibling( idx.data( Qt::DecorationRole ).value<QIcon>(),
0137                                            name, url );
0138         }
0139     }
0140     else if( path.isLocalFile() )
0141     {
0142         QDir dir( path.toLocalFile() );
0143         dir.cdUp();
0144         foreach( const QString &item, dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
0145         {
0146             siblings << BreadcrumbSibling( QIcon::fromTheme( "folder-amarok" ), item,
0147                                            dir.absoluteFilePath( item ) );
0148         }
0149     }
0150 
0151     return siblings;
0152 }
0153 
0154 void
0155 FileBrowser::Private::updateNavigateActions()
0156 {
0157     backAction->setEnabled( !backStack.isEmpty() );
0158     forwardAction->setEnabled( !forwardStack.isEmpty() );
0159     upAction->setEnabled( currentPath != placesUrl );
0160 }
0161 
0162 void
0163 FileBrowser::Private::restoreDefaultHeaderState()
0164 {
0165     fileView->hideColumn( 3 );
0166     fileView->hideColumn( 4 );
0167     fileView->hideColumn( 5 );
0168     fileView->hideColumn( 6 );
0169     fileView->sortByColumn( 0, Qt::AscendingOrder );
0170 }
0171 
0172 void
0173 FileBrowser::Private::restoreHeaderState()
0174 {
0175     QFile file( Amarok::saveLocation() + "file_browser_layout" );
0176     if( !file.open( QIODevice::ReadOnly ) )
0177     {
0178         restoreDefaultHeaderState();
0179         return;
0180     }
0181     if( !fileView->header()->restoreState( file.readAll() ) )
0182     {
0183         warning() << "invalid header state saved, unable to restore. Restoring defaults";
0184         restoreDefaultHeaderState();
0185         return;
0186     }
0187 }
0188 
0189 void
0190 FileBrowser::Private::saveHeaderState()
0191 {
0192     //save the state of the header (column size and order). Yay, another QByteArray thingie...
0193     QFile file( Amarok::saveLocation() + "file_browser_layout" );
0194     if( !file.open( QIODevice::WriteOnly ) )
0195     {
0196         warning() << "unable to save header state";
0197         return;
0198     }
0199     if( file.write( fileView->header()->saveState() ) < 0 )
0200     {
0201         warning() << "unable to save header state, writing failed";
0202         return;
0203     }
0204 }
0205 
0206 void
0207 FileBrowser::Private::updateHeaderState()
0208 {
0209     // this slot is triggered right after model change, when currentPath is not yet updated
0210     if( fileView->model() == mimeFilterProxyModel && currentPath == placesUrl )
0211         // we are transitioning from places to files
0212         restoreHeaderState();
0213 }
0214 
0215 FileBrowser::FileBrowser( const char *name, QWidget *parent )
0216     : BrowserCategory( name, parent )
0217     , d( new FileBrowser::Private( this ) )
0218 {
0219     setLongDescription( i18n( "The file browser lets you browse files anywhere on your system, "
0220                         "regardless of whether these files are part of your local collection. "
0221                         "You can then add these files to the playlist as well as perform basic "
0222                         "file operations." )
0223                        );
0224 
0225     setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/hover_info_files.png" ) );
0226 
0227     // set background
0228     if( AmarokConfig::showBrowserBackgroundImage() )
0229         setBackgroundImage( imagePath() );
0230 
0231     initView();
0232 }
0233 
0234 void
0235 FileBrowser::initView()
0236 {
0237     d->bottomPlacesModel = new FilePlacesModel( this );
0238     connect( d->bottomPlacesModel, &KFilePlacesModel::setupDone,
0239              this, &FileBrowser::setupDone );
0240     d->placesModel = new QSortFilterProxyModel( this );
0241     d->placesModel->setSourceModel( d->bottomPlacesModel );
0242     d->placesModel->setSortRole( -1 );
0243     d->placesModel->setDynamicSortFilter( true );
0244     d->placesModel->setFilterRole( KFilePlacesModel::HiddenRole );
0245     // HiddenRole is bool, but QVariant( false ).toString() gives "false"
0246     d->placesModel->setFilterFixedString( "false" );
0247     d->placesModel->setObjectName( "PLACESMODEL");
0248 
0249     d->kdirModel = new DirBrowserModel( this );
0250     d->mimeFilterProxyModel = new DirPlaylistTrackFilterProxyModel( this );
0251     d->mimeFilterProxyModel->setSourceModel( d->kdirModel );
0252     d->mimeFilterProxyModel->setSortCaseSensitivity( Qt::CaseInsensitive );
0253     d->mimeFilterProxyModel->setFilterCaseSensitivity( Qt::CaseInsensitive );
0254     d->mimeFilterProxyModel->setDynamicSortFilter( true );
0255     connect( d->searchWidget, &SearchWidget::filterChanged,
0256              d->mimeFilterProxyModel, &DirPlaylistTrackFilterProxyModel::setFilterFixedString );
0257 
0258     d->fileView->setModel( d->mimeFilterProxyModel );
0259     d->fileView->header()->setContextMenuPolicy( Qt::ActionsContextMenu );
0260     d->fileView->header()->setVisible( true );
0261     d->fileView->setDragEnabled( true );
0262     d->fileView->setSortingEnabled( true );
0263     d->fileView->setSelectionMode( QAbstractItemView::ExtendedSelection );
0264     d->readConfig();
0265     d->restoreHeaderState();
0266 
0267     setDir( d->currentPath );
0268 
0269     for( int i = 0, columns = d->fileView->model()->columnCount(); i < columns ; ++i )
0270     {
0271         QAction *action =
0272                 new QAction( d->fileView->model()->headerData( i, Qt::Horizontal ).toString(),
0273                              d->fileView->header()
0274                            );
0275         d->fileView->header()->addAction( action );
0276         d->columnActions.append( action );
0277         action->setCheckable( true );
0278         if( !d->fileView->isColumnHidden( i ) )
0279             action->setChecked( true );
0280         connect( action, &QAction::toggled, this, &FileBrowser::toggleColumn );
0281     }
0282 
0283     connect( d->fileView->header(), &QHeaderView::geometriesChanged,
0284              this, &FileBrowser::updateHeaderState );
0285     connect( d->fileView, &FileView::navigateToDirectory,
0286              this, &FileBrowser::slotNavigateToDirectory );
0287     connect( d->fileView, &FileView::refreshBrowser,
0288              this, &FileBrowser::refresh );
0289 }
0290 
0291 void
0292 FileBrowser::updateHeaderState()
0293 {
0294     d->updateHeaderState();
0295 }
0296 
0297 
0298 FileBrowser::~FileBrowser()
0299 {
0300     if( d->fileView->model() == d->mimeFilterProxyModel && d->currentPath != placesUrl )
0301         d->saveHeaderState();
0302     delete d;
0303 }
0304 
0305 void
0306 FileBrowser::toggleColumn( bool toggled )
0307 {
0308     int index = d->columnActions.indexOf( qobject_cast< QAction* >( sender() ) );
0309     if( index != -1 )
0310     {
0311         if( toggled )
0312             d->fileView->showColumn( index );
0313         else
0314             d->fileView->hideColumn( index );
0315     }
0316 }
0317 
0318 QString
0319 FileBrowser::currentDir() const
0320 {
0321     if( d->currentPath.isLocalFile() )
0322         return d->currentPath.toLocalFile();
0323     else
0324         return d->currentPath.url();
0325 }
0326 
0327 void
0328 FileBrowser::slotNavigateToDirectory( const QModelIndex &index )
0329 {
0330     if( d->currentPath == placesUrl )
0331     {
0332         QString url = index.data( KFilePlacesModel::UrlRole ).value<QString>();
0333 
0334         if( !url.isEmpty() )
0335         {
0336             d->backStack.push( d->currentPath );
0337             d->forwardStack.clear(); // navigating resets forward stack
0338             setDir( QUrl( url ) );
0339         }
0340         else
0341         {
0342             //check if this url needs setup/mounting
0343             if( index.data( KFilePlacesModel::SetupNeededRole ).value<bool>() )
0344             {
0345                 d->bottomPlacesModel->requestSetup( d->placesModel->mapToSource( index ) );
0346             }
0347             else
0348                 warning() << __PRETTY_FUNCTION__ << "empty places url that doesn't need setup?";
0349         }
0350     }
0351     else
0352     {
0353         KFileItem file = index.data( KDirModel::FileItemRole ).value<KFileItem>();
0354 
0355         if( file.isDir() )
0356         {
0357             d->backStack.push( d->currentPath );
0358             d->forwardStack.clear(); // navigating resets forward stack
0359             setDir( file.url() );
0360         }
0361         else
0362             warning() << __PRETTY_FUNCTION__ << "called for non-directory";
0363     }
0364 }
0365 
0366 
0367 void
0368 FileBrowser::addItemActivated( const QString &callbackString )
0369 {
0370     if( callbackString.isEmpty() )
0371         return;
0372 
0373     QUrl newPath;
0374     // we have been called with a places name, it means that we'll probably have to mount
0375     // the place
0376     if( callbackString.startsWith( placesString ) )
0377     {
0378         QString name = callbackString.mid( placesString.length() );
0379         for( int i = 0; i < d->placesModel->rowCount(); i++ )
0380         {
0381             QModelIndex idx = d->placesModel->index( i, 0 );
0382             if( idx.data().toString() == name )
0383             {
0384                 if( idx.data( KFilePlacesModel::SetupNeededRole ).toBool() )
0385                 {
0386                     d->bottomPlacesModel->requestSetup( d->placesModel->mapToSource( idx ) );
0387                     return;
0388                 }
0389                 newPath = QUrl::fromUserInput(idx.data( KFilePlacesModel::UrlRole ).toString());
0390                 break;
0391             }
0392         }
0393         if( newPath.isEmpty() )
0394         {
0395             warning() << __PRETTY_FUNCTION__ << "name" << name << "not found under Places";
0396             return;
0397         }
0398     }
0399     else
0400         newPath = QUrl::fromUserInput(callbackString);
0401 
0402     d->backStack.push( d->currentPath );
0403     d->forwardStack.clear(); // navigating resets forward stack
0404     setDir( QUrl( newPath ) );
0405 }
0406 
0407 void
0408 FileBrowser::setupAddItems()
0409 {
0410     clearAdditionalItems();
0411 
0412     if( d->currentPath == placesUrl )
0413         return; // no more items to add
0414 
0415     QString workingUrl = d->currentPath.toDisplayString( QUrl::StripTrailingSlash );
0416     int currentPosition = 0;
0417 
0418     QString name;
0419     QString callback;
0420     BreadcrumbSiblingList siblings;
0421 
0422     // find QModelIndex of the NON-HIDDEN closestItem
0423     QModelIndex placesIndex;
0424     QUrl tempUrl = d->currentPath;
0425     do
0426     {
0427         placesIndex = d->bottomPlacesModel->closestItem( tempUrl );
0428         if( !placesIndex.isValid() )
0429             break; // no valid index even in the bottom model
0430         placesIndex = d->placesModel->mapFromSource( placesIndex );
0431         if( placesIndex.isValid() )
0432             break; // found shown placesindex, good!
0433 
0434         if( KIO::upUrl(tempUrl) == tempUrl )
0435             break; // prevent infinite loop
0436         tempUrl = KIO::upUrl(tempUrl);
0437     } while( true );
0438 
0439     // special handling for the first additional item
0440     if( placesIndex.isValid() )
0441     {
0442         name = placesIndex.data( Qt::DisplayRole ).toString();
0443         callback = placesIndex.data( KFilePlacesModel::UrlRole ).toString();
0444 
0445         QUrl currPlaceUrl = d->placesModel->data( placesIndex, KFilePlacesModel::UrlRole ).toUrl();
0446         currPlaceUrl.setPath( QDir::toNativeSeparators(currPlaceUrl.path() + '/') );
0447         currentPosition = currPlaceUrl.toString().length();
0448     }
0449     else
0450     {
0451         QRegExp threeSlashes( "^[^/]*/[^/]*/[^/]*/" );
0452         if( workingUrl.indexOf( threeSlashes ) == 0 )
0453             currentPosition = threeSlashes.matchedLength();
0454         else
0455             currentPosition = workingUrl.length();
0456 
0457         callback = workingUrl.left( currentPosition );
0458         name = callback;
0459         if( name == "file:///" )
0460             name = '/'; // just niceness
0461         else
0462             name.remove( QRegExp( "/$" ) );
0463     }
0464     /* always provide siblings for places, regardless of what first item is; this also
0465      * work-arounds bug 312639, where creating QUrl with accented chars crashes */
0466     siblings = d->siblingsForDir( placesUrl );
0467     addAdditionalItem( new BrowserBreadcrumbItem( name, callback, siblings, this ) );
0468 
0469     // other additional items
0470     while( !workingUrl.midRef( currentPosition ).isEmpty() )
0471     {
0472         int nextPosition = workingUrl.indexOf( QLatin1Char('/'), currentPosition ) + 1;
0473         if( nextPosition <= 0 )
0474             nextPosition = workingUrl.length();
0475 
0476         name = workingUrl.mid( currentPosition, nextPosition - currentPosition );
0477         name.remove( QRegExp( "/$" ) );
0478         callback = workingUrl.left( nextPosition );
0479 
0480         siblings = d->siblingsForDir( QUrl::fromLocalFile( callback ) );
0481         addAdditionalItem( new BrowserBreadcrumbItem( name, callback, siblings, this ) );
0482 
0483         currentPosition = nextPosition;
0484     }
0485 
0486     if( parentList() )
0487         parentList()->childViewChanged(); // emits viewChanged() which causes breadCrumb update
0488 }
0489 
0490 void
0491 FileBrowser::reActivate()
0492 {
0493     d->backStack.push( d->currentPath );
0494     d->forwardStack.clear(); // navigating resets forward stack
0495     setDir( placesUrl );
0496 }
0497 
0498 void
0499 FileBrowser::setDir( const QUrl &dir )
0500 {
0501     if( dir == placesUrl )
0502     {
0503         if( d->currentPath != placesUrl )
0504         {
0505             d->saveHeaderState();
0506             d->fileView->setModel( d->placesModel );
0507             d->fileView->setSelectionMode( QAbstractItemView::SingleSelection );
0508             d->fileView->header()->setVisible( false );
0509             d->fileView->setDragEnabled( false );
0510         }
0511     }
0512     else
0513     {
0514         // if we are currently showing "places" we need to remember to change the model
0515         // back to the regular file model
0516         if( d->currentPath == placesUrl )
0517         {
0518             d->fileView->setModel( d->mimeFilterProxyModel );
0519             d->fileView->setSelectionMode( QAbstractItemView::ExtendedSelection );
0520             d->fileView->setDragEnabled( true );
0521             d->fileView->header()->setVisible( true );
0522         }
0523         d->kdirModel->dirLister()->openUrl( dir );
0524     }
0525 
0526     d->currentPath = dir;
0527     d->updateNavigateActions();
0528     setupAddItems();
0529     // set the first item as current so that keyboard navigation works
0530     new DelayedActivator( d->fileView );
0531 }
0532 
0533 void
0534 FileBrowser::back()
0535 {
0536     if( d->backStack.isEmpty() )
0537         return;
0538 
0539     d->forwardStack.push( d->currentPath );
0540     setDir( d->backStack.pop() );
0541 }
0542 
0543 void
0544 FileBrowser::forward()
0545 {
0546     if( d->forwardStack.isEmpty() )
0547         return;
0548 
0549     d->backStack.push( d->currentPath );
0550     // no clearing forward stack here!
0551     setDir( d->forwardStack.pop() );
0552 }
0553 
0554 void
0555 FileBrowser::up()
0556 {
0557     if( d->currentPath == placesUrl )
0558         return; // nothing to do, we consider places as the root view
0559 
0560     QUrl upUrl = KIO::upUrl(d->currentPath);
0561     if( upUrl == d->currentPath ) // apparently, we cannot go up withn url
0562         upUrl = placesUrl;
0563 
0564     d->backStack.push( d->currentPath );
0565     d->forwardStack.clear(); // navigating resets forward stack
0566     setDir( upUrl );
0567 }
0568 
0569 void
0570 FileBrowser::home()
0571 {
0572     d->backStack.push( d->currentPath );
0573     d->forwardStack.clear(); // navigating resets forward stack
0574     setDir( QUrl::fromLocalFile( QDir::homePath() ) );
0575 }
0576 
0577 void
0578 FileBrowser::refresh()
0579 {
0580     setDir( d->currentPath );
0581 }
0582 
0583 void
0584 FileBrowser::setupDone( const QModelIndex &index, bool success )
0585 {
0586     if( success )
0587     {
0588         QString url = index.data( KFilePlacesModel::UrlRole  ).value<QString>();
0589         if( !url.isEmpty() )
0590         {
0591             d->backStack.push( d->currentPath );
0592             d->forwardStack.clear(); // navigating resets forward stack
0593             setDir( QUrl::fromLocalFile(url) );
0594         }
0595     }
0596 }
0597 
0598 DelayedActivator::DelayedActivator( QAbstractItemView *view )
0599     : QObject( view )
0600     , m_view( view )
0601 {
0602     QAbstractItemModel *model = view->model();
0603     if( !model )
0604     {
0605         deleteLater();
0606         return;
0607     }
0608 
0609     // short-cut for already-filled models
0610     if( model->rowCount() > 0 )
0611     {
0612         slotRowsInserted( QModelIndex(), 0 );
0613         return;
0614     }
0615 
0616     connect( model, &QAbstractItemModel::rowsInserted, this, &DelayedActivator::slotRowsInserted );
0617 
0618     connect( model, &QAbstractItemModel::destroyed, this, &DelayedActivator::deleteLater );
0619     connect( model, &QAbstractItemModel::layoutChanged, this, &DelayedActivator::deleteLater );
0620     connect( model, &QAbstractItemModel::modelReset, this, &DelayedActivator::deleteLater );
0621 }
0622 
0623 void
0624 DelayedActivator::slotRowsInserted( const QModelIndex &parent, int start )
0625 {
0626     QAbstractItemModel *model = m_view->model();
0627     if( model )
0628     {
0629         // prevent duplicate calls, deleteLater() may fire REAL later
0630         disconnect( model, nullptr, this, nullptr );
0631         QModelIndex idx = model->index( start, 0, parent );
0632         m_view->selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate );
0633     }
0634     deleteLater();
0635 }
0636 
0637 #include "moc_FileBrowser.cpp"