1 /**
2  * \file basemainwindow.cpp
3  * Base class for main window.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 9 Jan 2003
8  *
9  * Copyright (C) 2003-2018  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "basemainwindow.h"
28 #include <QTimer>
29 #include <QDir>
30 #include <QCursor>
31 #include <QMessageBox>
32 #include <QInputDialog>
33 #include <QProgressBar>
34 #include <QToolButton>
35 #include <QCloseEvent>
36 #include <QHBoxLayout>
37 #include <QVBoxLayout>
38 #include <QMenu>
39 #include <QIcon>
40 #include <QToolBar>
41 #include <QStatusBar>
42 #include <QApplication>
43 #ifdef Q_OS_MAC
44 #include <sys/stat.h>
45 #include <unistd.h>
46 #endif
47 #include "kid3form.h"
48 #include "kid3application.h"
49 #include "framelist.h"
50 #include "frametablemodel.h"
51 #include "frametable.h"
52 #include "importdialog.h"
53 #include "tagimportdialog.h"
54 #include "batchimportdialog.h"
55 #include "browsecoverartdialog.h"
56 #include "exportdialog.h"
57 #include "findreplacedialog.h"
58 #include "tagsearcher.h"
59 #include "numbertracksdialog.h"
60 #include "filterdialog.h"
61 #include "rendirdialog.h"
62 #include "downloadclient.h"
63 #include "downloaddialog.h"
64 #include "playlistdialog.h"
65 #include "playlisteditdialog.h"
66 #include "editframefieldsdialog.h"
67 #include "progresswidget.h"
68 #include "fileproxymodel.h"
69 #include "fileproxymodeliterator.h"
70 #include "modeliterator.h"
71 #include "taggedfileselection.h"
72 #include "filelist.h"
73 #include "pictureframe.h"
74 #include "fileconfig.h"
75 #include "playlistconfig.h"
76 #include "playlistmodel.h"
77 #include "importconfig.h"
78 #include "exportconfig.h"
79 #include "guiconfig.h"
80 #include "tagconfig.h"
81 #include "filterconfig.h"
82 #include "isettings.h"
83 #include "contexthelp.h"
84 #include "frame.h"
85 #include "textexporter.h"
86 #include "serverimporter.h"
87 #include "batchimporter.h"
88 #include "dirrenamer.h"
89 #include "iplatformtools.h"
90 #include "saferename.h"
91 #include "config.h"
92 #ifdef HAVE_QTMULTIMEDIA
93 #include "audioplayer.h"
94 #include "playtoolbar.h"
95 #endif
96 
97 /**
98  * Constructor.
99  *
100  * @param mainWin main window widget
101  * @param platformTools platform specific tools
102  * @param app application context
103  */
BaseMainWindowImpl(QMainWindow * mainWin,IPlatformTools * platformTools,Kid3Application * app)104 BaseMainWindowImpl::BaseMainWindowImpl(QMainWindow* mainWin,
105                                        IPlatformTools* platformTools,
106                                        Kid3Application* app)
107   : m_platformTools(platformTools), m_w(mainWin), m_self(nullptr),
108     m_deferredItemCountTimer(new QTimer(this)),
109     m_deferredSelectionCountTimer(new QTimer(this)),
110     m_statusLabel(nullptr), m_form(nullptr), m_app(app),
111     m_exportDialog(nullptr), m_findReplaceDialog(nullptr),
112     m_downloadDialog(new DownloadDialog(m_w, tr("Download"))),
113     m_progressWidget(nullptr), m_progressLabel(nullptr),
114     m_progressBar(nullptr), m_progressAbortButton(nullptr),
115     m_editFrameDialog(nullptr), m_playToolBar(nullptr),
116     m_editFrameTaggedFile(nullptr),
117     m_editFrameTagNr(Frame::Tag_2),
118     m_progressTerminationHandler(nullptr),
119     m_folderCount(0), m_fileCount(0), m_selectionCount(0),
120     m_progressDisconnected(false),
121     m_findReplaceActive(false), m_expandNotificationNeeded(false)
122 {
123   m_deferredItemCountTimer->setSingleShot(true);
124   m_deferredItemCountTimer->setInterval(1000);
125   connect(m_deferredItemCountTimer, &QTimer::timeout,
126           this, &BaseMainWindowImpl::onItemCountChanged);
127   m_deferredSelectionCountTimer->setSingleShot(true);
128   m_deferredSelectionCountTimer->setInterval(500);
129   connect(m_deferredSelectionCountTimer, &QTimer::timeout,
130           this, &BaseMainWindowImpl::onSelectionCountChanged);
131 
132   m_downloadDialog->close();
133   ContextHelp::init(m_platformTools);
134 
135   DownloadClient* downloadClient = m_app->getDownloadClient();
136   connect(downloadClient, &HttpClient::progress,
137           m_downloadDialog, &DownloadDialog::updateProgressStatus);
138   connect(downloadClient, &DownloadClient::downloadStarted,
139           m_downloadDialog, &DownloadDialog::showStartOfDownload);
140   connect(downloadClient, &DownloadClient::aborted,
141           m_downloadDialog, &QProgressDialog::reset);
142   connect(m_downloadDialog, &QProgressDialog::canceled,
143           downloadClient, &DownloadClient::cancelDownload);
144   connect(downloadClient,
145     &DownloadClient::downloadFinished,
146     m_app,
147     &Kid3Application::imageDownloaded);
148 
149   connect(m_app, &Kid3Application::fileSelectionUpdateRequested,
150           this, &BaseMainWindowImpl::updateCurrentSelection);
151   connect(m_app, &Kid3Application::selectedFilesUpdated,
152           this, &BaseMainWindowImpl::updateGuiControls);
153   connect(m_app, &Kid3Application::selectedFilesChanged,
154           this, &BaseMainWindowImpl::applySelectionChange);
155   connect(m_app, &Kid3Application::frameModified,
156           this, &BaseMainWindowImpl::updateAfterFrameModification);
157   connect(m_app, &Kid3Application::confirmedOpenDirectoryRequested,
158           this, &BaseMainWindowImpl::confirmedOpenDirectory);
159   connect(m_app, &Kid3Application::toggleExpandedRequested,
160           this, &BaseMainWindowImpl::toggleExpanded);
161   connect(m_app, &Kid3Application::expandFileListRequested,
162           this, &BaseMainWindowImpl::expandFileList);
163   connect(m_app, &Kid3Application::directoryOpened,
164           this, &BaseMainWindowImpl::onDirectoryOpened);
165   connect(m_app, &Kid3Application::modifiedChanged,
166           this, &BaseMainWindowImpl::updateWindowCaption);
167   connect(m_app, &Kid3Application::filteredChanged,
168           this, &BaseMainWindowImpl::updateWindowCaption);
169   connect(m_app, &Kid3Application::longRunningOperationProgress,
170           this, &BaseMainWindowImpl::showOperationProgress);
171   connect(m_app, &Kid3Application::aboutToPlayAudio,
172           this, &BaseMainWindowImpl::showPlayToolBar);
173 }
174 
175 /**
176  * Destructor.
177  */
~BaseMainWindowImpl()178 BaseMainWindowImpl::~BaseMainWindowImpl()
179 {
180   qDeleteAll(m_playlistEditDialogs);
181 #ifdef HAVE_QTMULTIMEDIA
182   delete m_playToolBar;
183 #endif
184 }
185 
186 /**
187  * Initialize main window.
188  * Shall be called at end of constructor body.
189  */
init()190 void BaseMainWindowImpl::init()
191 {
192   m_statusLabel = new QLabel;
193   m_w->statusBar()->addWidget(m_statusLabel);
194   m_form = new Kid3Form(m_app, this, m_w);
195   m_w->setCentralWidget(m_form);
196 
197   m_self->initActions();
198 
199   m_w->resize(m_w->sizeHint());
200 
201   readOptions();
202   applyChangedShortcuts();
203 }
204 
205 /**
206  * Open directory, user has to confirm if current directory modified.
207  *
208  * @param paths directory or file paths
209  */
confirmedOpenDirectory(const QStringList & paths)210 void BaseMainWindowImpl::confirmedOpenDirectory(const QStringList& paths)
211 {
212   if (!saveModified()) {
213     return;
214   }
215   QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
216   slotStatusMsg(tr("Opening folder..."));
217 
218   m_app->openDirectory(paths, false);
219 
220   slotClearStatusMsg();
221   QApplication::restoreOverrideCursor();
222 }
223 
224 /**
225  * Update the recent file list and the caption when a new directory
226  * is opened.
227  */
onDirectoryOpened()228 void BaseMainWindowImpl::onDirectoryOpened()
229 {
230   m_self->addDirectoryToRecentFiles(m_app->getDirName());
231   updateWindowCaption();
232 }
233 
234 /**
235  * Save application options.
236  */
saveOptions()237 void BaseMainWindowImpl::saveOptions()
238 {
239   m_self->saveConfig();
240   m_form->saveConfig();
241   m_app->saveConfig();
242 }
243 
244 /**
245  * Load application options.
246  */
readOptions()247 void BaseMainWindowImpl::readOptions()
248 {
249   m_app->readConfig();
250   m_self->readConfig();
251   m_form->readConfig();
252 }
253 
254 /**
255  * Show progress of long running operation in status bar.
256  * @param name name of operation
257  * @param done amount of work done
258  * @param total total amount of work
259  * @param abort if not 0, can be set to true to abort operation
260  */
showOperationProgress(const QString & name,int done,int total,bool * abort)261 void BaseMainWindowImpl::showOperationProgress(const QString& name,
262                                                int done, int total, bool* abort)
263 {
264   if (done == -1) {
265     // Operation started.
266     if (!m_progressLabel) {
267       m_progressLabel = new QLabel;
268     }
269     if (!m_progressBar) {
270       m_progressBar = new QProgressBar;
271     }
272     if (!m_progressAbortButton) {
273       m_progressAbortButton = new QToolButton;
274       m_progressAbortButton->setIcon(
275             QIcon(m_w->style()->standardIcon(QStyle::SP_BrowserStop)));
276       m_progressAbortButton->setToolTip(tr("Abort"));
277       m_progressAbortButton->setCheckable(true);
278     }
279     if (m_statusLabel) {
280       m_w->statusBar()->removeWidget(m_statusLabel);
281     }
282     m_w->statusBar()->addPermanentWidget(m_progressLabel);
283     m_w->statusBar()->addPermanentWidget(m_progressBar, 1);
284     m_w->statusBar()->addPermanentWidget(m_progressAbortButton, 1);
285     m_progressLabel->setText(name);
286     m_progressBar->setMinimum(0);
287     m_progressBar->setMaximum(total);
288     m_progressBar->setValue(0);
289     m_progressAbortButton->setChecked(false);
290   } else if (done == total && total != 0) {
291     // Operation finished.
292     if (m_progressLabel) {
293       m_w->statusBar()->removeWidget(m_progressLabel);
294       delete m_progressLabel;
295       m_progressLabel = nullptr;
296     }
297     if (m_progressBar) {
298       m_w->statusBar()->removeWidget(m_progressBar);
299       delete m_progressBar;
300       m_progressBar = nullptr;
301     }
302     if (m_progressAbortButton) {
303       m_w->statusBar()->removeWidget(m_progressAbortButton);
304       delete m_progressAbortButton;
305       m_progressAbortButton = nullptr;
306       if (m_statusLabel) {
307         m_w->statusBar()->addWidget(m_statusLabel);
308         m_statusLabel->show();
309       }
310     }
311     slotClearStatusMsg();
312   } else if (done < total || (done == 0 && total == 0)) {
313     // Operation progress.
314     if (m_progressBar) {
315       m_progressBar->setMaximum(total);
316       m_progressBar->setValue(done);
317       // Is needed to get abort button events.
318       qApp->processEvents();
319     }
320     if (m_progressAbortButton && m_progressAbortButton->isChecked() && abort) {
321       *abort = true;
322     }
323   }
324 }
325 
326 /**
327  * Save all changed files.
328  *
329  * @param updateGui true to update GUI (controls, status, cursor)
330  */
saveDirectory(bool updateGui)331 void BaseMainWindowImpl::saveDirectory(bool updateGui)
332 {
333   if (updateGui) {
334     updateCurrentSelection();
335     QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
336   }
337 
338 #if defined Q_OS_WIN32 && defined HAVE_QTMULTIMEDIA
339   // Close player on Windows because it holds file handles which prevent
340   // files from being saved.
341   if (m_playToolBar) {
342     m_playToolBar->close();
343     delete m_playToolBar;
344     m_playToolBar = nullptr;
345   }
346   m_app->deleteAudioPlayer();
347 #endif
348 
349   const QStringList errorFiles = m_app->saveDirectory();
350 
351   if (!errorFiles.empty()) {
352     QStringList errorMsgs, notWritableFiles;
353     errorMsgs.reserve(errorFiles.size());
354     for (const QString& filePath : errorFiles) {
355       QFileInfo fileInfo(filePath);
356       if (!fileInfo.isWritable()) {
357         errorMsgs.append(tr("%1 is not writable").arg(fileInfo.fileName()));
358         notWritableFiles.append(filePath); // clazy:exclude=reserve-candidates
359       } else {
360         errorMsgs.append(fileInfo.fileName());
361       }
362     }
363     if (notWritableFiles.isEmpty()) {
364       m_platformTools->errorList(
365         m_w, tr("Error while writing file:\n"),
366         errorMsgs,
367         tr("File Error"));
368     } else {
369       int rc = m_platformTools->warningYesNoList(
370         m_w, tr("Error while writing file. "
371                 "Do you want to change the permissions?"),
372         errorMsgs,
373         tr("File Error"));
374       if (rc == QMessageBox::Yes) {
375         auto model =
376             qobject_cast<FileProxyModel*>(m_form->getFileList()->model());
377         TaggedFile* taggedFile;
378         const auto filePaths = notWritableFiles;
379         for (const QString& filePath : filePaths) {
380 #ifdef Q_OS_MAC
381           // On macOS, files may be locked, so try to make them mutable.
382           const auto utf8Path = filePath.toUtf8();
383           struct stat st;
384           if (::stat(utf8Path.constData(), &st) == 0 &&
385               st.st_flags & UF_IMMUTABLE) {
386             ::chflags(utf8Path.constData(), st.st_flags & ~UF_IMMUTABLE);
387           }
388 #endif
389           QFile::setPermissions(filePath,
390               QFile::permissions(filePath) | QFile::WriteUser);
391           if (model &&
392               (taggedFile = FileProxyModel::getTaggedFileOfIndex(
393                  model->index(filePath))) != nullptr) {
394             taggedFile->undoRevertChangedFilename();
395           }
396         }
397         m_app->saveDirectory();
398       }
399     }
400   }
401 
402   if (updateGui) {
403     QApplication::restoreOverrideCursor();
404     updateGuiControls();
405   }
406 }
407 
408 /**
409  * If anything was modified, save after asking user.
410  *
411  * @param doNotRevert if true, modifications are not reverted, this can be
412  * used to skip the possibly long process if the application is not be closed
413  *
414  * @return false if user canceled.
415  */
saveModified(bool doNotRevert)416 bool BaseMainWindowImpl::saveModified(bool doNotRevert)
417 {
418   bool completed = true;
419 
420   if(m_app->isModified() && !m_app->getDirName().isEmpty())
421   {
422     int want_save = m_platformTools->warningYesNoCancel(
423         m_w,
424         tr("The current folder has been modified.\n"
425        "Do you want to save it?"),
426         tr("Warning"));
427     switch(want_save)
428     {
429     case QMessageBox::Yes:
430       saveDirectory();
431       completed = true;
432       break;
433 
434     case QMessageBox::No:
435       if (!doNotRevert) {
436         if (m_app->getFileSelectionModel())
437           m_app->getFileSelectionModel()->clearSelection();
438         m_app->revertFileModifications();
439       }
440       completed = true;
441       break;
442 
443     case QMessageBox::Cancel:
444       completed = false;
445       break;
446 
447     default:
448       completed = false;
449       break;
450     }
451   }
452 
453   return completed;
454 }
455 
456 /**
457  * If a playlist was modified, save after asking user.
458  * @return false if user canceled.
459  */
saveModifiedPlaylists()460 bool BaseMainWindowImpl::saveModifiedPlaylists()
461 {
462   if (m_app->hasModifiedPlaylistModel()) {
463     int answer = m_platformTools->warningYesNoCancel(
464         m_w,
465         tr("A playlist has been modified.\n"
466            "Do you want to save it?"),
467         tr("Warning"));
468     if (answer == QMessageBox::Yes) {
469       m_app->saveModifiedPlaylistModels();
470     }
471     if (answer != QMessageBox::Yes && answer != QMessageBox::No) {
472       return false;
473     }
474   }
475   return true;
476 }
477 
478 /**
479  * Free allocated resources.
480  * Our destructor may not be called, so cleanup is done here.
481  */
cleanup()482 void BaseMainWindowImpl::cleanup()
483 {
484   m_app->getSettings()->sync();
485 }
486 
487 /**
488  * Update modification state before closing.
489  * If anything was modified, save after asking user.
490  * Save options before closing.
491  * This method shall be called by closeEvent() (Qt) or
492  * queryClose() (KDE).
493  *
494  * @return false if user canceled,
495  *         true will quit the application.
496  */
queryBeforeClosing()497 bool BaseMainWindowImpl::queryBeforeClosing()
498 {
499   updateCurrentSelection();
500   if (saveModified(true) && saveModifiedPlaylists()) {
501     saveOptions();
502     cleanup();
503     return true;
504   }
505   return false;
506 }
507 
508 /**
509  * Request new directory and open it.
510  */
slotFileOpen()511 void BaseMainWindowImpl::slotFileOpen()
512 {
513   updateCurrentSelection();
514   if(saveModified()) {
515     static QString flt = m_app->createFilterString();
516     QString filter(FileConfig::instance().nameFilter());
517     QStringList dirs = m_platformTools->getOpenFileNames(
518       m_w, QString(), m_app->getDirName(), flt, &filter);
519     if (!dirs.isEmpty()) {
520       m_app->resetFileFilterIfNotMatching(dirs);
521       m_app->openDirectory(dirs);
522     }
523   }
524 }
525 
526 /**
527  * Request new directory and open it.
528  */
slotFileOpenDirectory()529 void BaseMainWindowImpl::slotFileOpenDirectory()
530 {
531   updateCurrentSelection();
532   if(saveModified()) {
533     QString dir = m_platformTools->getExistingDirectory(m_w, QString(),
534                                                         m_app->getDirName());
535     if (!dir.isEmpty()) {
536       m_app->openDirectory({dir});
537     }
538   }
539 }
540 
541 /**
542  * Reload the current directory.
543  */
slotFileReload()544 void BaseMainWindowImpl::slotFileReload()
545 {
546   updateCurrentSelection();
547   if(saveModified()) {
548     m_app->openDirectoryAfterReset();
549   }
550 }
551 
552 /**
553  * Open recent directory.
554  *
555  * @param dir directory to open
556  */
openRecentDirectory(const QString & dir)557 void BaseMainWindowImpl::openRecentDirectory(const QString& dir)
558 {
559   updateCurrentSelection();
560   confirmedOpenDirectory({dir});
561 }
562 
563 /**
564  * Save modified files.
565  */
slotFileSave()566 void BaseMainWindowImpl::slotFileSave()
567 {
568   saveDirectory(true);
569 }
570 
571 /**
572  * Quit application.
573  */
slotFileQuit()574 void BaseMainWindowImpl::slotFileQuit()
575 {
576   slotStatusMsg(tr("Exiting..."));
577   m_w->close(); /* this will lead to call of closeEvent(), queryClose() */
578   slotClearStatusMsg();
579 }
580 
581 /**
582  * Change visibility of status bar.
583  * @param visible true to show status bar
584  */
setStatusBarVisible(bool visible)585 void BaseMainWindowImpl::setStatusBarVisible(bool visible)
586 {
587   auto model =
588       qobject_cast<FileProxyModel*>(m_form->getFileList()->model());
589   auto selModel = m_app->getFileSelectionModel();
590   if (visible) {
591     m_w->statusBar()->show();
592     if (model && selModel) {
593       connect(model, &FileProxyModel::sortingFinished,
594               m_deferredItemCountTimer,
595               static_cast<void (QTimer::*)()>(&QTimer::start),
596               Qt::UniqueConnection);
597       connect(model->sourceModel(), &QAbstractItemModel::dataChanged,
598               m_deferredItemCountTimer,
599               static_cast<void (QTimer::*)()>(&QTimer::start),
600               Qt::UniqueConnection);
601       connect(selModel, &QItemSelectionModel::selectionChanged,
602               m_deferredSelectionCountTimer,
603               static_cast<void (QTimer::*)()>(&QTimer::start),
604               Qt::UniqueConnection);
605     }
606     onItemCountChanged();
607     onSelectionCountChanged();
608   } else {
609     m_deferredItemCountTimer->stop();
610     m_deferredSelectionCountTimer->stop();
611     m_w->statusBar()->hide();
612     if (model && selModel) {
613       disconnect(model, &FileProxyModel::sortingFinished,
614                  m_deferredItemCountTimer,
615                  static_cast<void (QTimer::*)()>(&QTimer::start));
616       disconnect(model->sourceModel(), &QAbstractItemModel::dataChanged,
617                  m_deferredItemCountTimer,
618                  static_cast<void (QTimer::*)()>(&QTimer::start));
619       disconnect(selModel, &QItemSelectionModel::selectionChanged,
620                  m_deferredSelectionCountTimer,
621                  static_cast<void (QTimer::*)()>(&QTimer::start));
622     }
623     m_folderCount = 0;
624     m_fileCount = 0;
625     m_selectionCount = 0;
626     updateStatusLabel();
627   }
628 }
629 
630 /**
631  * Called when the item count of the file proxy model changed.
632  */
onItemCountChanged()633 void BaseMainWindowImpl::onItemCountChanged()
634 {
635   if (auto model =
636       qobject_cast<FileProxyModel*>(m_form->getFileList()->model())) {
637     model->countItems(m_app->getRootIndex(), m_folderCount, m_fileCount);
638     updateStatusLabel();
639   }
640 }
641 
642 /**
643  * Called when the item count of the file selection model changed.
644  */
onSelectionCountChanged()645 void BaseMainWindowImpl::onSelectionCountChanged()
646 {
647   if (auto selModel = m_app->getFileSelectionModel()) {
648     m_selectionCount = selModel->selectedRows().size();
649     updateStatusLabel();
650   }
651 }
652 
653 /**
654  * Update label of status bar with information about the number of files.
655  */
updateStatusLabel()656 void BaseMainWindowImpl::updateStatusLabel()
657 {
658   if (m_statusLabel) {
659     QStringList counts;
660     if (m_folderCount != 0) {
661       //~ singular %n folder
662       //~ plural %n folders
663       counts.append(tr("%n folders", "", m_folderCount));
664     }
665     if (m_fileCount != 0) {
666       //~ singular %n file
667       //~ plural %n files
668       counts.append(tr("%n files", "", m_fileCount));
669     }
670     if (m_selectionCount != 0) {
671       //~ singular %n selected
672       //~ plural %n selected
673       counts.append(tr("%n selected", "", m_selectionCount));
674     }
675     if (counts.isEmpty()) {
676       m_statusLabel->setText((tr("Ready.")));
677     } else {
678       m_statusLabel->setText(counts.join(QLatin1String(", ")));
679     }
680   }
681 }
682 
683 /**
684  * Change status message.
685  *
686  * @param text message
687  */
slotStatusMsg(const QString & text)688 void BaseMainWindowImpl::slotStatusMsg(const QString& text)
689 {
690   m_w->statusBar()->showMessage(text);
691   // processEvents() is necessary to make the change of the status bar
692   // visible when it is changed back again in the same function,
693   // i.e. in the same call from the Qt main event loop.
694   qApp->processEvents();
695 }
696 
697 /**
698  * Clear status message.
699  * To be called when a message set with slotStatusMsg() is no longer valid.
700  */
slotClearStatusMsg()701 void BaseMainWindowImpl::slotClearStatusMsg()
702 {
703   m_w->statusBar()->clearMessage();
704 }
705 
706 /**
707  * Show playlist dialog.
708  */
slotPlaylistDialog()709 void BaseMainWindowImpl::slotPlaylistDialog()
710 {
711   if (!m_playlistDialog) {
712     m_playlistDialog.reset(new PlaylistDialog(m_w));
713   }
714   m_playlistDialog->readConfig();
715   if (m_playlistDialog->exec() == QDialog::Accepted) {
716     PlaylistConfig cfg;
717     m_playlistDialog->getCurrentConfig(cfg);
718     QString newEmptyPlaylistFileName =
719         m_playlistDialog->getFileNameForNewEmptyPlaylist();
720     if (newEmptyPlaylistFileName.isEmpty()) {
721       writePlaylist(cfg);
722     } else {
723       m_app->writeEmptyPlaylist(cfg, newEmptyPlaylistFileName);
724     }
725   }
726 }
727 
728 /**
729  * Write playlist according to playlist configuration.
730  *
731  * @param cfg playlist configuration to use
732  *
733  * @return true if ok.
734  */
writePlaylist(const PlaylistConfig & cfg)735 bool BaseMainWindowImpl::writePlaylist(const PlaylistConfig& cfg)
736 {
737   QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
738   slotStatusMsg(tr("Creating playlist..."));
739 
740   bool ok = m_app->writePlaylist(cfg);
741 
742   slotClearStatusMsg();
743   QApplication::restoreOverrideCursor();
744   return ok;
745 }
746 
747 /**
748  * Create playlist.
749  *
750  * @return true if ok.
751  */
slotCreatePlaylist()752 bool BaseMainWindowImpl::slotCreatePlaylist()
753 {
754   return writePlaylist(PlaylistConfig::instance());
755 }
756 
757 /**
758  * Open dialog to edit playlist.
759  * @param playlistPath path to playlist file
760  */
showPlaylistEditDialog(const QString & playlistPath)761 void BaseMainWindowImpl::showPlaylistEditDialog(const QString& playlistPath)
762 {
763   PlaylistEditDialog* dialog = m_playlistEditDialogs.value(playlistPath);
764   if (!dialog) {
765     PlaylistModel* model = m_app->playlistModel(playlistPath);
766     dialog = new PlaylistEditDialog(model,
767                                     m_form->getFileList()->selectionModel(),
768                                     m_w);
769     connect(dialog, &QDialog::finished,
770             this, &BaseMainWindowImpl::onPlaylistEditDialogFinished);
771     m_playlistEditDialogs.insert(playlistPath, dialog);
772 
773     // The playlist windows are placed above the directory list.
774     // If multiple playlist windows are open, they are displaced by the height
775     // of the title bar.
776     QWidget* dirList = m_form->getDirList();
777     int titleBarHeight = dialog->style()->pixelMetric(QStyle::PM_TitleBarHeight);
778     int yOffset = titleBarHeight * m_playlistEditDialogs.size();
779     QRect geometry(dirList->mapToGlobal(QPoint(0, 0)), dirList->size());
780     geometry.setTop(geometry.top() + yOffset);
781     dialog->setGeometry(geometry);
782 
783     QStringList filesNotFound = model->filesNotFound();
784     if (!filesNotFound.isEmpty()) {
785       m_platformTools->warningDialog(
786             m_w, tr("Files not found"), filesNotFound.join(QLatin1Char('\n')),
787             tr("Error"));
788     }
789   }
790 
791   dialog->showNormal();
792   dialog->raise();
793 }
794 
795 /**
796  * Called when a playlist edit dialog is closed.
797  */
onPlaylistEditDialogFinished()798 void BaseMainWindowImpl::onPlaylistEditDialogFinished()
799 {
800   if (auto dialog = qobject_cast<PlaylistEditDialog*>(sender())) {
801     m_playlistEditDialogs.remove(m_playlistEditDialogs.key(dialog));
802     dialog->deleteLater();
803   }
804 }
805 
806 /**
807  * Update track data and create import dialog.
808  */
setupImportDialog()809 void BaseMainWindowImpl::setupImportDialog()
810 {
811   m_app->filesToTrackDataModel(ImportConfig::instance().importDest());
812   if (!m_importDialog) {
813     QString caption(tr("Import"));
814     m_importDialog.reset(
815       new ImportDialog(m_platformTools, m_w, caption,
816                        m_app->getTrackDataModel(),
817                        m_app->genreModel(Frame::Tag_2),
818                        m_app->getServerImporters(),
819                        m_app->getServerTrackImporters()));
820     connect(m_importDialog.data(), &QDialog::accepted,
821             this, &BaseMainWindowImpl::applyImportedTrackData);
822   }
823   m_importDialog->clear();
824 }
825 
826 /**
827  * Set tagged files of directory from imported track data model.
828  */
applyImportedTrackData()829 void BaseMainWindowImpl::applyImportedTrackData()
830 {
831   m_app->trackDataModelToFiles(m_importDialog->getDestination());
832 }
833 
834 /**
835  * Import.
836  */
slotImport()837 void BaseMainWindowImpl::slotImport()
838 {
839   if (auto action = qobject_cast<QAction*>(sender())) {
840     setupImportDialog();
841     if (m_importDialog) {
842       m_importDialog->showWithSubDialog(action->data().toInt());
843     }
844   }
845 }
846 
847 /**
848  * Tag import.
849  */
slotTagImport()850 void BaseMainWindowImpl::slotTagImport()
851 {
852   if (!m_tagImportDialog) {
853     m_tagImportDialog.reset(new TagImportDialog(m_w, nullptr));
854     connect(m_tagImportDialog.data(), &TagImportDialog::trackDataUpdated,
855             this, [this]() {
856       m_app->importFromTagsToSelection(
857             m_tagImportDialog->getDestination(),
858             m_tagImportDialog->getSourceFormat(),
859             m_tagImportDialog->getExtractionFormat());
860     });
861   }
862   m_tagImportDialog->clear();
863   m_tagImportDialog->show();
864 }
865 
866 /**
867  * Batch import.
868  */
slotBatchImport()869 void BaseMainWindowImpl::slotBatchImport()
870 {
871   if (!m_batchImportDialog) {
872     m_batchImportDialog.reset(new BatchImportDialog(m_app->getServerImporters(),
873                                                 m_w));
874     connect(m_batchImportDialog.data(), &BatchImportDialog::start,
875             m_app, static_cast<void (Kid3Application::*)(
876               const BatchImportProfile&, Frame::TagVersion)>(
877               &Kid3Application::batchImport));
878     connect(m_app->getBatchImporter(), &BatchImporter::reportImportEvent,
879             m_batchImportDialog.data(), &BatchImportDialog::showImportEvent);
880     connect(m_batchImportDialog.data(), &BatchImportDialog::abort,
881             m_app->getBatchImporter(), &BatchImporter::abort);
882     connect(m_app->getBatchImporter(), &BatchImporter::finished,
883             this, &BaseMainWindowImpl::updateGuiControls);
884   }
885   m_app->getBatchImporter()->clearAborted();
886   m_batchImportDialog->readConfig();
887   m_batchImportDialog->show();
888 }
889 
890 /**
891  * Browse album cover artwork.
892  */
slotBrowseCoverArt()893 void BaseMainWindowImpl::slotBrowseCoverArt()
894 {
895   if (!m_browseCoverArtDialog) {
896     m_browseCoverArtDialog.reset(new BrowseCoverArtDialog(m_app, m_w));
897   }
898   FrameCollection frames2;
899   QModelIndex index = m_form->getFileList()->currentIndex();
900   if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
901     taggedFile->readTags(false);
902     frames2.clear();
903     for (Frame::TagNumber tagNr : Frame::allTagNumbers()) {
904       if (frames2.empty()) {
905         taggedFile->getAllFrames(tagNr, frames2);
906       } else {
907         FrameCollection frames1;
908         taggedFile->getAllFrames(tagNr, frames1);
909         frames2.merge(frames1);
910       }
911     }
912   }
913 
914   m_browseCoverArtDialog->readConfig();
915   m_browseCoverArtDialog->setFrames(frames2);
916   m_browseCoverArtDialog->exec();
917 }
918 
919 /**
920  * Export.
921  */
slotExport()922 void BaseMainWindowImpl::slotExport()
923 {
924   m_exportDialog = new ExportDialog(
925         m_platformTools, m_w, m_app->getTextExporter());
926   m_exportDialog->readConfig();
927   ImportTrackDataVector trackDataVector;
928   m_app->filesToTrackData(ExportConfig::instance().exportSource(),
929                           trackDataVector);
930   m_app->getTextExporter()->setTrackData(trackDataVector);
931   m_exportDialog->showPreview();
932   m_exportDialog->exec();
933   delete m_exportDialog;
934   m_exportDialog = nullptr;
935 }
936 
937 /**
938  * Toggle auto hiding of tags.
939  */
slotSettingsAutoHideTags()940 void BaseMainWindowImpl::slotSettingsAutoHideTags()
941 {
942   GuiConfig::instance().setAutoHideTags(m_self->autoHideTagsAction()->isChecked());
943   updateCurrentSelection();
944   updateGuiControls();
945 }
946 
947 /**
948  * Show or hide picture.
949  */
slotSettingsShowHidePicture()950 void BaseMainWindowImpl::slotSettingsShowHidePicture()
951 {
952   GuiConfig::instance().setHidePicture(!m_self->showHidePictureAction()->isChecked());
953 
954   m_form->hidePicture(GuiConfig::instance().hidePicture());
955   // In Qt3 the picture is displayed too small if Kid3 is started with picture
956   // hidden, and then "Show Picture" is triggered while a file with a picture
957   // is selected. Thus updating the controls is only done for Qt4, in Qt3 the
958   // file has to be selected again for the picture to be shown.
959   if (!GuiConfig::instance().hidePicture()) {
960     updateGuiControls();
961   }
962 }
963 
964 /**
965  * Apply configuration changes.
966  */
applyChangedConfiguration()967 void BaseMainWindowImpl::applyChangedConfiguration()
968 {
969   m_app->applyChangedConfiguration();
970   if (!FileConfig::instance().markChanges()) {
971     m_form->markChangedFilename(false);
972   }
973 }
974 
975 /**
976  * Apply keyboard shortcut changes.
977  */
applyChangedShortcuts()978 void BaseMainWindowImpl::applyChangedShortcuts()
979 {
980   auto shortcuts = m_self->shortcutsMap();
981   m_form->setSectionActionShortcuts(shortcuts);
982 }
983 
984 /**
985  * Find and replace in tags of files.
986  * @param findOnly true to display only find part of dialog
987  */
findReplace(bool findOnly)988 void BaseMainWindowImpl::findReplace(bool findOnly)
989 {
990   TagSearcher* tagSearcher = m_app->getTagSearcher();
991   if (!m_findReplaceDialog) {
992     m_findReplaceDialog = new FindReplaceDialog(m_w);
993     connect(m_findReplaceDialog, &FindReplaceDialog::findRequested,
994             m_app, &Kid3Application::findText);
995     connect(m_findReplaceDialog,
996             &FindReplaceDialog::replaceRequested,
997             m_app, &Kid3Application::replaceText);
998     connect(m_findReplaceDialog,
999             &FindReplaceDialog::replaceAllRequested,
1000             m_app, &Kid3Application::replaceAll);
1001     connect(m_findReplaceDialog, &QDialog::finished,
1002             this, &BaseMainWindowImpl::deactivateFindReplace);
1003     connect(tagSearcher, &TagSearcher::progress,
1004             m_findReplaceDialog, &FindReplaceDialog::showProgress);
1005   }
1006   m_findReplaceDialog->init(findOnly);
1007   m_findReplaceDialog->show();
1008   if (!m_findReplaceActive) {
1009     QModelIndexList selItems(m_app->getFileSelectionModel()->selectedRows());
1010     if (selItems.size() == 1) {
1011       tagSearcher->setStartIndex(selItems.first());
1012     }
1013     connect(tagSearcher, &TagSearcher::textFound,
1014             this, &BaseMainWindowImpl::showFoundText);
1015     connect(tagSearcher, &TagSearcher::textReplaced,
1016             this, &BaseMainWindowImpl::updateReplacedText);
1017     m_findReplaceActive = true;
1018   }
1019 }
1020 
1021 /**
1022  * Deactivate showing of find replace results.
1023  */
deactivateFindReplace()1024 void BaseMainWindowImpl::deactivateFindReplace()
1025 {
1026   if (m_findReplaceActive) {
1027     TagSearcher* tagSearcher = m_app->getTagSearcher();
1028     tagSearcher->abort();
1029     disconnect(tagSearcher, &TagSearcher::textFound,
1030                this, &BaseMainWindowImpl::showFoundText);
1031     disconnect(tagSearcher, &TagSearcher::textReplaced,
1032                this, &BaseMainWindowImpl::updateReplacedText);
1033     m_findReplaceActive = false;
1034   }
1035 }
1036 
1037 /**
1038  * Ensure that found text is made visible in the GUI.
1039  */
showFoundText()1040 void BaseMainWindowImpl::showFoundText()
1041 {
1042   const TagSearcher::Position& pos = m_app->getTagSearcher()->getPosition();
1043   if (pos.isValid()) {
1044     m_app->getFileSelectionModel()->setCurrentIndex(pos.getFileIndex(),
1045         QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1046     if (pos.getPart() == TagSearcher::Position::FileName) {
1047       m_form->setFilenameSelection(pos.getMatchedPos(), pos.getMatchedLength());
1048     } else {
1049       m_form->frameTable(TagSearcher::Position::partToTagNumber(pos.getPart()))
1050           ->setValueSelection(pos.getFrameIndex(), pos.getMatchedPos(),
1051                               pos.getMatchedLength());
1052     }
1053   }
1054 }
1055 
1056 /**
1057  * Update GUI controls after text has been replaced.
1058  */
updateReplacedText()1059 void BaseMainWindowImpl::updateReplacedText()
1060 {
1061   const TagSearcher::Position& pos = m_app->getTagSearcher()->getPosition();
1062   if (pos.isValid()) {
1063     m_app->getFileSelectionModel()->setCurrentIndex(pos.getFileIndex(),
1064         QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1065     updateGuiControls();
1066   }
1067 }
1068 
1069 /**
1070  * Rename directory.
1071  */
slotRenameDirectory()1072 void BaseMainWindowImpl::slotRenameDirectory()
1073 {
1074   if (saveModified()) {
1075     if (!m_renDirDialog) {
1076       m_renDirDialog.reset(new RenDirDialog(m_w, m_app->getDirRenamer()));
1077       connect(m_renDirDialog.data(), &RenDirDialog::actionSchedulingRequested,
1078               m_app, &Kid3Application::scheduleRenameActions);
1079       connect(m_app->getDirRenamer(), &DirRenamer::actionScheduled,
1080               m_renDirDialog.data(), &RenDirDialog::displayActionPreview);
1081     }
1082     if (TaggedFile* taggedFile =
1083       TaggedFileOfDirectoryIterator::first(m_app->currentOrRootIndex())) {
1084       m_renDirDialog->startDialog(taggedFile);
1085     } else {
1086       m_renDirDialog->startDialog(nullptr, m_app->getDirName());
1087     }
1088     if (m_renDirDialog->exec() == QDialog::Accepted) {
1089       QString errorMsg(m_app->performRenameActions());
1090       if (!errorMsg.isEmpty()) {
1091 #ifdef Q_OS_WIN32
1092         if (m_platformTools->warningContinueCancelList(
1093               m_w, tr("Error while renaming:\n") +
1094               tr("Retry after closing directories?"), QStringList(errorMsg),
1095               tr("File Error"))) {
1096           m_app->tryRenameActionsAfterReset();
1097         }
1098 #else
1099         m_platformTools->warningDialog(m_w, tr("Error while renaming:\n"),
1100                                        errorMsg, tr("File Error"));
1101 #endif
1102       }
1103     }
1104   }
1105 }
1106 
1107 /**
1108  * Number tracks.
1109  */
slotNumberTracks()1110 void BaseMainWindowImpl::slotNumberTracks()
1111 {
1112   if (!m_numberTracksDialog) {
1113     m_numberTracksDialog.reset(new NumberTracksDialog(m_w));
1114   }
1115   m_numberTracksDialog->setTotalNumberOfTracks(
1116     m_app->getTotalNumberOfTracksInDir(),
1117         TagConfig::instance().enableTotalNumberOfTracks());
1118   if (m_numberTracksDialog->exec() == QDialog::Accepted) {
1119     int nr = m_numberTracksDialog->getStartNumber();
1120     bool totalEnabled;
1121     int total = m_numberTracksDialog->getTotalNumberOfTracks(&totalEnabled);
1122     if (!totalEnabled)
1123       total = 0;
1124     TagConfig::instance().setEnableTotalNumberOfTracks(totalEnabled);
1125     Kid3Application::NumberTrackOptions options;
1126     if (m_numberTracksDialog->isTrackNumberingEnabled())
1127       options |= Kid3Application::NumberTracksEnabled;
1128     if (m_numberTracksDialog->isDirectoryCounterResetEnabled())
1129       options |= Kid3Application::NumberTracksResetCounterForEachDirectory;
1130     m_app->numberTracks(nr, total, m_numberTracksDialog->getDestination(),
1131                         options);
1132   }
1133 }
1134 
1135 /**
1136  * Filter.
1137  */
slotFilter()1138 void BaseMainWindowImpl::slotFilter()
1139 {
1140   if (saveModified()) {
1141     if (!m_filterDialog) {
1142       m_filterDialog.reset(new FilterDialog(m_w));
1143       connect(m_filterDialog.data(), &FilterDialog::apply,
1144               m_app, static_cast<void (Kid3Application::*)(FileFilter&)>(
1145                 &Kid3Application::applyFilter));
1146       connect(m_app, &Kid3Application::fileFiltered,
1147               m_filterDialog.data(), &FilterDialog::showFilterEvent);
1148       connect(m_app, &Kid3Application::fileFiltered,
1149               this, &BaseMainWindowImpl::filterProgress);
1150     }
1151     FilterConfig::instance().setFilenameFormat(
1152           FileConfig::instance().toFilenameFormat());
1153     m_filterDialog->readConfig();
1154     m_filterDialog->show();
1155   }
1156 }
1157 
1158 /**
1159  * Show filter operation progress.
1160  * @param type filter event type
1161  * @param fileName name of file processed
1162  * @param passed number of files which passed the filter
1163  * @param total total number of files checked
1164  */
filterProgress(int type,const QString & fileName,int passed,int total)1165 void BaseMainWindowImpl::filterProgress(int type, const QString& fileName,
1166                                         int passed, int total)
1167 {
1168   Q_UNUSED(fileName)
1169   switch (type) {
1170   case FileFilter::Started:
1171     startProgressMonitoring(tr("Filter"), &BaseMainWindowImpl::terminateFilter,
1172                             true);
1173     break;
1174   case FileFilter::Finished:
1175   case FileFilter::Aborted:
1176     stopProgressMonitoring();
1177     break;
1178   default:
1179     checkProgressMonitoring(0, 0, QString::number(passed) +
1180                             QLatin1Char('/') + QString::number(total));
1181   }
1182 }
1183 
1184 /**
1185  * Terminate filtering the file list.
1186  */
terminateFilter()1187 void BaseMainWindowImpl::terminateFilter()
1188 {
1189   if (m_filterDialog) {
1190     m_filterDialog->abort();
1191   }
1192 }
1193 
1194 /**
1195  * Play audio file.
1196  */
slotPlayAudio()1197 void BaseMainWindowImpl::slotPlayAudio()
1198 {
1199   m_app->playAudio();
1200 }
1201 
1202 /**
1203  * Show play tool bar.
1204  */
showPlayToolBar()1205 void BaseMainWindowImpl::showPlayToolBar()
1206 {
1207 #ifdef HAVE_QTMULTIMEDIA
1208   if (!m_playToolBar) {
1209     if (AudioPlayer* player =
1210         qobject_cast<AudioPlayer*>(m_app->getAudioPlayer())) {
1211       m_playToolBar = new PlayToolBar(player, m_w);
1212       m_playToolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
1213       m_w->addToolBar(Qt::BottomToolBarArea, m_playToolBar);
1214       connect(m_playToolBar, &PlayToolBar::errorMessage,
1215               this, &BaseMainWindowImpl::slotStatusMsg);
1216 #ifdef HAVE_QTDBUS
1217       connect(m_playToolBar, &PlayToolBar::closed,
1218               m_app, &Kid3Application::deactivateMprisInterface);
1219 #endif
1220 #ifdef Q_OS_WIN32
1221       // Phonon on Windows cannot play if the file is open.
1222       connect(m_playToolBar, &PlayToolBar::aboutToPlay,
1223               m_app, &Kid3Application::closeFileHandle);
1224 #endif
1225     }
1226   }
1227   m_playToolBar->show();
1228 #endif
1229 }
1230 
1231 /**
1232  * Set window title with information from directory, filter and modification
1233  * state.
1234  */
updateWindowCaption()1235 void BaseMainWindowImpl::updateWindowCaption()
1236 {
1237   QString cap;
1238   if (!m_app->getDirName().isEmpty()) {
1239     cap += QDir(m_app->getDirName()).dirName();
1240   }
1241   if (m_app->isFiltered()) {
1242     cap += tr(" [filtered %1/%2]")
1243         .arg(m_app->filterPassedCount()).arg(m_app->filterTotalCount());
1244   }
1245   m_self->setWindowCaption(cap, m_app->isModified());
1246 }
1247 
1248 /**
1249  * Update files of current selection.
1250  */
updateCurrentSelection()1251 void BaseMainWindowImpl::updateCurrentSelection()
1252 {
1253   TaggedFileSelection* selection = m_app->selectionInfo();
1254   if (!selection->isEmpty()) {
1255     FOR_ALL_TAGS(tagNr) {
1256       m_form->frameTable(tagNr)->acceptEdit();
1257     }
1258     m_app->frameModelsToTags();
1259 
1260     selection->setFilename(m_form->getFilename());
1261   }
1262 }
1263 
1264 /**
1265  * Apply selection change and update GUI controls.
1266  * The new selection is stored and the GUI controls and frame list
1267  * updated accordingly (filtered for multiple selection).
1268  * @param selected selected items
1269  * @param deselected deselected items
1270  */
applySelectionChange(const QItemSelection & selected,const QItemSelection & deselected)1271 void BaseMainWindowImpl::applySelectionChange(const QItemSelection& selected,
1272                                               const QItemSelection& deselected)
1273 {
1274   if (!deselected.isEmpty()) {
1275     m_app->tagsToFrameModels();
1276   } else {
1277     m_app->selectedTagsToFrameModels(selected);
1278   }
1279   updateGuiControlsFromSelection();
1280 }
1281 
1282 /**
1283  * Update GUI controls from the tags in the files.
1284  * The new selection is stored and the GUI controls and frame list
1285  * updated accordingly (filtered for multiple selection).
1286  */
updateGuiControls()1287 void BaseMainWindowImpl::updateGuiControls()
1288 {
1289   m_app->tagsToFrameModels();
1290   updateGuiControlsFromSelection();
1291 }
1292 
1293 /**
1294  * Update GUI controls from the current selection.
1295  */
updateGuiControlsFromSelection()1296 void BaseMainWindowImpl::updateGuiControlsFromSelection()
1297 {
1298   TaggedFileSelection* selection = m_app->selectionInfo();
1299   m_form->setFilename(selection->getFilename());
1300   m_form->setFilenameEditEnabled(selection->isSingleFileSelected());
1301   m_form->setDetailInfo(selection->getDetailInfo());
1302   FOR_ALL_TAGS(tagNr) {
1303     m_form->setTagFormat(tagNr, selection->getTagFormat(tagNr));
1304   }
1305   if (FileConfig::instance().markChanges()) {
1306     m_form->markChangedFilename(selection->isFilenameChanged());
1307   }
1308 
1309   if (!GuiConfig::instance().hidePicture()) {
1310     m_form->setPictureData(selection->getPicture());
1311   }
1312 
1313   bool selectionEmpty = selection->isEmpty();
1314   bool autoHideTags = GuiConfig::instance().autoHideTags();
1315   FOR_ALL_TAGS(tagNr) {
1316     m_form->enableControls(tagNr,
1317         selection->isTagUsed(tagNr) || selectionEmpty);
1318 
1319     if (autoHideTags) {
1320       m_form->hideTag(tagNr, !selection->hasTag(tagNr));
1321     }
1322   }
1323 }
1324 
1325 /**
1326  * Update ID3v2 tags in GUI controls from file displayed in frame list.
1327  *
1328  * @param taggedFile the selected file
1329  * @param tagNr tag number
1330  */
updateAfterFrameModification(TaggedFile * taggedFile,Frame::TagNumber tagNr)1331 void BaseMainWindowImpl::updateAfterFrameModification(TaggedFile* taggedFile,
1332                                                       Frame::TagNumber tagNr)
1333 {
1334   if (taggedFile) {
1335     FrameCollection frames;
1336     taggedFile->getAllFrames(tagNr, frames);
1337     m_app->frameModel(tagNr)->transferFrames(frames);
1338   }
1339 }
1340 
1341 /**
1342  * Let user select a frame type.
1343  * frameSelected() is emitted when the edit dialog is closed with the selected
1344  * frame as a parameter if a frame is selected.
1345  *
1346  * @param frame is filled with the selected frame
1347  * @param taggedFile tagged file for which frame has to be selected
1348  */
selectFrame(Frame * frame,const TaggedFile * taggedFile)1349 void BaseMainWindowImpl::selectFrame(Frame* frame, const TaggedFile* taggedFile)
1350 {
1351   bool ok = false;
1352   if (taggedFile && frame) {
1353     QStringList frameIds = taggedFile->getFrameIds(m_editFrameTagNr);
1354     QMap<QString, QString> nameMap = Frame::getDisplayNameMap(frameIds);
1355     QString displayName = QInputDialog::getItem(
1356       m_w, tr("Add Frame"),
1357       tr("Select the frame ID"), nameMap.keys(), 0, true, &ok);
1358     if (ok) {
1359       if (displayName.startsWith(QLatin1Char('!'))) {
1360         QString name = displayName.mid(1);
1361         Frame::ExtendedType type(Frame::FT_Other, name);
1362         *frame = Frame(type, QLatin1String(""), -1);
1363       } else {
1364         QString name = nameMap.value(displayName, displayName);
1365         Frame::Type type = Frame::getTypeFromName(name);
1366         *frame = Frame(type, QLatin1String(""), name, -1);
1367       }
1368     }
1369   }
1370   emit frameSelected(m_editFrameTagNr, ok ? frame : nullptr);
1371 }
1372 
1373 /**
1374  * Return object which emits frameSelected(), frameEdited() signals.
1375  *
1376  * @return object which emits signals.
1377  */
qobject()1378 QObject* BaseMainWindowImpl::qobject()
1379 {
1380   return this;
1381 }
1382 
1383 /**
1384  * Get the tag number of the edited frame.
1385  * @return tag number.
1386  */
tagNumber() const1387 Frame::TagNumber BaseMainWindowImpl::tagNumber() const
1388 {
1389   return m_editFrameTagNr;
1390 }
1391 
1392 /**
1393  * Set the tag number of the edited frame.
1394  * @param tagNr tag number
1395  */
setTagNumber(Frame::TagNumber tagNr)1396 void BaseMainWindowImpl::setTagNumber(Frame::TagNumber tagNr)
1397 {
1398   m_editFrameTagNr = tagNr;
1399 }
1400 
1401 /**
1402  * Create dialog to edit a frame and update the fields
1403  * if Ok is returned.
1404  * frameEdited() is emitted when the edit dialog is closed with the edited
1405  * frame as a parameter if it was accepted.
1406  *
1407  * @param frame frame to edit
1408  * @param taggedFile tagged file where frame has to be set
1409  */
editFrameOfTaggedFile(const Frame * frame,TaggedFile * taggedFile)1410 void BaseMainWindowImpl::editFrameOfTaggedFile(const Frame* frame,
1411                                                TaggedFile* taggedFile)
1412 {
1413   if (!frame || !taggedFile) {
1414     emit frameEdited(m_editFrameTagNr, nullptr);
1415     return;
1416   }
1417 
1418   m_editFrame = *frame;
1419   m_editFrameTaggedFile = taggedFile;
1420   QString name(m_editFrame.getInternalName());
1421   if (name.isEmpty()) {
1422     name = m_editFrame.getName();
1423   }
1424   if (!name.isEmpty()) {
1425     int nlPos = name.indexOf(QLatin1Char('\n'));
1426     if (nlPos > 0) {
1427       // probably "TXXX - User defined text information\nDescription" or
1428       // "WXXX - User defined URL link\nDescription"
1429       name.truncate(nlPos);
1430     }
1431     name = QCoreApplication::translate("@default", name.toLatin1().data());
1432   }
1433   if (!m_editFrameDialog) {
1434     m_editFrameDialog = new EditFrameFieldsDialog(m_platformTools, m_app, m_w);
1435     connect(m_editFrameDialog, &QDialog::finished,
1436             this, &BaseMainWindowImpl::onEditFrameDialogFinished);
1437   }
1438   m_editFrameDialog->setWindowTitle(name);
1439   m_editFrameDialog->setFrame(m_editFrame, m_editFrameTaggedFile,
1440                               m_editFrameTagNr);
1441   m_editFrameDialog->show();
1442 }
1443 
1444 /**
1445  * Called when the edit fram dialog is finished.
1446  * @param result dialog result
1447  */
onEditFrameDialogFinished(int result)1448 void BaseMainWindowImpl::onEditFrameDialogFinished(int result)
1449 {
1450   if (auto dialog =
1451       qobject_cast<EditFrameFieldsDialog*>(sender())) {
1452     if (result == QDialog::Accepted) {
1453       const Frame::FieldList& fields = dialog->getUpdatedFieldList();
1454       if (fields.isEmpty()) {
1455         m_editFrame.setValue(dialog->getFrameValue());
1456       } else {
1457         m_editFrame.setFieldList(fields);
1458         m_editFrame.setValueFromFieldList();
1459       }
1460       if (m_editFrameTaggedFile->setFrame(m_editFrameTagNr, m_editFrame)) {
1461         m_editFrameTaggedFile->markTagChanged(m_editFrameTagNr,
1462                                               m_editFrame.getType());
1463       }
1464     }
1465   }
1466   emit frameEdited(m_editFrameTagNr,
1467                    result == QDialog::Accepted ? &m_editFrame : nullptr);
1468 }
1469 
1470 /**
1471  * Rename the selected file(s).
1472  */
renameFile()1473 void BaseMainWindowImpl::renameFile()
1474 {
1475   QItemSelectionModel* selectModel = m_app->getFileSelectionModel();
1476   auto model =
1477       qobject_cast<FileProxyModel*>(m_form->getFileList()->model());
1478   if (!selectModel || !model)
1479     return;
1480 
1481   QList<QPersistentModelIndex> selItems;
1482   const auto indexes = selectModel->selectedRows();
1483   selItems.reserve(indexes.size());
1484   for (const QModelIndex& index : indexes)
1485     selItems.append(index);
1486   const auto selectedIndexes = selItems;
1487   for (const QPersistentModelIndex& index : selectedIndexes) {
1488     TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index);
1489     QString absFilename, dirName, fileName;
1490     if (taggedFile) {
1491       absFilename = taggedFile->getAbsFilename();
1492       dirName = taggedFile->getDirname();
1493       fileName = taggedFile->getFilename();
1494     } else {
1495       QFileInfo fi(model->fileInfo(index));
1496       absFilename = fi.filePath();
1497       dirName = fi.dir().path();
1498       fileName = fi.fileName();
1499     }
1500     bool ok;
1501     QString newFileName = QInputDialog::getText(
1502       m_w,
1503       tr("Rename File"),
1504       tr("Enter new file name:"),
1505       QLineEdit::Normal, fileName, &ok);
1506     if (ok && !newFileName.isEmpty() && newFileName != fileName) {
1507       if (taggedFile) {
1508         if (taggedFile->isChanged()) {
1509           taggedFile->setFilename(newFileName);
1510           if (selItems.size() == 1)
1511             m_form->setFilename(newFileName);
1512           continue;
1513         }
1514         // This will close the file.
1515         // The file must be closed before renaming on Windows.
1516         taggedFile->closeFileHandle();
1517       } else if (model->isDir(index)) {
1518         // The directory must be closed before renaming on Windows.
1519         TaggedFileIterator::closeFileHandles(index);
1520       }
1521       QString newPath = dirName + QLatin1Char('/') + newFileName;
1522       bool ok = model->rename(index, newFileName);
1523       if (!ok && !(index.flags() & Qt::ItemIsEditable)) {
1524         // The file system model seems to be too restrictive, renaming without
1525         // write permission is possible on Linux, so try again without
1526         // using the model.
1527         ok = QFile::rename(absFilename, newPath);
1528       }
1529       if (ok) {
1530         if (taggedFile) {
1531           taggedFile->updateCurrentFilename();
1532           if (selItems.size() == 1) {
1533             m_form->setFilename(newFileName);
1534           }
1535         }
1536       } else {
1537 #ifdef Q_OS_WIN32
1538         if (QMessageBox::warning(
1539               0, tr("File Error"),
1540               tr("Error while renaming:\n") +
1541               tr("Rename %1 to %2 failed\n").arg(fileName).arg(newFileName) +
1542               tr("Retry after closing folders?"),
1543               QMessageBox::Ok, QMessageBox::Cancel) == QMessageBox::Ok) {
1544           m_app->tryRenameAfterReset(absFilename, newPath);
1545         }
1546 #else
1547         QMessageBox::warning(
1548           nullptr, tr("File Error"),
1549           tr("Error while renaming:\n") +
1550           tr("Rename %1 to %2 failed\n").arg(fileName, newFileName),
1551           QMessageBox::Ok, Qt::NoButton);
1552 #endif
1553       }
1554     }
1555   }
1556 }
1557 
1558 /** Only defined for generation of translation files */
1559 #define WANT_TO_DELETE_FOR_PO \
1560   QT_TRANSLATE_NOOP("@default", "Do you really want to move these %1 items to the trash?")
1561 
1562 /**
1563  * Delete the selected file(s).
1564  */
deleteFile()1565 void BaseMainWindowImpl::deleteFile()
1566 {
1567   QItemSelectionModel* selectModel = m_app->getFileSelectionModel();
1568   auto model =
1569       qobject_cast<FileProxyModel*>(m_form->getFileList()->model());
1570   if (!selectModel || !model)
1571     return;
1572 
1573   QStringList files;
1574   QList<QPersistentModelIndex> selItems;
1575   const auto indexes = selectModel->selectedRows();
1576   selItems.reserve(indexes.size());
1577   for (const QModelIndex& index : indexes)
1578     selItems.append(index);
1579   const auto selectedIndexes = selItems;
1580   for (const QPersistentModelIndex& index : selectedIndexes) {
1581     files.append(model->filePath(index)); // clazy:exclude=reserve-candidates
1582   }
1583 
1584   const int numFiles = files.size();
1585   if (numFiles > 0) {
1586     if (m_platformTools->warningContinueCancelList(
1587           m_w,
1588           numFiles > 1
1589           ? tr("Do you really want to move these %1 items to the trash?")
1590             .arg(numFiles)
1591           : tr("Do you really want to move this item to the trash?"),
1592           files,
1593           tr("Move to Trash"))) {
1594       bool rmdirError = false;
1595       files.clear();
1596       for (const QPersistentModelIndex& index : selectedIndexes) {
1597         QString absFilename(model->filePath(index));
1598         if (!QFileInfo(absFilename).isWritable()) {
1599           QFile::setPermissions(absFilename,
1600               QFile::permissions(absFilename) | QFile::WriteUser);
1601         }
1602         if (model->isDir(index)) {
1603           if (!m_platformTools->moveToTrash(absFilename)) {
1604             rmdirError = true;
1605             files.append(absFilename); // clazy:exclude=reserve-candidates
1606           }
1607         } else {
1608           if (TaggedFile* taggedFile =
1609               FileProxyModel::getTaggedFileOfIndex(index)) {
1610             // This will close the file.
1611             // The file must be closed before deleting on Windows.
1612             taggedFile->closeFileHandle();
1613           }
1614           if (!m_platformTools->moveToTrash(absFilename)) {
1615             files.append(absFilename); // clazy:exclude=reserve-candidates
1616           }
1617         }
1618       }
1619       if (!files.isEmpty()) {
1620         QString txt;
1621         if (rmdirError)
1622           txt += tr("Folder must be empty.\n");
1623         txt += tr("Could not move these files to the Trash");
1624         m_platformTools->errorList(m_w, txt, files, tr("File Error"));
1625       }
1626     }
1627   }
1628 }
1629 
1630 /**
1631  * Toggle expanded state of directory in the file list.
1632  * @param index index of directory
1633  */
toggleExpanded(const QModelIndex & index)1634 void BaseMainWindowImpl::toggleExpanded(const QModelIndex& index)
1635 {
1636   QTreeView* fileList = m_form->getFileList();
1637   fileList->setExpanded(index, !fileList->isExpanded(index));
1638 }
1639 
1640 /**
1641  * Expand the file list.
1642  */
expandFileList()1643 void BaseMainWindowImpl::expandFileList()
1644 {
1645   m_expandNotificationNeeded = sender() == m_app;
1646   connect(m_app->getFileProxyModelIterator(),
1647           &FileProxyModelIterator::nextReady,
1648           this, &BaseMainWindowImpl::expandNextDirectory);
1649   // If this slot is invoked from the file list menu action and the
1650   // shift key is pressed, only expand the current subtree.
1651   QObject* emitter = sender();
1652   bool sentFromAction =
1653       emitter && emitter->metaObject() == &QAction::staticMetaObject;
1654   bool expandOnlySubtree =
1655       sentFromAction && QApplication::keyboardModifiers() == Qt::ShiftModifier;
1656   startProgressMonitoring(tr("Expand All"),
1657                           &BaseMainWindowImpl::terminateExpandFileList,
1658                           !expandOnlySubtree);
1659   m_app->getFileProxyModelIterator()->start(expandOnlySubtree
1660         ? m_form->getFileList()->currentIndex()
1661         : m_form->getFileList()->rootIndex());
1662 }
1663 
1664 /**
1665  * Expand item if it is a directory.
1666  *
1667  * @param index index of file in file proxy model
1668  */
expandNextDirectory(const QPersistentModelIndex & index)1669 void BaseMainWindowImpl::expandNextDirectory(const QPersistentModelIndex& index)
1670 {
1671   if (index.isValid()) {
1672     if (m_app->getFileProxyModel()->isDir(index)) {
1673       m_form->getFileList()->expand(index);
1674     }
1675     int done = m_app->getFileProxyModelIterator()->getWorkDone();
1676     int total = m_app->getFileProxyModelIterator()->getWorkToDo() + done;
1677     checkProgressMonitoring(done, total, QString());
1678   } else {
1679     stopProgressMonitoring();
1680   }
1681 }
1682 
1683 /**
1684  * Terminate expanding the file list.
1685  */
terminateExpandFileList()1686 void BaseMainWindowImpl::terminateExpandFileList()
1687 {
1688   m_app->getFileProxyModelIterator()->abort();
1689   disconnect(m_app->getFileProxyModelIterator(),
1690              &FileProxyModelIterator::nextReady,
1691              this, &BaseMainWindowImpl::expandNextDirectory);
1692   if (m_expandNotificationNeeded) {
1693     m_expandNotificationNeeded = false;
1694     m_app->notifyExpandFileListFinished();
1695   }
1696 }
1697 
1698 /**
1699  * Start monitoring the progress of a possibly long operation.
1700  *
1701  * If the operation takes longer than 3 seconds, a progress widget is shown.
1702  *
1703  * @param title title to be displayed in progress widget
1704  * @param terminationHandler method to be called to terminate operation
1705  * @param disconnectModel true to disconnect the file list models while the
1706  * progress widget is shown
1707  */
startProgressMonitoring(const QString & title,void (BaseMainWindowImpl::* terminationHandler)(),bool disconnectModel)1708 void BaseMainWindowImpl::startProgressMonitoring(
1709     const QString& title, void (BaseMainWindowImpl::*terminationHandler)(),
1710     bool disconnectModel)
1711 {
1712   if (!m_progressTitle.isEmpty() && m_progressTitle != title) {
1713     stopProgressMonitoring();
1714   }
1715   m_progressTitle = title;
1716   m_progressTerminationHandler = terminationHandler;
1717   m_progressDisconnected = disconnectModel;
1718   m_progressStartTime = QDateTime::currentDateTime();
1719 }
1720 
1721 /**
1722  * Start monitoring the progress started with startProgressMonitoring().
1723  */
stopProgressMonitoring()1724 void BaseMainWindowImpl::stopProgressMonitoring()
1725 {
1726   if (m_progressWidget) {
1727     m_form->removeLeftSideWidget(m_progressWidget);
1728     m_progressWidget->reset();
1729     if (m_progressDisconnected) {
1730       m_form->getDirList()->reconnectModel();
1731       m_form->getFileList()->reconnectModel();
1732       m_form->getFileList()->expandAll();
1733     }
1734   }
1735   if (m_progressTerminationHandler) {
1736     (this->*m_progressTerminationHandler)();
1737   }
1738   m_progressTitle.clear();
1739   m_progressTerminationHandler = nullptr;
1740 }
1741 
1742 /**
1743  * Check progress of a possibly long operation.
1744  *
1745  * Progress monitoring is started with startProgressMonitoring(). This method
1746  * will check if the opeation is running long enough to show a progress widget
1747  * and update the progress information. It will call stopProgressMonitoring()
1748  * when the operation is aborted.
1749  *
1750  * @param done amount of work done
1751  * @param total total amount of work
1752  * @param text text for progress label
1753  */
checkProgressMonitoring(int done,int total,const QString & text)1754 void BaseMainWindowImpl::checkProgressMonitoring(int done, int total,
1755                                                  const QString& text)
1756 {
1757   if (m_progressStartTime.isValid() &&
1758       m_progressStartTime.secsTo(QDateTime::currentDateTime()) >= 3) {
1759     // Operation is taking some time, show dialog to abort it.
1760     m_progressStartTime = QDateTime();
1761     if (!m_progressWidget) {
1762       m_progressWidget = new ProgressWidget(m_w);
1763     }
1764     m_progressWidget->setWindowTitle(m_progressTitle);
1765     m_progressWidget->setLabelText(QString());
1766     m_progressWidget->setCancelButtonText(tr("A&bort"));
1767     m_progressWidget->setMinimum(0);
1768     m_progressWidget->setMaximum(0);
1769     m_form->setLeftSideWidget(m_progressWidget);
1770     if (m_progressDisconnected) {
1771       m_form->getFileList()->disconnectModel();
1772       m_form->getDirList()->disconnectModel();
1773     }
1774   }
1775   if (m_progressWidget) {
1776     m_progressWidget->setValueAndMaximum(done, total);
1777     m_progressWidget->setLabelText(text);
1778     if (m_progressWidget->wasCanceled()) {
1779       stopProgressMonitoring();
1780     }
1781   }
1782 }
1783 
1784 
1785 /**
1786  * Constructor.
1787  *
1788  * @param mainWin main window
1789  * @param platformTools platform specific tools
1790  * @param app application context
1791  */
BaseMainWindow(QMainWindow * mainWin,IPlatformTools * platformTools,Kid3Application * app)1792 BaseMainWindow::BaseMainWindow(QMainWindow* mainWin,
1793                                IPlatformTools* platformTools,
1794                                Kid3Application* app) :
1795   m_impl(new BaseMainWindowImpl(mainWin, platformTools, app))
1796 {
1797   m_impl->setBackPointer(this);
1798 }
1799 
1800 /**
1801  * Destructor.
1802  */
~BaseMainWindow()1803 BaseMainWindow::~BaseMainWindow()
1804 {
1805   // Must not be inline because of forwared declared QScopedPointer.
1806 }
1807 
1808 /**
1809  * Initialize main window.
1810  * Shall be called at end of constructor body in derived classes.
1811  */
init()1812 void BaseMainWindow::init()
1813 {
1814   m_impl->init();
1815 }
1816 
1817 /**
1818  * Play audio file.
1819  */
slotPlayAudio()1820 void BaseMainWindow::slotPlayAudio()
1821 {
1822   m_impl->slotPlayAudio();
1823 }
1824 
1825 /**
1826  * Change visibility of status bar.
1827  * @param visible true to show status bar
1828  */
setStatusBarVisible(bool visible)1829 void BaseMainWindow::setStatusBarVisible(bool visible)
1830 {
1831   m_impl->setStatusBarVisible(visible);
1832 }
1833 
1834 /**
1835  * Update files of current selection.
1836  */
updateCurrentSelection()1837 void BaseMainWindow::updateCurrentSelection()
1838 {
1839   m_impl->updateCurrentSelection();
1840 }
1841 
1842 /**
1843  * Open directory, user has to confirm if current directory modified.
1844  *
1845  * @param paths directory or file paths
1846  */
confirmedOpenDirectory(const QStringList & paths)1847 void BaseMainWindow::confirmedOpenDirectory(const QStringList& paths)
1848 {
1849   m_impl->confirmedOpenDirectory(paths);
1850 }
1851 
1852 /**
1853  * Update modification state before closing.
1854  * If anything was modified, save after asking user.
1855  * Save options before closing.
1856  * This method shall be called by closeEvent() (Qt) or
1857  * queryClose() (KDE).
1858  *
1859  * @return false if user canceled,
1860  *         true will quit the application.
1861  */
queryBeforeClosing()1862 bool BaseMainWindow::queryBeforeClosing()
1863 {
1864   return m_impl->queryBeforeClosing();
1865 }
1866 
1867 /**
1868  * Open recent directory.
1869  *
1870  * @param dir directory to open
1871  */
openRecentDirectory(const QString & dir)1872 void BaseMainWindow::openRecentDirectory(const QString& dir)
1873 {
1874   m_impl->openRecentDirectory(dir);
1875 }
1876 
1877 /**
1878  * Set window title with information from directory, filter and modification
1879  * state.
1880  */
updateWindowCaption()1881 void BaseMainWindow::updateWindowCaption()
1882 {
1883   m_impl->updateWindowCaption();
1884 }
1885 
1886 /**
1887  * Access to application.
1888  * @return application.
1889  */
app()1890 Kid3Application* BaseMainWindow::app()
1891 {
1892   return m_impl->app();
1893 }
1894 
1895 /**
1896  * Access to main form.
1897  * @return main form.
1898  */
form()1899 Kid3Form* BaseMainWindow::form()
1900 {
1901   return m_impl->form();
1902 }
1903