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 }