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