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