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"