1 #include "common/common_pch.h"
2 
3 #include <QByteArray>
4 #include <QDataStream>
5 #include <QDir>
6 #include <QFileInfo>
7 #include <QItemSelectionModel>
8 #include <QMimeData>
9 #include <QPainter>
10 #include <QPixmap>
11 #include <QRegularExpression>
12 
13 #include "common/logger.h"
14 #include "common/sorting.h"
15 #include "common/strings/formatting.h"
16 #include "mkvtoolnix-gui/main_window/main_window.h"
17 #include "mkvtoolnix-gui/mime_types.h"
18 #include "mkvtoolnix-gui/merge/attached_file_model.h"
19 #include "mkvtoolnix-gui/merge/source_file_model.h"
20 #include "mkvtoolnix-gui/merge/track_model.h"
21 #include "mkvtoolnix-gui/util/container.h"
22 #include "mkvtoolnix-gui/util/model.h"
23 #include "mkvtoolnix-gui/util/settings.h"
24 
25 namespace mtx::gui::Merge {
26 
27 namespace {
28 
29 QIcon
createSourceIndicatorIcon(SourceFile & sourceFile)30 createSourceIndicatorIcon(SourceFile &sourceFile) {
31   auto iconName = sourceFile.isAdditionalPart() ? Q("distribute-horizontal-margin")
32                 : sourceFile.isAppended()       ? Q("distribute-horizontal-x")
33                 :                                 Q("distribute-vertical-page");
34   iconName      = Q(":/icons/16x16/%1.png").arg(iconName);
35 
36   if (!Util::Settings::get().m_mergeUseFileAndTrackColors)
37     return QIcon{iconName};
38 
39   auto color = Util::Settings::get().nthFileColor(sourceFile.m_colorIndex);
40 
41   QPixmap combinedPixmap{28, 16};
42   combinedPixmap.fill(Qt::transparent);
43 
44   QPainter painter{&combinedPixmap};
45 
46   painter.drawPixmap(0, 0, QPixmap{iconName});
47 
48   painter.setPen(color);
49   painter.setBrush(color);
50   painter.drawRect(20, 2, 8, 12);
51 
52   QIcon combinedIcon;
53   combinedIcon.addPixmap(combinedPixmap);
54 
55   return combinedIcon;
56 }
57 
58 struct SequencedFileNameData {
59   QString prefix, suffix;
60   unsigned int number{};
61 
followsmtx::gui::Merge::__anon9fb46faf0111::SequencedFileNameData62   bool follows(SequencedFileNameData const &previous) const {
63     return (prefix == previous.prefix)
64         && (suffix == previous.suffix)
65         && (number == (previous.number + 1));
66   }
67 };
68 
69 std::optional<SequencedFileNameData>
analyzeFileNameForSequenceData(QString const & fileName)70 analyzeFileNameForSequenceData(QString const &fileName) {
71   QRegularExpression re{Q(R"(([^/\\]*)(\d+)([^\d]+)$)")};
72   auto match = re.match(fileName);
73 
74   if (match.hasMatch())
75     return SequencedFileNameData{ match.captured(1), match.captured(3), match.captured(2).toUInt() };
76 
77   return {};
78 }
79 
80 int
insertPriorityForSourceFile(SourceFile const & file)81 insertPriorityForSourceFile(SourceFile const &file) {
82   return file.hasVideoTrack()     ? 0
83        : file.hasAudioTrack()     ? 1
84        : file.hasSubtitlesTrack() ? 2
85        :                            3;
86 }
87 
88 } // anonymous namespace
89 
SourceFileModel(QObject * parent)90 SourceFileModel::SourceFileModel(QObject *parent)
91   : QStandardItemModel{parent}
92   , m_sourceFiles{}
93   , m_tracksModel{}
94   , m_attachedFilesModel{}
95   , m_nonAppendedSelected{}
96   , m_appendedSelected{}
97   , m_additionalPartSelected{}
98 {
99   initializeColorIndexes();
100 
101   connect(MainWindow::get(), &MainWindow::preferencesChanged, this, &SourceFileModel::updateFileColors);
102 }
103 
~SourceFileModel()104 SourceFileModel::~SourceFileModel() {
105 }
106 
107 void
retranslateUi()108 SourceFileModel::retranslateUi() {
109   Util::setDisplayableAndSymbolicColumnNames(*this, {
110     { QY("File name"), Q("fileName")  },
111     { QY("Container"), Q("container") },
112     { QY("File size"), Q("fileSize")  },
113     { QY("Directory"), Q("directory") },
114   });
115 
116   horizontalHeaderItem(2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
117 
118   if (!m_sourceFiles)
119     return;
120 
121   for (auto const &sourceFile : *m_sourceFiles) {
122     sourceFileUpdated(sourceFile.get());
123 
124     for (auto const &additionalPart : sourceFile->m_additionalParts)
125       sourceFileUpdated(additionalPart.get());
126 
127     for (auto const &appendedFile : sourceFile->m_appendedFiles)
128       sourceFileUpdated(appendedFile.get());
129   }
130 }
131 
132 void
setOtherModels(TrackModel * tracksModel,AttachedFileModel * attachedFilesModel)133 SourceFileModel::setOtherModels(TrackModel *tracksModel,
134                                 AttachedFileModel *attachedFilesModel) {
135   m_tracksModel        = tracksModel;
136   m_attachedFilesModel = attachedFilesModel;
137 }
138 
139 void
createAndAppendRow(QStandardItem * item,SourceFilePtr const & file,int position)140 SourceFileModel::createAndAppendRow(QStandardItem *item,
141                                     SourceFilePtr const &file,
142                                     int position) {
143   m_sourceFileMap[reinterpret_cast<quint64>(file.get())] = file;
144   auto row                                               = createRow(file.get());
145 
146   if (file->isAdditionalPart()) {
147     auto fileToAddTo = m_sourceFileMap[item->data(Util::SourceFileRole).value<quint64>()];
148     Q_ASSERT(fileToAddTo);
149     item->insertRow(position, row);
150 
151   } else
152     item->appendRow(row);
153 }
154 
155 void
setSourceFiles(QList<SourceFilePtr> & sourceFiles)156 SourceFileModel::setSourceFiles(QList<SourceFilePtr> &sourceFiles) {
157   removeRows(0, rowCount());
158   m_sourceFileMap.clear();
159 
160   initializeColorIndexes();
161 
162   m_sourceFiles = &sourceFiles;
163   auto row      = 0u;
164 
165   for (auto const &file : *m_sourceFiles) {
166     assignColorIndex(*file);
167 
168     createAndAppendRow(invisibleRootItem(), file);
169 
170     auto rowItem  = item(row);
171     auto position = 0;
172 
173     for (auto const &additionalPart : file->m_additionalParts)
174       createAndAppendRow(rowItem, additionalPart, position++);
175 
176     for (auto const &appendedFile : file->m_appendedFiles)
177       createAndAppendRow(rowItem, appendedFile);
178 
179     ++row;
180   }
181 }
182 
183 QList<QStandardItem *>
createRow(SourceFile * sourceFile) const184 SourceFileModel::createRow(SourceFile *sourceFile)
185   const {
186   auto items = QList<QStandardItem *>{};
187   for (int idx = 0; idx < columnCount(); ++idx)
188     items << new QStandardItem{};
189 
190   setItemsFromSourceFile(items, sourceFile);
191 
192   return items;
193 }
194 
195 void
setItemsFromSourceFile(QList<QStandardItem * > const & items,SourceFile * sourceFile) const196 SourceFileModel::setItemsFromSourceFile(QList<QStandardItem *> const &items,
197                                         SourceFile *sourceFile)
198   const {
199   auto info = QFileInfo{sourceFile->m_fileName};
200 
201   items[0]->setText(info.fileName());
202   items[1]->setText(sourceFile->isAdditionalPart() ? QY("(additional part)") : sourceFile->container());
203   items[2]->setText(to_qs(mtx::string::format_file_size(sourceFile->isPlaylist() ? sourceFile->m_playlistSize : info.size())));
204   items[3]->setText(QDir::toNativeSeparators(info.path()));
205 
206   items[0]->setData(reinterpret_cast<quint64>(sourceFile), Util::SourceFileRole);
207   items[0]->setIcon(createSourceIndicatorIcon(*sourceFile));
208 
209   items[2]->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
210 }
211 
212 void
sourceFileUpdated(SourceFile * sourceFile)213 SourceFileModel::sourceFileUpdated(SourceFile *sourceFile) {
214   auto idx = indexFromSourceFile(sourceFile);
215   if (!idx.isValid())
216     return;
217 
218   auto items = QList<QStandardItem *>{};
219 
220   for (auto column = 0, numColumns = columnCount(); column < numColumns; ++column)
221     items << itemFromIndex(idx.sibling(idx.row(), column));
222 
223   setItemsFromSourceFile(items, sourceFile);
224 }
225 
226 quint64
storageValueFromIndex(QModelIndex const & idx) const227 SourceFileModel::storageValueFromIndex(QModelIndex const &idx)
228   const {
229   return idx.sibling(idx.row(), 0)
230     .data(Util::SourceFileRole)
231     .value<quint64>();
232 }
233 
234 SourceFilePtr
fromIndex(QModelIndex const & idx) const235 SourceFileModel::fromIndex(QModelIndex const &idx)
236   const {
237   if (!idx.isValid())
238     return nullptr;
239   return m_sourceFileMap[storageValueFromIndex(idx)];
240 }
241 
242 QModelIndex
indexFromSourceFile(SourceFile * sourceFile) const243 SourceFileModel::indexFromSourceFile(SourceFile *sourceFile)
244   const {
245   if (!sourceFile)
246     return QModelIndex{};
247 
248   return indexFromSourceFile(reinterpret_cast<quint64>(sourceFile), QModelIndex{});
249 }
250 
251 QModelIndex
indexFromSourceFile(quint64 value,QModelIndex const & parent) const252 SourceFileModel::indexFromSourceFile(quint64 value,
253                                      QModelIndex const &parent)
254   const {
255   auto currentValue = storageValueFromIndex(parent);
256   if (currentValue == value)
257     return parent;
258 
259   auto invalidIdx = QModelIndex{};
260 
261   for (auto row = 0, numRows = rowCount(parent); row < numRows; ++row) {
262     auto idx = indexFromSourceFile(value, index(row, 0, parent));
263     if (idx != invalidIdx)
264       return idx;
265   }
266 
267   return invalidIdx;
268 }
269 
270 void
addAdditionalParts(QStringList const & fileNames,QModelIndex const & fileToAddToIdx)271 SourceFileModel::addAdditionalParts(QStringList const &fileNames,
272                                     QModelIndex const &fileToAddToIdx) {
273   auto actualIdx = Util::toTopLevelIdx(fileToAddToIdx);
274   if (fileNames.isEmpty() || !actualIdx.isValid())
275     return;
276 
277   auto fileToAddTo = fromIndex(actualIdx);
278   auto itemToAddTo = itemFromIndex(actualIdx);
279   Q_ASSERT(fileToAddTo && itemToAddTo);
280 
281   auto actualFileNames = QStringList{};
282   std::copy_if(fileNames.begin(), fileNames.end(), std::back_inserter(actualFileNames), [&fileToAddTo](QString const &fileName) -> bool {
283       if (fileToAddTo->m_fileName == fileName)
284         return false;
285       for (auto additionalPart : fileToAddTo->m_additionalParts)
286         if (additionalPart->m_fileName == fileName)
287           return false;
288       return true;
289     });
290 
291   if (actualFileNames.isEmpty())
292     return;
293 
294   mtx::sort::naturally(actualFileNames.begin(), actualFileNames.end());
295 
296   for (auto &fileName : actualFileNames) {
297     auto additionalPart              = std::make_shared<SourceFile>(fileName);
298     additionalPart->m_additionalPart = true;
299 
300     createAndAppendRow(itemToAddTo, additionalPart, fileToAddTo->m_additionalParts.size());
301 
302     fileToAddTo->m_additionalParts << additionalPart;
303   }
304 }
305 
306 void
addOrAppendFilesAndTracks(QVector<SourceFilePtr> const & files,QModelIndex const & fileToAddToIdx,bool append)307 SourceFileModel::addOrAppendFilesAndTracks(QVector<SourceFilePtr> const &files,
308                                            QModelIndex const &fileToAddToIdx,
309                                            bool append) {
310   Q_ASSERT(m_tracksModel);
311 
312   if (files.isEmpty())
313     return;
314 
315   for (auto const &file : files)
316     assignColorIndex(*file);
317 
318   if (append)
319     appendFilesAndTracks(files, fileToAddToIdx);
320   else
321     addFilesAndTracks(files);
322 }
323 
324 QModelIndex
addFileSortedByType(SourceFilePtr const & file)325 SourceFileModel::addFileSortedByType(SourceFilePtr const &file) {
326   auto newFilePrio = insertPriorityForSourceFile(*file);
327 
328   for (int idx = 0, numFiles = m_sourceFiles->size(); idx < numFiles; ++idx) {
329     auto existingFilePrio = insertPriorityForSourceFile(*m_sourceFiles->at(idx));
330 
331     if (existingFilePrio <= newFilePrio)
332       continue;
333 
334     m_sourceFileMap[reinterpret_cast<quint64>(file.get())] = file;
335     auto row                                               = createRow(file.get());
336 
337     invisibleRootItem()->insertRow(idx, row);
338     m_sourceFiles->insert(idx, file);
339 
340     return index(idx, 0);
341   }
342 
343   return {};
344 }
345 
346 QModelIndex
addFileAtAppropriatePlace(SourceFilePtr const & file,bool sortByType)347 SourceFileModel::addFileAtAppropriatePlace(SourceFilePtr const &file,
348                                            bool sortByType) {
349   QModelIndex insertPosIdx;
350 
351   if (sortByType) {
352     insertPosIdx = addFileSortedByType(file);
353 
354     if (insertPosIdx.isValid())
355       return insertPosIdx;
356   }
357 
358   createAndAppendRow(invisibleRootItem(), file);
359   *m_sourceFiles << file;
360 
361   return index(rowCount() - 1, 0);
362 }
363 
364 void
addFilesAndTracks(QVector<SourceFilePtr> const & files)365 SourceFileModel::addFilesAndTracks(QVector<SourceFilePtr> const &files) {
366   std::optional<SequencedFileNameData> previouslyAddedSequenceData;
367   QModelIndex previouslyAddedPosition;
368 
369   auto &cfg                 = Util::Settings::get();
370   auto sortByType           = cfg.m_mergeSortFilesTracksByTypeWhenAdding;
371   auto reconstructSequences = cfg.m_mergeReconstructSequencesWhenAdding;
372   auto filesToProcess       = files;
373 
374   if (reconstructSequences)
375     mtx::sort::by(filesToProcess.begin(), filesToProcess.end(), [](auto const &file) {
376       return mtx::sort::natural_string_c(file->m_fileName);
377     });
378 
379   for (auto const &file : filesToProcess) {
380     auto sequenceData = analyzeFileNameForSequenceData(file->m_fileName);
381 
382     if (   reconstructSequences
383         && previouslyAddedSequenceData
384         && previouslyAddedPosition.isValid()
385         && sequenceData
386         && sequenceData->follows(*previouslyAddedSequenceData)) {
387       appendFilesAndTracks({ file }, previouslyAddedPosition);
388       previouslyAddedSequenceData = sequenceData;
389 
390       continue;
391     }
392 
393     previouslyAddedPosition     = addFileAtAppropriatePlace(file, sortByType);
394     previouslyAddedSequenceData = sequenceData;
395 
396     for (auto const &track : file->m_tracks)
397       m_tracksModel->addTrackAtAppropriatePlace(track, sortByType);
398 
399     if (file->m_additionalParts.isEmpty())
400       continue;
401 
402     auto itemToAddTo = item(rowCount() - 1, 0);
403     auto row         = 0;
404     for (auto const &additionalPart : file->m_additionalParts)
405       createAndAppendRow(itemToAddTo, additionalPart, row++);
406   }
407 
408   m_attachedFilesModel->addAttachedFiles(std::accumulate(files.begin(), files.end(), QList<TrackPtr>{}, [](QList<TrackPtr> &accu, SourceFilePtr const &file) { return accu << file->m_attachedFiles; }));
409 }
410 
411 void
removeFile(SourceFile * fileToBeRemoved)412 SourceFileModel::removeFile(SourceFile *fileToBeRemoved) {
413   m_availableColorIndexes.prepend(fileToBeRemoved->m_colorIndex);
414 
415   m_sourceFileMap.remove(reinterpret_cast<quint64>(fileToBeRemoved));
416 
417   if (fileToBeRemoved->isAdditionalPart()) {
418     auto row = -1, parentFileRow = -1;
419     auto numParentRows = m_sourceFiles->count();
420 
421     for (parentFileRow = 0; parentFileRow < numParentRows; ++parentFileRow) {
422       row = Util::findPtr(fileToBeRemoved, (*m_sourceFiles)[parentFileRow]->m_additionalParts);
423       if (row != -1)
424         break;
425     }
426 
427     Q_ASSERT((-1 != row) && (-1 != parentFileRow));
428 
429     item(parentFileRow)->removeRow(row);
430     (*m_sourceFiles)[parentFileRow]->m_additionalParts.removeAt(row);
431 
432     return;
433   }
434 
435   if (fileToBeRemoved->isAppended()) {
436     auto row           = Util::findPtr(fileToBeRemoved,               fileToBeRemoved->m_appendedTo->m_appendedFiles);
437     auto parentFileRow = Util::findPtr(fileToBeRemoved->m_appendedTo, *m_sourceFiles);
438 
439     Q_ASSERT((-1 != row) && (-1 != parentFileRow));
440 
441     row += fileToBeRemoved->m_appendedTo->m_additionalParts.size();
442 
443     item(parentFileRow)->removeRow(row);
444     fileToBeRemoved->m_appendedTo->m_appendedFiles.removeAt(row);
445 
446     return;
447   }
448 
449   auto row = Util::findPtr(fileToBeRemoved, *m_sourceFiles);
450   Q_ASSERT(-1 != row);
451 
452   invisibleRootItem()->removeRow(row);
453   m_sourceFiles->removeAt(row);
454 }
455 
456 void
removeFiles(QList<SourceFile * > const & files)457 SourceFileModel::removeFiles(QList<SourceFile *> const &files) {
458   auto filesToRemove  = Util::qListToSet(files);
459   auto tracksToRemove = QSet<Track *>{};
460   auto attachedFiles  = QList<TrackPtr>{};
461 
462   for (auto const &file : files) {
463     for (auto const &track : file->m_tracks)
464       tracksToRemove << track.get();
465 
466     attachedFiles += file->m_attachedFiles;
467 
468     for (auto const &appendedFile : file->m_appendedFiles) {
469       filesToRemove << appendedFile.get();
470       for (auto const &track : appendedFile->m_tracks)
471         tracksToRemove << track.get();
472 
473       attachedFiles += appendedFile->m_attachedFiles;
474     }
475   }
476 
477   m_tracksModel->reDistributeAppendedTracksForFileRemoval(filesToRemove);
478   m_tracksModel->removeTracks(tracksToRemove);
479   m_attachedFilesModel->removeAttachedFiles(attachedFiles);
480 
481   auto filesToRemoveLast = QList<SourceFile *>{};
482   for (auto &file : filesToRemove)
483     if (!file->isRegular())
484       removeFile(file);
485     else
486       filesToRemoveLast << file;
487 
488   for (auto &file : filesToRemoveLast)
489     if (file->isRegular())
490       removeFile(file);
491 }
492 
493 void
appendFilesAndTracks(QVector<SourceFilePtr> const & files,QModelIndex const & fileToAppendToIdx)494 SourceFileModel::appendFilesAndTracks(QVector<SourceFilePtr> const &files,
495                                       QModelIndex const &fileToAppendToIdx) {
496   auto actualIdx = Util::toTopLevelIdx(fileToAppendToIdx);
497   if (files.isEmpty() || !actualIdx.isValid())
498     return;
499 
500   auto fileToAppendTo = fromIndex(actualIdx);
501   auto itemToAppendTo = itemFromIndex(actualIdx);
502   Q_ASSERT(fileToAppendTo && itemToAppendTo);
503 
504   for (auto const &file : files) {
505     file->m_appended   = true;
506     file->m_appendedTo = fileToAppendTo.get();
507 
508     createAndAppendRow(itemToAppendTo, file);
509 
510     fileToAppendTo->m_appendedFiles << file;
511   }
512 
513   for (auto const &file : files)
514     m_tracksModel->appendTracks(fileToAppendTo.get(), file->m_tracks);
515 }
516 
517 void
updateSelectionStatus()518 SourceFileModel::updateSelectionStatus() {
519   m_nonAppendedSelected    = false;
520   m_appendedSelected       = false;
521   m_additionalPartSelected = false;
522 
523   auto selectionModel      = qobject_cast<QItemSelectionModel *>(QObject::sender());
524   Q_ASSERT(selectionModel);
525 
526   Util::withSelectedIndexes(selectionModel, [this](QModelIndex const &selectedIndex) {
527     auto sourceFile = fromIndex(selectedIndex);
528     if (!sourceFile)
529       return;
530 
531     if (sourceFile->isRegular())
532       m_nonAppendedSelected = true;
533 
534     else if (sourceFile->isAppended())
535       m_appendedSelected = true;
536 
537     else if (sourceFile->isAdditionalPart())
538       m_additionalPartSelected = true;
539   });
540 }
541 
542 void
dumpSourceFiles(QString const & label) const543 SourceFileModel::dumpSourceFiles(QString const &label)
544   const {
545   auto dumpIt = [](std::string const &prefix, SourceFilePtr const &sourceFile) {
546     log_it(fmt::format("{0}{1}\n", prefix, sourceFile->m_fileName));
547   };
548 
549   log_it(fmt::format("Dumping source files {0}\n", label));
550 
551   for (auto const &sourceFile : *m_sourceFiles) {
552     dumpIt("  ", sourceFile);
553     for (auto const &additionalPart : sourceFile->m_additionalParts)
554       dumpIt("    () ", additionalPart);
555     for (auto const &appendedSourceFile : sourceFile->m_appendedFiles)
556       dumpIt("    +  ", appendedSourceFile);
557   }
558 }
559 
560 void
updateSourceFileLists()561 SourceFileModel::updateSourceFileLists() {
562   for (auto const &sourceFile : *m_sourceFiles) {
563     sourceFile->m_additionalParts.clear();
564     sourceFile->m_appendedFiles.clear();
565   }
566 
567   m_sourceFiles->clear();
568 
569   for (auto row = 0, numRows = rowCount(); row < numRows; ++row) {
570     auto idx        = index(row, 0, QModelIndex{});
571     auto sourceFile = fromIndex(idx);
572 
573     Q_ASSERT(sourceFile);
574 
575     *m_sourceFiles << sourceFile;
576 
577     for (auto appendedRow = 0, numAppendedRows = rowCount(idx); appendedRow < numAppendedRows; ++appendedRow) {
578       auto appendedSourceFile = fromIndex(index(appendedRow, 0, idx));
579       Q_ASSERT(appendedSourceFile);
580 
581       appendedSourceFile->m_appendedTo = sourceFile.get();
582       if (appendedSourceFile->isAppended())
583         sourceFile->m_appendedFiles << appendedSourceFile;
584       else
585         sourceFile->m_additionalParts << appendedSourceFile;
586     }
587   }
588 
589   // TODO: SourceFileModel::updateSourceFileLists move dropped additional parts to end of additional parts sub-list
590 
591   dumpSourceFiles("updateSourceFileLists END");
592 }
593 
594 Qt::DropActions
supportedDropActions() const595 SourceFileModel::supportedDropActions()
596   const {
597   return Qt::MoveAction;
598 }
599 
600 Qt::ItemFlags
flags(QModelIndex const & index) const601 SourceFileModel::flags(QModelIndex const &index)
602   const {
603   auto actualFlags = QStandardItemModel::flags(index) & ~Qt::ItemIsDropEnabled & ~Qt::ItemIsDragEnabled;
604 
605   // If both appended files/additional parts and non-appended files
606   // have been selected then those cannot be dragged & dropped at the
607   // same time.
608   if (m_nonAppendedSelected && (m_appendedSelected | m_additionalPartSelected))
609     return actualFlags;
610 
611   // Everyting else can be at least dragged.
612   actualFlags |= Qt::ItemIsDragEnabled;
613 
614   auto indexSourceFile = fromIndex(index);
615 
616   // Appended files/additional parts can only be dropped onto
617   // non-appended files (meaning on model indexes that are valid) –
618   // but only on top level items (meaning the parent index is
619   // invalid).
620   if ((m_appendedSelected | m_additionalPartSelected) && index.isValid() && !index.parent().isValid())
621     actualFlags |= Qt::ItemIsDropEnabled;
622 
623   // Non-appended files can only be dropped onto the root note (whose
624   // index isn't valid).
625   else if (m_nonAppendedSelected && !index.isValid())
626     actualFlags |= Qt::ItemIsDropEnabled;
627 
628   return actualFlags;
629 }
630 
631 QStringList
mimeTypes() const632 SourceFileModel::mimeTypes()
633   const {
634   return QStringList{} << mtx::gui::MimeTypes::MergeSourceFileModelItem;
635 }
636 
637 QMimeData *
mimeData(QModelIndexList const & indexes) const638 SourceFileModel::mimeData(QModelIndexList const &indexes)
639   const {
640   auto valuesToStore = QSet<quint64>{};
641 
642   for (auto const &index : indexes)
643     if (index.isValid())
644       valuesToStore << storageValueFromIndex(index);
645 
646   if (valuesToStore.isEmpty())
647     return nullptr;
648 
649   auto data    = new QMimeData{};
650   auto encoded = QByteArray{};
651 
652   QDataStream stream{&encoded, QIODevice::WriteOnly};
653 
654   for (auto const &value : valuesToStore)
655     stream << value;
656 
657   data->setData(mtx::gui::MimeTypes::MergeSourceFileModelItem, encoded);
658   return data;
659 }
660 
661 bool
canDropMimeData(QMimeData const * data,Qt::DropAction action,int,int,QModelIndex const & parent) const662 SourceFileModel::canDropMimeData(QMimeData const *data,
663                                  Qt::DropAction action,
664                                  int,
665                                  int,
666                                  QModelIndex const &parent)
667   const {
668   if (   !data
669       || !data->hasFormat(mtx::gui::MimeTypes::MergeSourceFileModelItem)
670       || (Qt::MoveAction != action))
671     return false;
672 
673   // If both appended files/additional parts and non-appended files
674   // have been selected then those cannot be dragged & dropped at the
675   // same time.
676   if (m_nonAppendedSelected && (m_appendedSelected | m_additionalPartSelected))
677     return false;
678 
679   // No drag & drop inside appended/additional parts, please.
680   if (parent.isValid() && parent.parent().isValid())
681     return false;
682 
683   // Appended files/additional parts can only be dropped onto
684   // non-appended files (meaning on model indexes that are valid) –
685   // but only on top level items (meaning the parent index is
686   // invalid).
687   if ((m_appendedSelected | m_additionalPartSelected) && !parent.isValid())
688     return false;
689 
690   // Non-appended files can only be dropped onto the root note (whose
691   // index isn't valid).
692   if (m_nonAppendedSelected && parent.isValid())
693     return false;
694 
695   return true;
696 }
697 
698 bool
dropMimeData(QMimeData const * data,Qt::DropAction action,int row,int column,QModelIndex const & parent)699 SourceFileModel::dropMimeData(QMimeData const *data,
700                               Qt::DropAction action,
701                               int row,
702                               int column,
703                               QModelIndex const &parent) {
704   if (!canDropMimeData(data, action, row, column, parent))
705     return false;
706 
707   if (row > rowCount(parent))
708     row = rowCount(parent);
709   if (row == -1)
710     row = rowCount(parent);
711 
712   auto result = dropSourceFiles(data, action, row, parent.isValid() ? parent.sibling(parent.row(), 0) : parent);
713 
714   Util::requestAllItems(*this);
715 
716   return result;
717 }
718 
719 QString
dumpIdx(QModelIndex const & idx,QString dumped=QString{})720 dumpIdx(QModelIndex const &idx,
721         QString dumped = QString{}) {
722   if (!idx.isValid())
723     return dumped.isEmpty() ? "<invalid>" : dumped;
724 
725   return Q("%1/%2%3").arg(idx.row()).arg(idx.column()).arg(dumped.isEmpty() ? Q("") : Q(">%1").arg(dumped));
726 }
727 
728 bool
dropSourceFiles(QMimeData const * data,Qt::DropAction action,int row,QModelIndex const & parent)729 SourceFileModel::dropSourceFiles(QMimeData const *data,
730                                  Qt::DropAction action,
731                                  int row,
732                                  QModelIndex const &parent) {
733   if (action != Qt::MoveAction)
734     return QAbstractItemModel::dropMimeData(data, action, row, 0, parent);
735 
736   auto encoded = data->data(mtx::gui::MimeTypes::MergeSourceFileModelItem);
737   QDataStream stream{&encoded, QIODevice::ReadOnly};
738 
739   while (!stream.atEnd()) {
740     quint64 value;
741     stream >> value;
742     auto sourceFile = m_sourceFileMap[value];
743     auto sourceIdx  = indexFromSourceFile(sourceFile.get());
744 
745     if (!sourceIdx.isValid())
746       continue;
747 
748     auto sourceParent     = sourceIdx.parent();
749     auto sourceParentItem = sourceParent.isValid() ? itemFromIndex(sourceParent) : invisibleRootItem();
750     auto rowItems         = sourceParentItem->takeRow(sourceIdx.row());
751 
752     if (!parent.isValid()) {
753       if ((sourceParent == parent) && (sourceIdx.row() < row))
754         --row;
755 
756       invisibleRootItem()->insertRow(row, rowItems);
757       ++row;
758 
759     } else {
760       auto parentFile = fromIndex(parent);
761       Q_ASSERT(parentFile);
762 
763       if (sourceFile->isAdditionalPart())
764         row = std::min<int>(row, parentFile->m_additionalParts.size());
765       else
766         row = std::max<int>(row, parentFile->m_additionalParts.size());
767 
768       if ((sourceParent == parent) && (sourceIdx.row() < row))
769         --row;
770 
771       itemFromIndex(parent)->insertRow(row, rowItems);
772       ++row;
773     }
774 
775     updateSourceFileLists();
776   }
777 
778   return false;
779 }
780 
781 void
sortSourceFiles(QList<SourceFile * > & files,bool reverse)782 SourceFileModel::sortSourceFiles(QList<SourceFile *> &files,
783                                  bool reverse) {
784   auto rows = QHash<SourceFile *, int>{};
785 
786   for (auto const &file : files)
787     rows[file] = indexFromSourceFile(file).row();
788 
789   std::sort(files.begin(), files.end(), [&rows](SourceFile *a, SourceFile *b) -> bool {
790     auto rowA = rows[a];
791     auto rowB = rows[b];
792 
793     if ( a->isRegular() &&  b->isRegular())
794       return rowA < rowB;
795 
796     if ( a->isRegular() && !b->isRegular())
797       return true;
798 
799     if (!a->isRegular() &&  b->isRegular())
800       return false;
801 
802     auto parentA = rows[a->m_appendedTo];
803     auto parentB = rows[b->m_appendedTo];
804 
805     return (parentA < parentB)
806         || ((parentA == parentB) && (rowA < rowB));
807   });
808 
809   if (reverse) {
810     std::reverse(files.begin(), files.end());
811     std::stable_partition(files.begin(), files.end(), [](SourceFile *file) { return file->isRegular(); });
812   }
813 }
814 
815 std::pair<int, int>
countAppendedAndAdditionalParts(QStandardItem * parentItem)816 SourceFileModel::countAppendedAndAdditionalParts(QStandardItem *parentItem) {
817   auto numbers = std::make_pair(0, 0);
818 
819   for (auto row = 0, numRows = parentItem->rowCount(); row < numRows; ++row) {
820     auto sourceFile = fromIndex(parentItem->child(row)->index());
821     Q_ASSERT(!!sourceFile);
822 
823     if (sourceFile->isAdditionalPart())
824       ++numbers.first;
825     else
826       ++numbers.second;
827   }
828 
829   return numbers;
830 }
831 
832 void
moveSourceFilesUpOrDown(QList<SourceFile * > files,bool up)833 SourceFileModel::moveSourceFilesUpOrDown(QList<SourceFile *> files,
834                                          bool up) {
835   sortSourceFiles(files, !up);
836 
837   // qDebug() << "move up?" << up << "files" << files;
838 
839   auto couldNotBeMoved = QHash<SourceFile *, bool>{};
840   auto isSelected      = QHash<SourceFile *, bool>{};
841   auto const direction = up ? -1 : +1;
842   auto const topRows   = rowCount();
843 
844   for (auto const &file : files) {
845     isSelected[file] = true;
846 
847     if (!file->isRegular() && isSelected[file->m_appendedTo])
848       continue;
849 
850     auto idx = indexFromSourceFile(file);
851     Q_ASSERT(idx.isValid());
852 
853     auto targetRow = idx.row() + direction;
854     if (couldNotBeMoved[fromIndex(idx.sibling(targetRow, 0)).get()]) {
855       couldNotBeMoved[file] = true;
856       continue;
857     }
858 
859     if (file->isRegular()) {
860       if (!((0 <= targetRow) && (targetRow < topRows))) {
861         couldNotBeMoved[file] = true;
862         continue;
863       }
864 
865       // qDebug() << "top level: would like to move" << idx.row() << "to" << targetRow;
866 
867       insertRow(targetRow, takeRow(idx.row()));
868 
869       continue;
870     }
871 
872     auto parentItem                   = itemFromIndex(idx.parent());
873     auto const appendedAdditionalRows = countAppendedAndAdditionalParts(parentItem);
874     auto const additionalPartsRows    = appendedAdditionalRows.first;
875     auto const appendedRows           = appendedAdditionalRows.second;
876     auto const lowerLimit             = (file->isAdditionalPart() ? 0 : additionalPartsRows);
877     auto const upperLimit             = (file->isAdditionalPart() ? 0 : appendedRows) +  additionalPartsRows;
878 
879     if ((lowerLimit <= targetRow) && (targetRow < upperLimit)) {
880       // qDebug() << "appended level normal: would like to move" << idx.row() << "to" << targetRow;
881 
882       parentItem->insertRow(targetRow, parentItem->takeRow(idx.row()));
883       continue;
884     }
885 
886     auto parentIdx = parentItem->index();
887     Q_ASSERT(parentIdx.isValid());
888 
889     auto newParentRow = parentIdx.row() + direction;
890     if ((0 > newParentRow) || (rowCount() <= newParentRow)) {
891       // qDebug() << "appended, cannot move further";
892       couldNotBeMoved[file] = true;
893       continue;
894     }
895 
896     auto newParent        = fromIndex(index(newParentRow, 0));
897     auto newParentItem    = itemFromIndex(index(newParentRow, 0));
898     auto rowItems         = parentItem->takeRow(idx.row());
899     auto newParentNumbers = countAppendedAndAdditionalParts(newParentItem);
900     targetRow             = up  && file->isAdditionalPart() ? newParentNumbers.first
901                           : up                              ? newParentNumbers.first + newParentNumbers.second
902                           : !up && file->isAdditionalPart() ? 0
903                           :                                   newParentNumbers.first;
904 
905     Q_ASSERT(!!newParent);
906 
907     // qDebug() << "appended level cross: would like to move" << idx.row() << "from" << file->m_appendedTo << "to" << newParent.get() << "as" << targetRow;
908 
909     newParentItem->insertRow(targetRow, rowItems);
910     file->m_appendedTo = newParent.get();
911   }
912 
913   updateSourceFileLists();
914 }
915 
916 void
initializeColorIndexes()917 SourceFileModel::initializeColorIndexes() {
918   m_availableColorIndexes.clear();
919   m_nextColorIndex = 0;
920 }
921 
922 void
assignColorIndex(SourceFile & file)923 SourceFileModel::assignColorIndex(SourceFile &file) {
924   if (m_availableColorIndexes.isEmpty())
925     m_availableColorIndexes << m_nextColorIndex++;
926 
927   file.m_colorIndex = m_availableColorIndexes.front();
928 
929   for (auto const &track : file.m_tracks)
930     track->m_colorIndex = file.m_colorIndex;
931 
932   m_availableColorIndexes.removeFirst();
933 
934   for (auto const &additionalPart : file.m_additionalParts)
935     assignColorIndex(*additionalPart);
936 
937   for (auto const &appendedFile : file.m_appendedFiles)
938     assignColorIndex(*appendedFile);
939 }
940 
941 void
updateFileColors()942 SourceFileModel::updateFileColors() {
943   Util::walkTree(*this, {}, [this](auto const &idx) {
944     auto item       = itemFromIndex(idx);
945     auto sourceFile = fromIndex(idx);
946 
947     if (item && sourceFile)
948       item->setIcon(createSourceIndicatorIcon(*sourceFile));
949   });
950 }
951 
952 }
953