1 /*
2  * soundkonverterview.cpp
3  *
4  * Copyright (C) 2007 Daniel Faust <hessijames@gmail.com>
5  */
6 #include "soundkonverterview.h"
7 #include "filelist.h"
8 #include "filelistitem.h"
9 #include "combobutton.h"
10 #include "progressindicator.h"
11 #include "optionslayer.h"
12 #include "config.h"
13 #include "logger.h"
14 #include "opener/fileopener.h"
15 #include "opener/diropener.h"
16 #include "opener/cdopener.h"
17 #include "opener/urlopener.h"
18 #include "opener/playlistopener.h"
19 #include "convert.h"
20 #include "options.h"
21 #include "codecproblems.h"
22 
23 #include <KLocale>
24 #include <KPushButton>
25 #include <KIcon>
26 #include <KFileDialog>
27 #include <KMenu>
28 #include <KAction>
29 #include <KActionMenu>
30 #include <KMessageBox>
31 
32 #include <QApplication>
33 #include <QLabel>
34 #include <QLayout>
35 #include <QHBoxLayout>
36 #include <QFont>
37 #include <QTreeView>
38 #include <QToolButton>
39 
40 
soundKonverterView(Logger * _logger,Config * _config,CDManager * _cdManager,QWidget * parent)41 soundKonverterView::soundKonverterView( Logger *_logger, Config *_config, CDManager *_cdManager, QWidget *parent )
42     : QWidget( parent ),
43       config( _config ),
44       logger( _logger ),
45       cdManager( _cdManager )
46 {
47     setAcceptDrops( true );
48 
49     const int fontHeight = QFontMetrics(QApplication::font()).boundingRect("M").size().height();
50 
51     // the grid for all widgets in the main window
52     QGridLayout* gridLayout = new QGridLayout( this );
53 
54     fileList = new FileList( logger, config, this );
55     gridLayout->addWidget( fileList, 1, 0 );
56     gridLayout->setRowStretch( 1, 1 );
57     connect( fileList, SIGNAL(fileCountChanged(int)), this, SLOT(fileCountChanged(int)) );
58     connect( fileList, SIGNAL(conversionStarted()), this, SLOT(conversionStarted()) );
59     connect( fileList, SIGNAL(conversionStopped(bool)), this, SLOT(conversionStopped(bool)) );
60     connect( fileList, SIGNAL(queueModeChanged(bool)), this, SLOT(queueModeChanged(bool)) );
61     connect( fileList, SIGNAL(showLog(int)), this, SIGNAL(showLog(int)) );
62 
63     optionsLayer = new OptionsLayer( config, this );
64     fileList->setOptionsLayer( optionsLayer );
65     optionsLayer->hide();
66     gridLayout->addWidget( optionsLayer, 1, 0 );
67     connect( optionsLayer, SIGNAL(done(const QList<QUrl>&,ConversionOptions*,const QString&)), fileList, SLOT(addFiles(const QList<QUrl>&,ConversionOptions*,const QString&)) );
68     connect( optionsLayer, SIGNAL(saveFileList()), fileList, SLOT(save()) );
69 
70 
71     // add a horizontal box layout for the add combobutton to the grid
72     QHBoxLayout *addBox = new QHBoxLayout();
73     addBox->setContentsMargins( 1, 0, 1, 0 ); // extra margin - determined by experiments
74     gridLayout->addLayout( addBox, 3, 0 );
75 
76     // create the combobutton for adding files to the file list
77     cAdd = new ComboButton( this );
78     QFont font = cAdd->font();
79     //font.setWeight( QFont::DemiBold );
80     font.setPointSize( font.pointSize() + 3 );
81     cAdd->setFont( font );
82     cAdd->insertItem( KIcon("audio-x-generic"), i18n("Add files...") );
83     cAdd->insertItem( KIcon("folder"), i18n("Add folder...") );
84     cAdd->insertItem( KIcon("media-optical-audio"), i18n("Add CD tracks...") );
85     cAdd->insertItem( KIcon("network-workgroup"), i18n("Add url...") );
86     cAdd->insertItem( KIcon("view-media-playlist"), i18n("Add playlist...") );
87     cAdd->increaseHeight( 0.3*fontHeight );
88     addBox->addWidget( cAdd, 0, Qt::AlignVCenter );
89     connect( cAdd, SIGNAL(clicked(int)), this, SLOT(addClicked(int)) );
90     cAdd->setFocus();
91 
92     addBox->addSpacing( fontHeight );
93 
94     startAction = new KAction( KIcon("system-run"), i18n("Start"), this );
95     connect( startAction, SIGNAL(triggered()), fileList, SLOT(startConversion()) );
96 
97     pStart = new KPushButton( KIcon("system-run"), i18n("Start"), this );
98     pStart->setFixedHeight( pStart->size().height() );
99     pStart->setEnabled( false );
100     startAction->setEnabled( false );
101     addBox->addWidget( pStart, 0, Qt::AlignVCenter );
102     connect( pStart, SIGNAL(clicked()), fileList, SLOT(startConversion()) );
103 
104     stopActionMenu = new KActionMenu( KIcon("process-stop"), i18n("Stop"), this );
105     stopActionMenu->setDelayed( false );
106     killAction = new KAction( KIcon("flag-red"), i18n("Stop immediatelly"), this );
107     stopActionMenu->addAction( killAction );
108     connect( killAction, SIGNAL(triggered()), fileList, SLOT(killConversion()) );
109     stopAction = new KAction( KIcon("flag-yellow"), i18n("Stop after current conversions are completed"), this );
110     stopActionMenu->addAction( stopAction );
111     connect( stopAction, SIGNAL(triggered()), fileList, SLOT(stopConversion()) );
112     continueAction = new KAction( KIcon("flag-green"), i18n("Continue after current conversions are completed"), this );
113     stopActionMenu->addAction( continueAction );
114     connect( continueAction, SIGNAL(triggered()), fileList, SLOT(continueConversion()) );
115     queueModeChanged( true );
116 
117     pStop = new KPushButton( KIcon("process-stop"), i18n("Stop"), this );
118     pStop->setFixedHeight( pStop->size().height() );
119     pStop->hide();
120     stopActionMenu->setEnabled( false );
121     pStop->setMenu( stopActionMenu->menu() );
122     addBox->addWidget( pStop, 0, Qt::AlignVCenter );
123 
124     addBox->addSpacing( fontHeight );
125 
126     progressIndicator = new ProgressIndicator( this, ProgressIndicator::Feature( ProgressIndicator::FeatureSpeed | ProgressIndicator::FeatureTime ) );
127     addBox->addWidget( progressIndicator, 0, Qt::AlignVCenter );
128     connect( progressIndicator, SIGNAL(progressChanged(const QString&)), this, SIGNAL(progressChanged(const QString&)) );
129     connect( fileList, SIGNAL(timeChanged(float)), progressIndicator, SLOT(timeChanged(float)) );
130     connect( fileList, SIGNAL(finished(bool)), progressIndicator, SLOT(finished(bool)) );
131 
132     Convert *convert = new Convert( config, fileList, logger, this );
133     connect( fileList, SIGNAL(convertItem(FileListItem*)), convert, SLOT(add(FileListItem*)) );
134     connect( fileList, SIGNAL(killItem(FileListItem*)), convert, SLOT(kill(FileListItem*)) );
135     connect( fileList, SIGNAL(itemRemoved(FileListItem*)), convert, SLOT(itemRemoved(FileListItem*)) );
136     connect( convert, SIGNAL(finished(FileListItem*,FileListItem::ReturnCode,bool)), fileList, SLOT(itemFinished(FileListItem*,FileListItem::ReturnCode,bool)) );
137     connect( convert, SIGNAL(rippingFinished(const QString&)), fileList, SLOT(rippingFinished(const QString&)) );
138 
139     connect( convert, SIGNAL(finishedProcess(int,bool,bool)), logger, SLOT(processCompleted(int,bool,bool)) );
140 
141     connect( convert, SIGNAL(updateTime(float)), progressIndicator, SLOT(update(float)) );
142     connect( convert, SIGNAL(timeFinished(float)), progressIndicator, SLOT(timeFinished(float)) );
143 }
144 
~soundKonverterView()145 soundKonverterView::~soundKonverterView()
146 {}
147 
addClicked(int index)148 void soundKonverterView::addClicked( int index )
149 {
150     if( index == 0 )
151     {
152         showFileDialog();
153     }
154     else if( index == 1 )
155     {
156         showDirDialog();
157     }
158     else if( index == 2 )
159     {
160         showCdDialog();
161     }
162     else if( index == 3 )
163     {
164         showUrlDialog();
165     }
166     else
167     {
168         showPlaylistDialog();
169     }
170 }
171 
showFileDialog()172 void soundKonverterView::showFileDialog()
173 {
174     FileOpener *dialog = new FileOpener( config, this );
175 //     dialog->resize( size().width() - 10, size().height() );
176 
177     if( !dialog->dialogAborted )
178     {
179         connect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), fileList, SLOT(addFiles(const QList<QUrl>&,ConversionOptions*)) );
180 
181         dialog->exec();
182 
183         disconnect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), 0, 0 );
184 
185         fileList->save( false );
186     }
187 
188     delete dialog;
189 }
190 
showDirDialog()191 void soundKonverterView::showDirDialog()
192 {
193     DirOpener *dialog = new DirOpener( config, DirOpener::Convert, this );
194 
195     if( !dialog->dialogAborted )
196     {
197         connect( dialog, SIGNAL(openFiles(const QUrl&,bool,const QStringList&,ConversionOptions*)), fileList, SLOT(addDir(const QUrl&,bool,const QStringList&,ConversionOptions*)) );
198 
199         dialog->exec();
200 
201         disconnect( dialog, SIGNAL(openFiles(const QUrl&,bool,const QStringList&,ConversionOptions*)), 0, 0 );
202 
203         fileList->save( false );
204     }
205 
206     delete dialog;
207 }
208 
showCdDialog(const QString & device,QString _profile,QString _format,const QString & directory,const QString & notifyCommand)209 bool soundKonverterView::showCdDialog( const QString& device, QString _profile, QString _format, const QString& directory, const QString& notifyCommand )
210 {
211     QString profile = _profile;
212     QString format = _format;
213 
214     cleanupParameters( &profile, &format );
215 
216     bool success = false;
217 
218     QString message;
219     QStringList errorList;
220     if( !config->pluginLoader()->canDecode("audio cd",&errorList) )
221     {
222         QList<CodecProblems::Problem> problemList;
223         CodecProblems::Problem problem;
224         problem.codecName = "audio cd";
225         problem.solutions = errorList;
226         problemList += problem;
227         CodecProblems *problemsDialog = new CodecProblems( CodecProblems::AudioCd, problemList, this );
228         problemsDialog->exec();
229         return false;
230     }
231 
232     // create a new CDOpener object for letting the user add some tracks from a CD
233     CDOpener *dialog = new CDOpener( config, device, this );
234 
235     if( !dialog->noCdFound )
236     {
237         if( !profile.isEmpty() )
238             dialog->setProfile( profile );
239 
240         if( !format.isEmpty() )
241             dialog->setFormat( format );
242 
243         if( !directory.isEmpty() )
244             dialog->setOutputDirectory( directory );
245 
246         if( !notifyCommand.isEmpty() )
247             dialog->setCommand( notifyCommand );
248 
249         connect( dialog, SIGNAL(addTracks(const QString&,QList<int>,int,QList<TagData*>,ConversionOptions*,const QString&)), fileList, SLOT(addTracks(const QString&,QList<int>,int,QList<TagData*>,ConversionOptions*,const QString&)) );
250 
251         dialog->exec();
252 
253         disconnect( dialog, SIGNAL(addTracks(const QString&,QList<int>,int,QList<TagData*>,ConversionOptions*,const QString&)), 0, 0 );
254 
255         if( dialog->result() == QDialog::Accepted )
256         {
257             success = true;
258             fileList->save( false );
259         }
260     }
261     else
262     {
263         KMessageBox::error( this, i18n("No CD device found") );
264     }
265 
266     delete dialog;
267 
268     return success;
269 }
270 
showUrlDialog()271 void soundKonverterView::showUrlDialog()
272 {
273     UrlOpener *dialog = new UrlOpener( config, this );
274 
275     connect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), fileList, SLOT(addFiles(const QList<QUrl>&,ConversionOptions*)) );
276 
277     dialog->exec();
278 
279     disconnect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), 0, 0 );
280 
281     delete dialog;
282 
283     fileList->save( false );
284 }
285 
showPlaylistDialog()286 void soundKonverterView::showPlaylistDialog()
287 {
288     PlaylistOpener *dialog = new PlaylistOpener( config, this );
289 //     dialog->resize( size().width() - 10, size().height() );
290 
291     if( !dialog->dialogAborted )
292     {
293         connect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), fileList, SLOT(addFiles(const QList<QUrl>&,ConversionOptions*)) );
294 
295         dialog->exec();
296 
297         disconnect( dialog, SIGNAL(openFiles(const QList<QUrl>&,ConversionOptions*)), 0, 0 );
298 
299         fileList->save( false );
300     }
301 
302     delete dialog;
303 }
304 
addConvertFiles(const KUrl::List & urls,QString _profile,QString _format,const QString & directory,const QString & notifyCommand)305 void soundKonverterView::addConvertFiles( const KUrl::List& urls, QString _profile, QString _format, const QString& directory, const QString& notifyCommand )
306 {
307     QList<QUrl> k_urls;
308     QStringList errorList;
309     //    codec    @0 files @1 solutions
310     QMap< QString, QList<QStringList> > problems;
311     QString fileName;
312 
313     const bool canDecodeAac = config->pluginLoader()->canDecode( "m4a/aac" );
314     const bool canDecodeAlac = config->pluginLoader()->canDecode( "m4a/alac" );
315     const bool checkM4a = ( !canDecodeAac || !canDecodeAlac ) && canDecodeAac != canDecodeAlac;
316 
317     for( int i=0; i<urls.size(); i++ )
318     {
319         QString mimeType;
320         QString codecName = config->pluginLoader()->getCodecFromFile( urls.at(i), &mimeType, checkM4a );
321 
322         if( codecName == "inode/directory" || config->pluginLoader()->canDecode(codecName,&errorList) )
323         {
324             k_urls += urls.at(i);
325         }
326         else
327         {
328             fileName = urls.at(i).pathOrUrl();
329 
330             if( codecName.isEmpty() )
331                 codecName = mimeType;
332             if( codecName.isEmpty() )
333                 codecName = fileName.right(fileName.length()-fileName.lastIndexOf(".")-1);
334 
335             if( problems.value(codecName).count() < 2 )
336             {
337                 problems[codecName] += QStringList();
338                 problems[codecName] += QStringList();
339             }
340             problems[codecName][0] += fileName;
341             if( !errorList.isEmpty() )
342             {
343                 problems[codecName][1] += errorList;
344             }
345             else
346             {
347                 problems[codecName][1] += i18n("This file type is unknown to soundKonverter.\nMaybe you need to install an additional soundKonverter plugin.\nYou should have a look at your distribution's package manager for this.");
348             }
349         }
350     }
351 
352     QList<CodecProblems::Problem> problemList;
353     for( int i=0; i<problems.count(); i++ )
354     {
355         CodecProblems::Problem problem;
356         problem.codecName = problems.keys().at(i);
357         if( problem.codecName != "wav" )
358         {
359             problems[problem.codecName][1].removeDuplicates();
360             problem.solutions = problems.value(problem.codecName).at(1);
361             if( problems.value(problem.codecName).at(0).count() <= 3 )
362             {
363                 problem.affectedFiles = problems.value(problem.codecName).at(0);
364             }
365             else
366             {
367                 problem.affectedFiles += problems.value(problem.codecName).at(0).at(0);
368                 problem.affectedFiles += problems.value(problem.codecName).at(0).at(1);
369                 problem.affectedFiles += i18n("... and %1 more files",problems.value(problem.codecName).at(0).count()-3);
370             }
371             problemList += problem;
372         }
373     }
374 
375     if( problemList.count() > 0 )
376     {
377         CodecProblems *problemsDialog = new CodecProblems( CodecProblems::Decode, problemList, this );
378         problemsDialog->exec();
379     }
380 
381     if( k_urls.count() > 0 )
382     {
383         QString profile = _profile;
384         QString format = _format;
385 
386         cleanupParameters( &profile, &format );
387 
388         const bool isUserProfile = config->data.profiles.contains(profile);
389 
390         if( ( !profile.isEmpty() && !format.isEmpty() && !directory.isEmpty() ) || isUserProfile )
391         {
392             ConversionOptions *conversionOptions = 0;
393 
394             if( !isUserProfile )
395             {
396                 Options *options = new Options( config, "", 0 );
397                 options->hide();
398                 options->setProfile( profile );
399                 options->setFormat( format );
400                 options->setOutputDirectory( directory );
401                 conversionOptions = options->currentConversionOptions();
402                 delete options;
403             }
404             else
405             {
406                 conversionOptions = config->data.profiles.value( profile )->copy();
407             }
408 
409             if( conversionOptions )
410             {
411                 fileList->addFiles( k_urls, conversionOptions, notifyCommand );
412             }
413             else
414             {
415                 // FIXME error message, null pointer for conversion options
416 //                 KMessageBox::error( this, i18n("Sorry, this shouldn't happen.\n\nPlease report this bug and attach the following error message:\n\nsoundKonverterView::addConvertFiles; Options::currentConversionOptions returned 0"), i18n("Internal error") );
417                 KMessageBox::error( this, "Sorry, this shouldn't happen.\n\nPlease report this bug and attach the following error message:\n\nsoundKonverterView::addConvertFiles; conversionOptions=0, isUserProfile="+QString::number(isUserProfile), "Internal error" );
418             }
419         }
420         else
421         {
422             optionsLayer->addUrls( k_urls );
423 
424             if( !profile.isEmpty() )
425                 optionsLayer->setProfile( profile );
426 
427             if( !format.isEmpty() )
428                 optionsLayer->setFormat( format );
429 
430             if( !directory.isEmpty() )
431                 optionsLayer->setOutputDirectory( directory );
432 
433             if( !notifyCommand.isEmpty() )
434                 optionsLayer->setCommand( notifyCommand );
435 
436             optionsLayer->fadeIn();
437         }
438     }
439 
440     fileList->save( false );
441 }
442 
loadAutosaveFileList()443 void soundKonverterView::loadAutosaveFileList()
444 {
445     fileList->load( false );
446 }
447 
loadFileList(const QString & fileListPath)448 void soundKonverterView::loadFileList(const QString& fileListPath)
449 {
450     fileList->load( fileListPath );
451 }
452 
startConversion()453 void soundKonverterView::startConversion()
454 {
455     fileList->startConversion();
456 }
457 
killConversion()458 void soundKonverterView::killConversion()
459 {
460     fileList->killConversion();
461 }
462 
fileCountChanged(int count)463 void soundKonverterView::fileCountChanged( int count )
464 {
465     pStart->setEnabled( count > 0 );
466     startAction->setEnabled( count > 0 );
467 }
468 
conversionStarted()469 void soundKonverterView::conversionStarted()
470 {
471     pStart->hide();
472     startAction->setEnabled( false );
473     pStop->show();
474     stopActionMenu->setEnabled( true );
475     emit signalConversionStarted();
476 }
477 
conversionStopped(bool failed)478 void soundKonverterView::conversionStopped( bool failed )
479 {
480     pStart->show();
481     startAction->setEnabled( true );
482     pStop->hide();
483     stopActionMenu->setEnabled( false );
484     emit signalConversionStopped( failed );
485 }
486 
queueModeChanged(bool enabled)487 void soundKonverterView::queueModeChanged( bool enabled )
488 {
489     stopAction->setVisible( enabled );
490     continueAction->setVisible( !enabled );
491 }
492 
loadFileList(bool user)493 void soundKonverterView::loadFileList( bool user )
494 {
495     fileList->load( user );
496 }
497 
saveFileList(bool user)498 void soundKonverterView::saveFileList( bool user )
499 {
500     fileList->save( user );
501 }
502 
updateFileList()503 void soundKonverterView::updateFileList()
504 {
505     fileList->updateAllItems();
506 }
507 
cleanupParameters(QString * profile,QString * format)508 void soundKonverterView::cleanupParameters( QString *profile, QString *format )
509 {
510     QString old_profile = *profile;
511     QString old_format = *format;
512 
513     QString new_profile;
514     QString new_format;
515 
516     const QStringList formatList = config->pluginLoader()->formatList( PluginLoader::Encode, PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy|PluginLoader::Lossless|PluginLoader::Hybrid) );
517     if( formatList.contains(old_format) )
518     {
519         new_format = old_format;
520     }
521     else
522     {
523         foreach( const QString& format, formatList )
524         {
525             if( config->pluginLoader()->codecExtensions(format).contains(old_format) )
526             {
527                 new_format = format;
528                 break;
529             }
530         }
531     }
532 
533     if( old_profile.toLower() == i18n("Very low").toLower() || old_profile.toLower() == "very low" || old_profile.toLower() == "very_low" )
534     {
535         new_profile = i18n("Very low");
536         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy)).contains(new_format) ? new_format : "";
537     }
538     else if( old_profile.toLower() == i18n("Low").toLower() || old_profile.toLower() == "low" )
539     {
540         new_profile = i18n("Low");
541         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy)).contains(new_format) ? new_format : "";
542     }
543     else if( old_profile.toLower() == i18n("Medium").toLower() || old_profile.toLower() == "medium" )
544     {
545         new_profile = i18n("Medium");
546         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy)).contains(new_format) ? new_format : "";
547     }
548     else if( old_profile.toLower() == i18n("High").toLower() || old_profile.toLower() == "high" )
549     {
550         new_profile = i18n("High");
551         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy)).contains(new_format) ? new_format : "";
552     }
553     else if( old_profile.toLower() == i18n("Very high").toLower() || old_profile.toLower() == "very high" || old_profile.toLower() == "very_high" )
554     {
555         new_profile = i18n("Very high");
556         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::CompressionType(PluginLoader::InferiorQuality|PluginLoader::Lossy)).contains(new_format) ? new_format : "";
557     }
558     else if( old_profile.toLower() == i18n("Lossless").toLower() || old_profile.toLower() == "lossless" )
559     {
560         new_profile = i18n("Lossless");
561         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::Lossless).contains(new_format) ? new_format : "";
562     }
563     else if( old_profile.toLower() == i18n("Hybrid").toLower() || old_profile.toLower() == "hybrid" )
564     {
565         new_profile = i18n("Hybrid");
566         new_format = config->pluginLoader()->formatList(PluginLoader::Encode,PluginLoader::Hybrid).contains(new_format) ? new_format : "";
567     }
568     else if( config->data.profiles.contains(old_profile) )
569     {
570         new_profile = old_profile;
571         const ConversionOptions *conversionOptions = config->data.profiles.value( new_profile );
572         if( conversionOptions )
573             new_format += conversionOptions->codecName;
574     }
575 
576     *profile = new_profile;
577     *format = new_format;
578 }
579 
580 
581 #include "soundkonverterview.moc"
582