1 #include "common/common_pch.h"
2 
3 #include "common/debugging.h"
4 #include "common/qt.h"
5 #include "mkvtoolnix-gui/jobs/job.h"
6 #include "mkvtoolnix-gui/jobs/mux_job.h"
7 #include "mkvtoolnix-gui/jobs/tool.h"
8 #include "mkvtoolnix-gui/main_window/main_window.h"
9 #include "mkvtoolnix-gui/merge/command_line_dialog.h"
10 #include "mkvtoolnix-gui/merge/tab.h"
11 #include "mkvtoolnix-gui/merge/tab_p.h"
12 #include "mkvtoolnix-gui/merge/tool.h"
13 #include "mkvtoolnix-gui/forms/main_window/main_window.h"
14 #include "mkvtoolnix-gui/forms/merge/tab.h"
15 #include "mkvtoolnix-gui/util/file.h"
16 #include "mkvtoolnix-gui/util/file_dialog.h"
17 #include "mkvtoolnix-gui/util/message_box.h"
18 #include "mkvtoolnix-gui/util/option_file.h"
19 #include "mkvtoolnix-gui/util/settings.h"
20 #include "mkvtoolnix-gui/util/widget.h"
21 #include "mkvtoolnix-gui/watch_jobs/tool.h"
22 
23 #include <QComboBox>
24 #include <QDebug>
25 #include <QDir>
26 #include <QMenu>
27 #include <QTreeView>
28 #include <QInputDialog>
29 #include <QMessageBox>
30 #include <QString>
31 #include <QTimer>
32 
33 namespace mtx::gui::Merge {
34 
35 using namespace mtx::gui;
36 
TabPrivate(QWidget * parent)37 TabPrivate::TabPrivate(QWidget *parent)
38   : ui{new Ui::Tab}
39   , filesModel{new SourceFileModel{parent}}
40   , tracksModel{new TrackModel{parent}}
41   , currentlySettingInputControlValues{false}
42   , addFilesAction{new QAction{parent}}
43   , appendFilesAction{new QAction{parent}}
44   , addAdditionalPartsAction{new QAction{parent}}
45   , addFilesAction2{new QAction{parent}}
46   , appendFilesAction2{new QAction{parent}}
47   , addAdditionalPartsAction2{new QAction{parent}}
48   , removeFilesAction{new QAction{parent}}
49   , removeAllFilesAction{new QAction{parent}}
50   , setDestinationFileNameAction{new QAction{parent}}
51   , selectAllTracksAction{new QAction{parent}}
52   , enableAllTracksAction{new QAction{parent}}
53   , disableAllTracksAction{new QAction{parent}}
54   , selectAllVideoTracksAction{new QAction{parent}}
55   , selectAllAudioTracksAction{new QAction{parent}}
56   , selectAllSubtitlesTracksAction{new QAction{parent}}
57   , openFilesInMediaInfoAction{new QAction{parent}}
58   , openTracksInMediaInfoAction{new QAction{parent}}
59   , selectTracksFromFilesAction{new QAction{parent}}
60   , enableAllAttachedFilesAction{new QAction{parent}}
61   , disableAllAttachedFilesAction{new QAction{parent}}
62   , enableSelectedAttachedFilesAction{new QAction{parent}}
63   , disableSelectedAttachedFilesAction{new QAction{parent}}
64   , startMuxingLeaveAsIs{new QAction{parent}}
65   , startMuxingCreateNewSettings{new QAction{parent}}
66   , startMuxingCloseSettings{new QAction{parent}}
67   , startMuxingRemoveInputFiles{new QAction{parent}}
68   , addToJobQueueLeaveAsIs{new QAction{parent}}
69   , addToJobQueueCreateNewSettings{new QAction{parent}}
70   , addToJobQueueCloseSettings{new QAction{parent}}
71   , addToJobQueueRemoveInputFiles{new QAction{parent}}
72   , filesMenu{new QMenu{parent}}
73   , tracksMenu{new QMenu{parent}}
74   , attachedFilesMenu{new QMenu{parent}}
75   , attachmentsMenu{new QMenu{parent}}
76   , selectTracksOfTypeMenu{new QMenu{parent}}
77   , addFilesMenu{new QMenu{parent}}
78   , startMuxingMenu{new QMenu{parent}}
79   , addToJobQueueMenu{new QMenu{parent}}
80   , attachedFilesModel{new AttachedFileModel{parent}}
81   , attachmentsModel{new AttachmentModel{parent}}
82   , addAttachmentsAction{new QAction{parent}}
83   , removeAttachmentsAction{new QAction{parent}}
84   , removeAllAttachmentsAction{new QAction{parent}}
85   , selectAllAttachmentsAction{new QAction{parent}}
86 {
87 }
88 
89 // ------------------------------------------------------------
90 
Tab(QWidget * parent)91 Tab::Tab(QWidget *parent)
92   : QWidget{parent}
93   , p_ptr{new TabPrivate{this}}
94 {
95   auto &p = *p_func();
96 
97   // Setup UI controls.
98   p.ui->setupUi(this);
99 
100   auto mw = MainWindow::get();
__anon7f2be8a60102() 101   connect(mw, &MainWindow::preferencesChanged, this, [this]() { Util::setupTabWidgetHeaders(*p_func()->ui->tabs); });
102 
103   p.filesModel->setOtherModels(p.tracksModel, p.attachedFilesModel);
104 
105   setupInputControls();
106   setupOutputControls();
107   setupAttachmentsControls();
108 
109   setControlValuesFromConfig();
110 
111   Util::setupTabWidgetHeaders(*p.ui->tabs);
112 
113   retranslateUi();
114 
115   Util::fixScrollAreaBackground(p.ui->propertiesScrollArea);
116   Util::preventScrollingWithoutFocus(this);
117 
118   p.savedState = currentState();
119   p.emptyState = p.savedState;
120 
121   p.ui->files->setIconSize({ 28, 16 });
122 
123   p.ui->trackLanguage->registerBuddyLabel(*p.ui->trackLanguageLabel);
124   p.ui->chapterLanguage->registerBuddyLabel(*p.ui->chapterLanguageLabel);
125 }
126 
~Tab()127 Tab::~Tab() {
128 }
129 
130 QString const &
fileName() const131 Tab::fileName()
132   const {
133   return p_func()->config.m_configFileName;
134 }
135 
136 QString
title() const137 Tab::title()
138   const {
139   auto &p    = *p_func();
140   auto title = p.config.m_destination.isEmpty() ? QY("<No destination file>") : QFileInfo{p.config.m_destination}.fileName();
141   if (!p.config.m_configFileName.isEmpty())
142     title = Q("%1 (%2)").arg(title).arg(QFileInfo{p.config.m_configFileName}.fileName());
143 
144   return title;
145 }
146 
147 void
onShowCommandLine()148 Tab::onShowCommandLine() {
149   auto exe     = Util::Settings::get().actualMkvmergeExe();
150   auto options = updateConfigFromControlValues().buildMkvmergeOptions().setExecutable(exe);
151 
152   CommandLineDialog{this, options, QY("mkvmerge command line")}.exec();
153 }
154 
155 void
load(QString const & fileName)156 Tab::load(QString const &fileName) {
157   auto &p = *p_func();
158 
159   try {
160     if (Util::ConfigFile::determineType(fileName) == Util::ConfigFile::UnknownType)
161       throw InvalidSettingsX{};
162 
163     p.config.load(fileName);
164     setControlValuesFromConfig();
165 
166     p.savedState = currentState();
167 
168     MainWindow::get()->setStatusBarMessage(QY("The configuration has been loaded."));
169 
170     Q_EMIT titleChanged();
171 
172   } catch (InvalidSettingsX &) {
173     p.config.reset();
174 
175     Util::MessageBox::critical(this)->title(QY("Error loading settings file")).text(QY("The settings file '%1' contains invalid settings and was not loaded.").arg(fileName)).exec();
176 
177     Q_EMIT removeThisTab();
178   }
179 }
180 
181 void
cloneConfig(MuxConfig const & config)182 Tab::cloneConfig(MuxConfig const &config) {
183   auto &p  = *p_func();
184   p.config = config;
185 
186   setControlValuesFromConfig();
187 
188   p.config.m_configFileName.clear();
189   p.savedState.clear();
190 
191   Q_EMIT titleChanged();
192 }
193 
194 void
onSaveConfig()195 Tab::onSaveConfig() {
196   auto &p = *p_func();
197 
198   if (p.config.m_configFileName.isEmpty()) {
199     onSaveConfigAs();
200     return;
201   }
202 
203   updateConfigFromControlValues();
204   p.config.save();
205 
206   p.savedState = currentState();
207 
208   MainWindow::get()->setStatusBarMessage(QY("The configuration has been saved."));
209 }
210 
211 void
onSaveOptionFile()212 Tab::onSaveOptionFile() {
213   auto &settings = Util::Settings::get();
214   auto fileName  = Util::getSaveFileName(this, QY("Save option file"), settings.m_lastConfigDir.path(), defaultFileNameForSaving(Q(".json")), QY("MKVToolNix option files (JSON-formatted)") + Q(" (*.json);;") + QY("All files") + Q(" (*)"), Q("json"));
215   if (fileName.isEmpty())
216     return;
217 
218   Util::OptionFile::create(fileName, updateConfigFromControlValues().buildMkvmergeOptions().effectiveOptions(Util::EscapeJSON));
219   settings.m_lastConfigDir.setPath(QFileInfo{fileName}.path());
220   settings.save();
221 
222   MainWindow::get()->setStatusBarMessage(QY("The option file has been created."));
223 }
224 
225 void
onSaveConfigAs()226 Tab::onSaveConfigAs() {
227   auto &p        = *p_func();
228   auto &settings = Util::Settings::get();
229   auto fileName  = Util::getSaveFileName(this, QY("Save settings file as"), settings.m_lastConfigDir.path(), defaultFileNameForSaving(Q(".mtxcfg")), QY("MKVToolNix GUI config files") + Q(" (*.mtxcfg);;") + QY("All files") + Q(" (*)"), Q("mtxcfg"));
230   if (fileName.isEmpty())
231     return;
232 
233   updateConfigFromControlValues();
234   p.config.save(fileName);
235   settings.m_lastConfigDir.setPath(QFileInfo{fileName}.path());
236   settings.save();
237 
238   p.savedState = currentState();
239 
240   Q_EMIT titleChanged();
241 
242   MainWindow::get()->setStatusBarMessage(QY("The configuration has been saved."));
243 }
244 
245 QString
defaultFileNameForSaving(QString const & ext)246 Tab::defaultFileNameForSaving(QString const &ext) {
247   auto &p = *p_func();
248 
249   return !p.config.m_destination.isEmpty() ? QFileInfo{p.config.m_destination         }.completeBaseName() + ext
250        : !p.config.m_files.isEmpty()       ? QFileInfo{p.config.m_files[0]->m_fileName}.completeBaseName() + ext
251        :                                     QString{};
252 }
253 
254 QString
determineInitialDirForOpening(QLineEdit * lineEdit,InitialDirMode mode) const255 Tab::determineInitialDirForOpening(QLineEdit *lineEdit,
256                                    InitialDirMode mode)
257   const {
258   auto &p = *p_func();
259 
260   if (lineEdit && !lineEdit->text().isEmpty())
261     return Util::dirPath(QFileInfo{ lineEdit->text() }.path());
262 
263   if (   (mode == InitialDirMode::ContentFirstInputFileLastOpenDir)
264       && !p.config.m_files.isEmpty()
265       && !p.config.m_files[0]->m_fileName.isEmpty())
266     return Util::dirPath(QFileInfo{ p.config.m_files[0]->m_fileName }.path());
267 
268   return Util::Settings::get().lastOpenDirPath();
269 }
270 
271 QString
getOpenFileName(QString const & title,QString const & filter,QLineEdit * lineEdit,InitialDirMode initialDirMode)272 Tab::getOpenFileName(QString const &title,
273                      QString const &filter,
274                      QLineEdit *lineEdit,
275                      InitialDirMode initialDirMode) {
276   auto fullFilter = filter;
277   if (!fullFilter.isEmpty())
278     fullFilter += Q(";;");
279   fullFilter += QY("All files") + Q(" (*)");
280 
281   auto &settings = Util::Settings::get();
282   auto dir       = determineInitialDirForOpening(lineEdit, initialDirMode);
283   auto fileName  = Util::getOpenFileName(this, title, dir, fullFilter);
284   if (fileName.isEmpty())
285     return fileName;
286 
287   settings.m_lastOpenDir.setPath(QFileInfo{fileName}.path());
288   settings.save();
289 
290   if (lineEdit)
291     lineEdit->setText(fileName);
292 
293   return fileName;
294 }
295 
296 QString
determineInitialDirForSaving(QLineEdit * lineEdit) const297 Tab::determineInitialDirForSaving(QLineEdit *lineEdit)
298   const {
299   auto &p        = *p_func();
300   auto &settings = Util::Settings::get();
301   QString dir;
302 
303   if (lineEdit && !lineEdit->text().isEmpty())
304     dir = QFileInfo{lineEdit->text()}.path();
305 
306   else if (settings.m_outputFileNamePolicy == Util::Settings::ToFixedDirectory)
307     dir = settings.m_fixedOutputDir.path();
308 
309   else if (   (settings.m_outputFileNamePolicy == Util::Settings::ToParentOfFirstInputFile)
310            || (settings.m_outputFileNamePolicy == Util::Settings::ToSameAsFirstInputFile)
311            || (settings.m_outputFileNamePolicy == Util::Settings::ToRelativeOfFirstInputFile)) {
312     if (!p.config.m_files.isEmpty()) {
313       auto firstDir = QFileInfo{p.config.m_files.at(0)->m_fileName}.path();
314       dir           = settings.m_outputFileNamePolicy == Util::Settings::ToParentOfFirstInputFile ? QFileInfo{firstDir}.path()
315                     : settings.m_outputFileNamePolicy == Util::Settings::ToSameAsFirstInputFile   ? firstDir
316                     :                                                                               firstDir + Q("/") + settings.m_relativeOutputDir.path();
317     }
318 
319   } else if (!settings.m_lastOutputDir.path().isEmpty() && (settings.m_lastOutputDir.path() != Q(".")))
320     dir = settings.m_lastOutputDir.path();
321 
322   qDebug() << "determineInitialDirForSaving()"
323            << "dir"      << dir
324            << "lineEdit" << (lineEdit ? lineEdit->text() : QString{})
325            << "mode"     << settings.m_outputFileNamePolicy
326            << "fixed"    << settings.m_fixedOutputDir.path()
327            << "relative" << settings.m_relativeOutputDir.path()
328            << "lastOut"  << settings.m_lastOutputDir.path();
329 
330   return Util::dirPath(dir);
331 }
332 
333 QString
getSaveFileName(QString const & title,QString const & defaultFileName,QString const & filter,QLineEdit * lineEdit,QString const & defaultSuffix)334 Tab::getSaveFileName(QString const &title,
335                      QString const &defaultFileName,
336                      QString const &filter,
337                      QLineEdit *lineEdit,
338                      QString const &defaultSuffix) {
339   auto fullFilter = filter;
340   if (!fullFilter.isEmpty())
341     fullFilter += Q(";;");
342   fullFilter += QY("All files") + Q(" (*)");
343 
344   auto dir      = determineInitialDirForSaving(lineEdit);
345   auto fileName = Util::getSaveFileName(this, title, dir, defaultFileName, fullFilter, defaultSuffix);
346 
347 
348   if (fileName.isEmpty())
349     return fileName;
350 
351   auto &settings = Util::Settings::get();
352   settings.m_lastOutputDir.setPath(QFileInfo{fileName}.path());
353   settings.save();
354 
355   lineEdit->setText(fileName);
356 
357   return fileName;
358 }
359 
360 void
setControlValuesFromConfig()361 Tab::setControlValuesFromConfig() {
362   auto &p = *p_func();
363 
364   p.filesModel->setSourceFiles(p.config.m_files);
365   p.tracksModel->setTracks(p.config.m_tracks);
366   p.attachmentsModel->replaceAttachments(p.config.m_attachments);
367 
368   p.attachedFilesModel->reset();
369   for (auto const &sourceFile : p.config.m_files) {
370     p.attachedFilesModel->addAttachedFiles(sourceFile->m_attachedFiles);
371 
372     for (auto const &appendedFile : sourceFile->m_appendedFiles)
373       p.attachedFilesModel->addAttachedFiles(appendedFile->m_attachedFiles);
374   }
375 
376   p.ui->attachedFiles->sortByColumn(0, Qt::AscendingOrder);
377   p.attachedFilesModel->sort(0, Qt::AscendingOrder);
378 
379   onTrackSelectionChanged();
380   setOutputControlValues();
381   onAttachmentSelectionChanged();
382 }
383 
384 MuxConfig &
updateConfigFromControlValues()385 Tab::updateConfigFromControlValues() {
386   auto &p = *p_func();
387 
388   p.config.m_attachments = p.attachmentsModel->attachments();
389 
390   return p.config;
391 }
392 
393 void
retranslateUi()394 Tab::retranslateUi() {
395   auto &p = *p_func();
396 
397   p.ui->retranslateUi(this);
398 
399   retranslateInputUI();
400   retranslateOutputUI();
401   retranslateAttachmentsUI();
402 
403   Q_EMIT titleChanged();
404 }
405 
406 bool
isReadyForMerging()407 Tab::isReadyForMerging() {
408   auto &p = *p_func();
409 
410   auto destination = QDir::toNativeSeparators(p.ui->output->text());
411 
412   if (destination.isEmpty()) {
413     Util::MessageBox::critical(this)->title(QY("Cannot start multiplexing")).text(QY("You have to set the destination file name before you can start multiplexing or add a job to the job queue.")).exec();
414     return false;
415   }
416 
417   auto destinationValid = destination == Util::removeInvalidPathCharacters(destination);
418 
419 #if defined(SYS_WINDOWS)
420   if (destinationValid)
421     destinationValid = destination.contains(QRegularExpression{Q("^[a-zA-Z]:[\\\\/]|^\\\\\\\\.+\\.+")});
422 #endif  // SYS_WINDOWS
423 
424   if (!destinationValid) {
425     Util::MessageBox::critical(this)->title(QY("Cannot start multiplexing")).text(QY("The destination file name is invalid and must be fixed before you can start multiplexing or add a job to the job queue.")).exec();
426     return false;
427   }
428 
429   return true;
430 }
431 
432 QString
findExistingDestination() const433 Tab::findExistingDestination()
434   const {
435   auto &p                = *p_func();
436   auto nativeDestination = QDir::toNativeSeparators(p.config.m_destination);
437   QFileInfo destinationInfo{nativeDestination};
438 
439   if (destinationInfo.exists())
440     return nativeDestination;
441 
442   if (!p.config.isSplittingEnabled())
443     return {};
444 
445 #if defined(SYS_WINDOWS)
446   auto rePatternOptions     = QRegularExpression::CaseInsensitiveOption;
447 #else
448   auto rePatternOptions     = QRegularExpression::NoPatternOption;
449 #endif
450   auto destinationBaseName  = QRegularExpression::escape(destinationInfo.completeBaseName());
451   auto destinationSuffix    = QRegularExpression::escape(destinationInfo.suffix());
452   auto splitNameTestPattern = Q("^%1-\\d+%2%3$").arg(destinationBaseName).arg(destinationSuffix.isEmpty() ? Q("") : Q("\\.")).arg(destinationSuffix);
453   auto splitNameTestRE      = QRegularExpression{splitNameTestPattern, rePatternOptions};
454   auto destinationDir       = destinationInfo.dir();
455 
456   for (auto const &existingFileName : destinationDir.entryList(QDir::NoFilter, QDir::Name | QDir::IgnoreCase))
457     if (splitNameTestRE.match(existingFileName).hasMatch())
458       return QDir::toNativeSeparators(destinationDir.filePath(existingFileName));
459 
460   return {};
461 }
462 
463 bool
checkIfOverwritingIsOK()464 Tab::checkIfOverwritingIsOK() {
465   if (!Util::Settings::get().m_warnBeforeOverwriting)
466     return true;
467 
468   auto existingDestination = findExistingDestination();
469 
470   if (!existingDestination.isEmpty() && !MainWindow::jobTool()->checkIfOverwritingExistingFileIsOK(existingDestination))
471     return false;
472 
473   return MainWindow::jobTool()->checkIfOverwritingExistingJobIsOK(p_func()->config.m_destination, p_func()->config.isSplittingEnabled());
474 }
475 
476 bool
checkIfMissingAudioTrackIsOK()477 Tab::checkIfMissingAudioTrackIsOK() {
478   auto policy = Util::Settings::get().m_mergeWarnMissingAudioTrack;
479   if (policy == Util::Settings::MergeMissingAudioTrackPolicy::Never)
480     return true;
481 
482   auto haveAudioTrack = false;
483 
484   for (auto const &file : p_func()->config.m_files)
485     for (auto const &track : file->m_tracks) {
486       if (!track->isAudio())
487         continue;
488 
489       if (track->m_muxThis)
490         return true;
491 
492       haveAudioTrack = true;
493     }
494 
495   if (!haveAudioTrack && (policy == Util::Settings::MergeMissingAudioTrackPolicy::IfAudioTrackPresent))
496     return true;
497 
498   auto answer = Util::MessageBox::question(this)
499     ->title(QY("Create file without audio track"))
500     .text(Q("%1 %2")
501           .arg(QY("With the current multiplex settings the destination file will not contain an audio track."))
502           .arg(QY("Do you want to continue?")))
503     .buttonLabel(QMessageBox::Yes, QY("&Create file without audio track"))
504     .buttonLabel(QMessageBox::No,  QY("Cancel"))
505     .exec();
506 
507   return answer == QMessageBox::Yes;
508 }
509 
510 void
addToJobQueue(bool startNow,std::optional<Util::Settings::ClearMergeSettingsAction> clearSettings)511 Tab::addToJobQueue(bool startNow,
512                    std::optional<Util::Settings::ClearMergeSettingsAction> clearSettings) {
513   auto &p = *p_func();
514 
515   updateConfigFromControlValues();
516   setOutputFileNameMaybe();
517 
518   if (   !isReadyForMerging()
519       || !checkIfOverwritingIsOK()
520       || !checkIfMissingAudioTrackIsOK())
521     return;
522 
523   auto &cfg      = Util::Settings::get();
524   auto newConfig = std::make_shared<MuxConfig>(p.config);
525   auto job       = std::make_shared<Jobs::MuxJob>(startNow ? Jobs::Job::PendingAuto : Jobs::Job::PendingManual, newConfig);
526 
527   job->setDateAdded(QDateTime::currentDateTime());
528   job->setDescription(job->displayableDescription());
529 
530   if (!startNow) {
531     if (!cfg.m_useDefaultJobDescription) {
532       auto newDescription = QString{};
533 
534       while (newDescription.isEmpty()) {
535         bool ok = false;
536         newDescription = QInputDialog::getText(this, QY("Enter job description"), QY("Please enter the new job's description."), QLineEdit::Normal, job->description(), &ok);
537         if (!ok)
538           return;
539       }
540 
541       job->setDescription(newDescription);
542     }
543 
544   } else if (cfg.m_switchToJobOutputAfterStarting)
545     MainWindow::get()->switchToTool(MainWindow::watchJobTool());
546 
547   MainWindow::jobTool()->addJob(std::static_pointer_cast<Jobs::Job>(job));
548 
549   p.savedState = currentState();
550 
551   cfg.m_mergeLastOutputDirs.add(QDir::toNativeSeparators(QFileInfo{ p.config.m_destination }.path()));
552   cfg.save();
553 
554   auto action = clearSettings ? *clearSettings : Util::Settings::get().m_clearMergeSettings;
555   handleClearingMergeSettings(action);
556 }
557 
558 void
handleClearingMergeSettings(Util::Settings::ClearMergeSettingsAction action)559 Tab::handleClearingMergeSettings(Util::Settings::ClearMergeSettingsAction action) {
560   if (Util::Settings::ClearMergeSettingsAction::None == action)
561     return;
562 
563   if (Util::Settings::ClearMergeSettingsAction::RemoveInputFiles == action) {
564     onRemoveAllFiles();
565     return;
566   }
567 
568   if (Util::Settings::ClearMergeSettingsAction::CloseSettings == action) {
569     QTimer::singleShot(0, this, [this]() { signalRemovalOfThisTab(); });
570     return;
571   }
572 
573   // Util::Settings::ClearMergeSettingsAction::NewSettings
574   MainWindow::mergeTool()->appendNewTab();
575   QTimer::singleShot(0, this, [this]() { signalRemovalOfThisTab(); });
576 }
577 
578 void
signalRemovalOfThisTab()579 Tab::signalRemovalOfThisTab() {
580   Q_EMIT removeThisTab();
581 }
582 
583 QString
currentState()584 Tab::currentState() {
585   updateConfigFromControlValues();
586   return p_func()->config.toString();
587 }
588 
589 bool
hasBeenModified()590 Tab::hasBeenModified() {
591   return currentState() != p_func()->savedState;
592 }
593 
594 bool
isEmpty()595 Tab::isEmpty() {
596   return currentState() == p_func()->emptyState;
597 }
598 
599 QList<SourceFilePtr> const &
sourceFiles()600 Tab::sourceFiles() {
601   return p_func()->config.m_files;
602 }
603 
604 QModelIndex
fileModelIndexForFileNum(unsigned int num)605 Tab::fileModelIndexForFileNum(unsigned int num) {
606   return p_func()->filesModel->index(num, 0);
607 }
608 
609 }
610