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