File indexing completed on 2024-05-05 04:48:25
0001 /**************************************************************************************** 0002 * Copyright (c) 2008 Bonne Eggleston <b.eggleston@gmail.com> * 0003 * Copyright (c) 2008 Téo Mrnjavac <teo@kde.org> * 0004 * Copyright (c) 2010 Casey Link <unnamedrambler@gmail.com> * 0005 * Copyright (c) 2012 Ralf Engels <ralf-engels@gmx.de> * 0006 * * 0007 * This program is free software; you can redistribute it and/or modify it under * 0008 * the terms of the GNU General Public License as published by the Free Software * 0009 * Foundation; either version 2 of the License, or (at your option) any later * 0010 * version. * 0011 * * 0012 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0013 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0014 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0015 * * 0016 * You should have received a copy of the GNU General Public License along with * 0017 * this program. If not, see <http://www.gnu.org/licenses/>. * 0018 ****************************************************************************************/ 0019 0020 #define DEBUG_PREFIX "OrganizeCollectionDialog" 0021 0022 #include "OrganizeCollectionDialog.h" 0023 0024 #include "amarokconfig.h" 0025 #include "core/support/Amarok.h" 0026 #include "core/support/Debug.h" 0027 #include "core-impl/meta/file/File.h" 0028 #include "dialogs/TrackOrganizer.h" 0029 #include "widgets/TokenPool.h" 0030 #include "ui_OrganizeCollectionDialogBase.h" 0031 0032 #include <QApplication> 0033 #include <QDesktopWidget> 0034 #include <QDir> 0035 #include <QPushButton> 0036 #include <QTimer> 0037 0038 #include <KColorScheme> 0039 #include <KWindowConfig> 0040 0041 // -------------- OrganizeCollectionOptionWidget ------------ 0042 OrganizeCollectionOptionWidget::OrganizeCollectionOptionWidget( QWidget *parent ) 0043 : QGroupBox( parent ) 0044 { 0045 setupUi( this ); 0046 0047 connect( spaceCheck, &QCheckBox::toggled, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0048 connect( ignoreTheCheck, &QCheckBox::toggled, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0049 connect( vfatCheck, &QCheckBox::toggled, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0050 connect( asciiCheck, &QCheckBox::toggled, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0051 connect( regexpEdit, &QLineEdit::editingFinished, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0052 connect( replaceEdit, &QLineEdit::editingFinished, this, &OrganizeCollectionOptionWidget::optionsChanged ); 0053 } 0054 0055 // ------------------------- OrganizeCollectionWidget ------------------- 0056 0057 OrganizeCollectionWidget::OrganizeCollectionWidget( QWidget *parent ) 0058 : FilenameLayoutWidget( parent ) 0059 { 0060 m_configCategory = "OrganizeCollectionDialog"; 0061 0062 // TODO: also supported by TrackOrganizer: 0063 // folder theartist thealbumartist rating filesize length 0064 m_tokenPool->addToken( createToken( Title ) ); 0065 m_tokenPool->addToken( createToken( Artist ) ); 0066 m_tokenPool->addToken( createToken( AlbumArtist ) ); 0067 m_tokenPool->addToken( createToken( Album ) ); 0068 m_tokenPool->addToken( createToken( Genre ) ); 0069 m_tokenPool->addToken( createToken( Composer ) ); 0070 m_tokenPool->addToken( createToken( Comment ) ); 0071 m_tokenPool->addToken( createToken( Year ) ); 0072 m_tokenPool->addToken( createToken( TrackNumber ) ); 0073 m_tokenPool->addToken( createToken( DiscNumber ) ); 0074 0075 m_tokenPool->addToken( createToken( Folder ) ); 0076 m_tokenPool->addToken( createToken( FileType ) ); 0077 m_tokenPool->addToken( createToken( Initial ) ); 0078 0079 m_tokenPool->addToken( createToken( Slash ) ); 0080 m_tokenPool->addToken( createToken( Underscore ) ); 0081 m_tokenPool->addToken( createToken( Dash ) ); 0082 m_tokenPool->addToken( createToken( Dot ) ); 0083 m_tokenPool->addToken( createToken( Space ) ); 0084 0085 // show some non-editable tags before and after 0086 // but only if screen size is large enough (BR: 283361) 0087 const QRect screenRect = QApplication::desktop()->screenGeometry(); 0088 if( screenRect.width() >= 1024 ) 0089 { 0090 m_schemaLineLayout->insertWidget( 0, 0091 createStaticToken( CollectionRoot ), 0 ); 0092 m_schemaLineLayout->insertWidget( 1, 0093 createStaticToken( Slash ), 0 ); 0094 0095 m_schemaLineLayout->insertWidget( m_schemaLineLayout->count(), 0096 createStaticToken( Dot ) ); 0097 m_schemaLineLayout->insertWidget( m_schemaLineLayout->count(), 0098 createStaticToken( FileType ) ); 0099 } 0100 0101 m_syntaxLabel->setText( buildFormatTip() ); 0102 0103 populateConfiguration(); 0104 } 0105 0106 0107 QString 0108 OrganizeCollectionWidget::buildFormatTip() const 0109 { 0110 QMap<QString, QString> args; 0111 args["albumartist"] = i18n( "%1 or %2", QLatin1String("Album Artist, The") , QLatin1String("The Album Artist") ); 0112 args["thealbumartist"] = i18n( "The Album Artist" ); 0113 args["theartist"] = i18n( "The Artist" ); 0114 args["artist"] = i18n( "%1 or %2", QLatin1String("Artist, The") , QLatin1String("The Artist") ); 0115 args["initial"] = i18n( "Artist's Initial" ); 0116 args["filetype"] = i18n( "File Extension of Source" ); 0117 args["track"] = i18n( "Track Number" ); 0118 0119 QString tooltip = i18n( "You can use the following tokens:" ); 0120 tooltip += "<ul>"; 0121 0122 for( QMap<QString, QString>::iterator it = args.begin(), total = args.end(); it != total; ++it ) 0123 tooltip += QString( "<li>%1 - %%2%" ).arg( it.value(), it.key() ); 0124 0125 tooltip += "</ul>"; 0126 tooltip += i18n( "If you surround sections of text that contain a token with curly-braces, " 0127 "that section will be hidden if the token is empty." ); 0128 0129 return tooltip; 0130 } 0131 0132 0133 OrganizeCollectionDialog::OrganizeCollectionDialog( const Meta::TrackList &tracks, 0134 const QStringList &folders, 0135 const QString &targetExtension, 0136 QWidget *parent, 0137 const char *name, 0138 bool modal, 0139 const QString &caption, 0140 QFlags<QDialogButtonBox::StandardButton> buttonMask ) 0141 : QDialog( parent ) 0142 , ui( new Ui::OrganizeCollectionDialogBase ) 0143 , m_conflict( false ) 0144 { 0145 Q_UNUSED( name ) 0146 0147 setWindowTitle( caption ); 0148 setModal( modal ); 0149 m_targetFileExtension = targetExtension; 0150 0151 if( tracks.size() > 0 ) 0152 m_allTracks = tracks; 0153 0154 QWidget *mainContainer = new QWidget( this ); 0155 QDialogButtonBox* buttonBox = new QDialogButtonBox( buttonMask, this ); 0156 connect(buttonBox, &QDialogButtonBox::accepted, this, &OrganizeCollectionDialog::accept); 0157 connect(buttonBox, &QDialogButtonBox::rejected, this, &OrganizeCollectionDialog::reject); 0158 0159 QVBoxLayout* mainLayout = new QVBoxLayout(this); 0160 mainLayout->addWidget( mainContainer ); 0161 mainLayout->addWidget( buttonBox ); 0162 0163 ui->setupUi( mainContainer ); 0164 0165 m_trackOrganizer = new TrackOrganizer( m_allTracks, this ); 0166 0167 ui->folderCombo->insertItems( 0, folders ); 0168 if( ui->folderCombo->contains( AmarokConfig::organizeDirectory() ) ) 0169 ui->folderCombo->setCurrentItem( AmarokConfig::organizeDirectory() ); 0170 else 0171 ui->folderCombo->setCurrentIndex( 0 ); //TODO possible bug: assumes folder list is not empty. 0172 0173 ui->overwriteCheck->setChecked( AmarokConfig::overwriteFiles() ); 0174 0175 ui->optionsWidget->setReplaceSpaces( AmarokConfig::replaceSpace() ); 0176 ui->optionsWidget->setPostfixThe( AmarokConfig::ignoreThe() ); 0177 ui->optionsWidget->setVfatCompatible( AmarokConfig::vfatCompatible() ); 0178 ui->optionsWidget->setAsciiOnly( AmarokConfig::asciiOnly() ); 0179 ui->optionsWidget->setRegexpText( AmarokConfig::replacementRegexp() ); 0180 ui->optionsWidget->setReplaceText( AmarokConfig::replacementString() ); 0181 0182 ui->previewTableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::ResizeToContents ); 0183 ui->conflictLabel->setText(""); 0184 QPalette p = ui->conflictLabel->palette(); 0185 KColorScheme::adjustForeground( p, KColorScheme::NegativeText ); // TODO this isn't working, the color is still normal 0186 ui->conflictLabel->setPalette( p ); 0187 ui->previewTableWidget->sortItems( 0, Qt::AscendingOrder ); 0188 0189 // only show the options when the Options button is checked 0190 connect( ui->optionsButton, &QAbstractButton::toggled, ui->organizeCollectionWidget, &OrganizeCollectionWidget::setVisible ); 0191 connect( ui->optionsButton, &QAbstractButton::toggled, ui->optionsWidget, &OrganizeCollectionOptionWidget::setVisible ); 0192 ui->organizeCollectionWidget->hide(); 0193 ui->optionsWidget->hide(); 0194 0195 connect( ui->folderCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), 0196 this, &OrganizeCollectionDialog::slotUpdatePreview ); 0197 connect( ui->organizeCollectionWidget, &OrganizeCollectionWidget::schemeChanged, this, &OrganizeCollectionDialog::slotUpdatePreview ); 0198 connect( ui->optionsWidget, &OrganizeCollectionOptionWidget::optionsChanged, this, &OrganizeCollectionDialog::slotUpdatePreview); 0199 // to show the conflict error 0200 connect( ui->overwriteCheck, &QCheckBox::stateChanged, this, &OrganizeCollectionDialog::slotOverwriteModeChanged ); 0201 0202 connect( this, &OrganizeCollectionDialog::accepted, ui->organizeCollectionWidget, &OrganizeCollectionWidget::onAccept ); 0203 connect( this, &OrganizeCollectionDialog::accepted, this, &OrganizeCollectionDialog::slotDialogAccepted ); 0204 connect( ui->folderCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), 0205 this, &OrganizeCollectionDialog::slotEnableOk ); 0206 0207 slotEnableOk( ui->folderCombo->currentIndex() ); 0208 KWindowConfig::restoreWindowSize( windowHandle(), Amarok::config( "OrganizeCollectionDialog" ) ); 0209 0210 QTimer::singleShot( 0, this, &OrganizeCollectionDialog::slotUpdatePreview ); 0211 } 0212 0213 OrganizeCollectionDialog::~OrganizeCollectionDialog() 0214 { 0215 KConfigGroup group = Amarok::config( "OrganizeCollectionDialog" ); 0216 group.writeEntry( "geometry", saveGeometry() ); 0217 0218 AmarokConfig::setOrganizeDirectory( ui->folderCombo->currentText() ); 0219 delete ui; 0220 } 0221 0222 QMap<Meta::TrackPtr, QString> 0223 OrganizeCollectionDialog::getDestinations() const 0224 { 0225 return m_trackOrganizer->getDestinations(); 0226 } 0227 0228 bool 0229 OrganizeCollectionDialog::overwriteDestinations() const 0230 { 0231 return ui->overwriteCheck->isChecked(); 0232 } 0233 0234 0235 QString 0236 OrganizeCollectionDialog::buildFormatString() const 0237 { 0238 if( ui->organizeCollectionWidget->getParsableScheme().simplified().isEmpty() ) 0239 return ""; 0240 return "%collectionroot%/" + ui->organizeCollectionWidget->getParsableScheme() + ".%filetype%"; 0241 } 0242 0243 void 0244 OrganizeCollectionDialog::slotUpdatePreview() 0245 { 0246 QString formatString = buildFormatString(); 0247 0248 m_trackOrganizer->setAsciiOnly( ui->optionsWidget->asciiOnly() ); 0249 m_trackOrganizer->setFolderPrefix( ui->folderCombo->currentText() ); 0250 m_trackOrganizer->setFormatString( formatString ); 0251 m_trackOrganizer->setTargetFileExtension( m_targetFileExtension ); 0252 m_trackOrganizer->setPostfixThe( ui->optionsWidget->postfixThe() ); 0253 m_trackOrganizer->setReplaceSpaces( ui->optionsWidget->replaceSpaces() ); 0254 m_trackOrganizer->setReplace( ui->optionsWidget->regexpText(), 0255 ui->optionsWidget->replaceText() ); 0256 m_trackOrganizer->setVfatSafe( ui->optionsWidget->vfatCompatible() ); 0257 0258 // empty the table, not only its contents 0259 ui->previewTableWidget->clearContents(); 0260 ui->previewTableWidget->setRowCount( 0 ); 0261 ui->previewTableWidget->setSortingEnabled( false ); // interferes with inserting 0262 m_trackOrganizer->resetTrackOffset(); 0263 m_conflict = false; 0264 setCursor( Qt::BusyCursor ); 0265 0266 // be nice do the UI, try not to block for too long 0267 QTimer::singleShot( 0, this, &OrganizeCollectionDialog::processPreviewPaths ); 0268 } 0269 0270 void 0271 OrganizeCollectionDialog::processPreviewPaths() 0272 { 0273 QStringList originals; 0274 QStringList previews; 0275 QStringList commonOriginalPrefix; // common initial directories 0276 QStringList commonPreviewPrefix; // common initial directories 0277 0278 QMap<Meta::TrackPtr, QString> destinations = m_trackOrganizer->getDestinations(); 0279 for( auto it = destinations.constBegin(); it != destinations.constEnd(); ++it ) 0280 { 0281 originals << it.key()->prettyUrl(); 0282 previews << it.value(); 0283 0284 QStringList originalPrefix = originals.last().split( QLatin1Char('/') ); 0285 originalPrefix.removeLast(); // we never include file name in the common prefix 0286 QStringList previewPrefix = previews.last().split( QLatin1Char('/') ); 0287 previewPrefix.removeLast(); 0288 0289 if( it == destinations.constBegin() ) 0290 { 0291 commonOriginalPrefix = originalPrefix; 0292 commonPreviewPrefix = previewPrefix; 0293 } else { 0294 int commonLength = 0; 0295 while( commonOriginalPrefix.size() > commonLength && 0296 originalPrefix.size() > commonLength && 0297 commonOriginalPrefix[ commonLength ] == originalPrefix[ commonLength ] ) 0298 { 0299 commonLength++; 0300 } 0301 commonOriginalPrefix = commonOriginalPrefix.mid( 0, commonLength ); 0302 0303 commonLength = 0; 0304 while( commonPreviewPrefix.size() > commonLength && 0305 previewPrefix.size() > commonLength && 0306 commonPreviewPrefix[ commonLength ] == previewPrefix[ commonLength ] ) 0307 { 0308 commonLength++; 0309 } 0310 commonPreviewPrefix = commonPreviewPrefix.mid( 0, commonLength ); 0311 } 0312 } 0313 0314 QString originalPrefix = commonOriginalPrefix.isEmpty() ? QString() : commonOriginalPrefix.join( QLatin1Char('/') ) + '/'; 0315 m_previewPrefix = commonPreviewPrefix.isEmpty() ? QString() : commonPreviewPrefix.join( QLatin1Char('/') ) + '/'; 0316 ui->previewTableWidget->horizontalHeaderItem( 1 )->setText( i18n( "Original: %1", originalPrefix ) ); 0317 ui->previewTableWidget->horizontalHeaderItem( 0 )->setText( i18n( "Preview: %1", m_previewPrefix ) ); 0318 0319 m_originals.clear(); 0320 m_originals.reserve( originals.size() ); 0321 m_previews.clear(); 0322 m_previews.reserve( previews.size() ); 0323 for( int i = 0; i < qMin( originals.size(), previews.size() ); i++ ) 0324 { 0325 m_originals << originals.at( i ).mid( originalPrefix.length() ); 0326 m_previews << previews.at( i ).mid( m_previewPrefix.length() ); 0327 } 0328 0329 QTimer::singleShot( 0, this, &OrganizeCollectionDialog::previewNextBatch ); 0330 } 0331 0332 void 0333 OrganizeCollectionDialog::previewNextBatch() 0334 { 0335 const int batchSize = 100; 0336 0337 QPalette negativePalette = ui->previewTableWidget->palette(); 0338 KColorScheme::adjustBackground( negativePalette, KColorScheme::NegativeBackground ); 0339 0340 int processed = 0; 0341 while( !m_originals.isEmpty() && !m_previews.isEmpty() ) 0342 { 0343 QString originalPath = m_originals.takeFirst(); 0344 QString newPath = m_previews.takeFirst(); 0345 0346 int newRow = ui->previewTableWidget->rowCount(); 0347 ui->previewTableWidget->insertRow( newRow ); 0348 0349 // new path preview in the 1st column 0350 QTableWidgetItem *item = new QTableWidgetItem( newPath ); 0351 if( QFileInfo( m_previewPrefix + newPath ).exists() ) 0352 { 0353 item->setBackground( negativePalette.base() ); 0354 m_conflict = true; 0355 } 0356 ui->previewTableWidget->setItem( newRow, 0, item ); 0357 0358 //original in the second column 0359 item = new QTableWidgetItem( originalPath ); 0360 ui->previewTableWidget->setItem( newRow, 1, item ); 0361 0362 processed++; 0363 if( processed >= batchSize ) 0364 { 0365 // yield some room to the other events in the main loop 0366 QTimer::singleShot( 0, this, &OrganizeCollectionDialog::previewNextBatch ); 0367 return; 0368 } 0369 } 0370 0371 // finished 0372 unsetCursor(); 0373 ui->previewTableWidget->setSortingEnabled( true ); 0374 slotOverwriteModeChanged(); // in fact, m_conflict may have changed 0375 } 0376 0377 void 0378 OrganizeCollectionDialog::slotOverwriteModeChanged() 0379 { 0380 if( m_conflict ) 0381 { 0382 if( ui->overwriteCheck->isChecked() ) 0383 ui->conflictLabel->setText( i18n( "There is a filename conflict, existing files will be overwritten." ) ); 0384 else 0385 ui->conflictLabel->setText( i18n( "There is a filename conflict, existing files will not be changed." ) ); 0386 } 0387 else 0388 ui->conflictLabel->setText(""); // we clear the text instead of hiding it to retain the layout spacing 0389 } 0390 0391 void 0392 OrganizeCollectionDialog::slotDialogAccepted() 0393 { 0394 AmarokConfig::setOrganizeDirectory( ui->folderCombo->currentText() ); 0395 0396 AmarokConfig::setIgnoreThe( ui->optionsWidget->postfixThe() ); 0397 AmarokConfig::setReplaceSpace( ui->optionsWidget->replaceSpaces() ); 0398 AmarokConfig::setVfatCompatible( ui->optionsWidget->vfatCompatible() ); 0399 AmarokConfig::setAsciiOnly( ui->optionsWidget->asciiOnly() ); 0400 AmarokConfig::setReplacementRegexp( ui->optionsWidget->regexpText() ); 0401 AmarokConfig::setReplacementString( ui->optionsWidget->replaceText() ); 0402 } 0403 0404 //The Ok button should be disabled when there's no collection root selected, and when there is no .%filetype in format string 0405 void 0406 OrganizeCollectionDialog::slotEnableOk( int currentCollectionRootIndex ) 0407 { 0408 QString currentCollectionRoot = ui->folderCombo->itemText( currentCollectionRootIndex ); 0409 auto okButton = findChild<QDialogButtonBox*>()->button( QDialogButtonBox::Ok ); 0410 if( okButton ) 0411 okButton->setEnabled( !currentCollectionRoot.isEmpty() ); 0412 }