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