1 #include "contentmodel.hpp"
2 #include "esmfile.hpp"
3
4 #include <stdexcept>
5
6 #include <QDir>
7 #include <QTextCodec>
8 #include <QDebug>
9
10 #include <components/esm/esmreader.hpp>
11
ContentModel(QObject * parent,QIcon warningIcon)12 ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) :
13 QAbstractTableModel(parent),
14 mWarningIcon(warningIcon),
15 mMimeType ("application/omwcontent"),
16 mMimeTypes (QStringList() << mMimeType),
17 mColumnCount (1),
18 mDropActions (Qt::MoveAction)
19 {
20 setEncoding ("win1252");
21 uncheckAll();
22 }
23
~ContentModel()24 ContentSelectorModel::ContentModel::~ContentModel()
25 {
26 qDeleteAll(mFiles);
27 mFiles.clear();
28 }
29
setEncoding(const QString & encoding)30 void ContentSelectorModel::ContentModel::setEncoding(const QString &encoding)
31 {
32 mEncoding = encoding;
33 }
34
columnCount(const QModelIndex & parent) const35 int ContentSelectorModel::ContentModel::columnCount(const QModelIndex &parent) const
36 {
37 if (parent.isValid())
38 return 0;
39
40 return mColumnCount;
41 }
42
rowCount(const QModelIndex & parent) const43 int ContentSelectorModel::ContentModel::rowCount(const QModelIndex &parent) const
44 {
45 if(parent.isValid())
46 return 0;
47
48 return mFiles.size();
49 }
50
item(int row) const51 const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(int row) const
52 {
53 if (row >= 0 && row < mFiles.size())
54 return mFiles.at(row);
55
56 return nullptr;
57 }
58
item(int row)59 ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(int row)
60 {
61 if (row >= 0 && row < mFiles.count())
62 return mFiles.at(row);
63
64 return nullptr;
65 }
item(const QString & name) const66 const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(const QString &name) const
67 {
68 EsmFile::FileProperty fp = EsmFile::FileProperty_FileName;
69
70 if (name.contains ('/'))
71 fp = EsmFile::FileProperty_FilePath;
72
73 for (const EsmFile *file : mFiles)
74 {
75 if (name.compare(file->fileProperty (fp).toString(), Qt::CaseInsensitive) == 0)
76 return file;
77 }
78 return nullptr;
79 }
80
indexFromItem(const EsmFile * item) const81 QModelIndex ContentSelectorModel::ContentModel::indexFromItem(const EsmFile *item) const
82 {
83 //workaround: non-const pointer cast for calls from outside contentmodel/contentselector
84 EsmFile *non_const_file_ptr = const_cast<EsmFile *>(item);
85
86 if (item)
87 return index(mFiles.indexOf(non_const_file_ptr),0);
88
89 return QModelIndex();
90 }
91
flags(const QModelIndex & index) const92 Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index) const
93 {
94 if (!index.isValid())
95 return Qt::ItemIsDropEnabled;
96
97 const EsmFile *file = item(index.row());
98
99 if (!file)
100 return Qt::NoItemFlags;
101
102 //game files can always be checked
103 if (file->isGameFile())
104 return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
105
106 Qt::ItemFlags returnFlags;
107
108 // addon can be checked if its gamefile is
109 // ... special case, addon with no dependency can be used with any gamefile.
110 bool gamefileChecked = (file->gameFiles().count() == 0);
111 for (const QString &fileName : file->gameFiles())
112 {
113 for (QListIterator<EsmFile *> dependencyIter(mFiles); dependencyIter.hasNext(); dependencyIter.next())
114 {
115 //compare filenames only. Multiple instances
116 //of the filename (with different paths) is not relevant here.
117 bool depFound = (dependencyIter.peekNext()->fileName().compare(fileName, Qt::CaseInsensitive) == 0);
118
119 if (!depFound)
120 continue;
121
122 if (!gamefileChecked)
123 {
124 if (isChecked (dependencyIter.peekNext()->filePath()))
125 gamefileChecked = (dependencyIter.peekNext()->isGameFile());
126 }
127
128 // force it to iterate all files in cases where the current
129 // dependency is a game file to ensure that a later duplicate
130 // game file is / is not checked.
131 // (i.e., break only if it's not a gamefile or the game file has been checked previously)
132 if (gamefileChecked || !(dependencyIter.peekNext()->isGameFile()))
133 break;
134 }
135 }
136
137 if (gamefileChecked)
138 {
139 returnFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
140 }
141
142 return returnFlags;
143 }
144
data(const QModelIndex & index,int role) const145 QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int role) const
146 {
147 if (!index.isValid())
148 return QVariant();
149
150 if (index.row() >= mFiles.size())
151 return QVariant();
152
153 const EsmFile *file = item(index.row());
154
155 if (!file)
156 return QVariant();
157
158 const int column = index.column();
159
160 switch (role)
161 {
162 case Qt::DecorationRole:
163 {
164 return isLoadOrderError(file) ? mWarningIcon : QVariant();
165 }
166
167 case Qt::EditRole:
168 case Qt::DisplayRole:
169 {
170 if (column >=0 && column <=EsmFile::FileProperty_GameFile)
171 return file->fileProperty(static_cast<EsmFile::FileProperty>(column));
172
173 return QVariant();
174 }
175
176 case Qt::TextAlignmentRole:
177 {
178 switch (column)
179 {
180 case 0:
181 case 1:
182 return Qt::AlignLeft + Qt::AlignVCenter;
183 case 2:
184 case 3:
185 return Qt::AlignRight + Qt::AlignVCenter;
186 default:
187 return Qt::AlignLeft + Qt::AlignVCenter;
188 }
189 }
190
191 case Qt::ToolTipRole:
192 {
193 if (column != 0)
194 return QVariant();
195
196 return toolTip(file);
197 }
198
199 case Qt::CheckStateRole:
200 {
201 if (file->isGameFile())
202 return QVariant();
203
204 return mCheckStates[file->filePath()];
205 }
206
207 case Qt::UserRole:
208 {
209 if (file->isGameFile())
210 return ContentType_GameFile;
211 else
212 if (flags(index))
213 return ContentType_Addon;
214
215 break;
216 }
217
218 case Qt::UserRole + 1:
219 return isChecked(file->filePath());
220 }
221 return QVariant();
222 }
223
setData(const QModelIndex & index,const QVariant & value,int role)224 bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const QVariant &value, int role)
225 {
226 if(!index.isValid())
227 return false;
228
229 EsmFile *file = item(index.row());
230 QString fileName = file->fileName();
231 bool success = false;
232
233 switch(role)
234 {
235 case Qt::EditRole:
236 {
237 QStringList list = value.toStringList();
238
239 for (int i = 0; i < EsmFile::FileProperty_GameFile; i++)
240 file->setFileProperty(static_cast<EsmFile::FileProperty>(i), list.at(i));
241
242 for (int i = EsmFile::FileProperty_GameFile; i < list.size(); i++)
243 file->setFileProperty (EsmFile::FileProperty_GameFile, list.at(i));
244
245 emit dataChanged(index, index);
246
247 success = true;
248 }
249 break;
250
251 case Qt::UserRole+1:
252 {
253 success = (flags (index) & Qt::ItemIsEnabled);
254
255 if (success)
256 {
257 success = setCheckState(file->filePath(), value.toBool());
258 emit dataChanged(index, index);
259 }
260 }
261 break;
262
263 case Qt::CheckStateRole:
264 {
265 int checkValue = value.toInt();
266 bool setState = false;
267 if ((checkValue==Qt::Checked) && !isChecked(file->filePath()))
268 {
269 setState = true;
270 success = true;
271 }
272 else if ((checkValue == Qt::Checked) && isChecked (file->filePath()))
273 setState = true;
274 else if (checkValue == Qt::Unchecked)
275 setState = true;
276
277 if (setState)
278 {
279 setCheckState(file->filePath(), success);
280 emit dataChanged(index, index);
281 checkForLoadOrderErrors();
282 }
283 else
284 return success;
285
286 for (EsmFile *file2 : mFiles)
287 {
288 if (file2->gameFiles().contains(fileName, Qt::CaseInsensitive))
289 {
290 QModelIndex idx = indexFromItem(file2);
291 emit dataChanged(idx, idx);
292 }
293 }
294
295 success = true;
296 }
297 break;
298 }
299
300 return success;
301 }
302
insertRows(int position,int rows,const QModelIndex & parent)303 bool ContentSelectorModel::ContentModel::insertRows(int position, int rows, const QModelIndex &parent)
304 {
305 if (parent.isValid())
306 return false;
307
308 beginInsertRows(parent, position, position+rows-1);
309 {
310 for (int row = 0; row < rows; ++row)
311 mFiles.insert(position, new EsmFile);
312
313 } endInsertRows();
314
315 return true;
316 }
317
removeRows(int position,int rows,const QModelIndex & parent)318 bool ContentSelectorModel::ContentModel::removeRows(int position, int rows, const QModelIndex &parent)
319 {
320 if (parent.isValid())
321 return false;
322
323 beginRemoveRows(parent, position, position+rows-1);
324 {
325 for (int row = 0; row < rows; ++row)
326 delete mFiles.takeAt(position);
327
328 } endRemoveRows();
329
330 // at this point we know that drag and drop has finished.
331 checkForLoadOrderErrors();
332 return true;
333 }
334
supportedDropActions() const335 Qt::DropActions ContentSelectorModel::ContentModel::supportedDropActions() const
336 {
337 return mDropActions;
338 }
339
mimeTypes() const340 QStringList ContentSelectorModel::ContentModel::mimeTypes() const
341 {
342 return mMimeTypes;
343 }
344
mimeData(const QModelIndexList & indexes) const345 QMimeData *ContentSelectorModel::ContentModel::mimeData(const QModelIndexList &indexes) const
346 {
347 QByteArray encodedData;
348
349 for (const QModelIndex &index : indexes)
350 {
351 if (!index.isValid())
352 continue;
353
354 encodedData.append(item(index.row())->encodedData());
355 }
356
357 QMimeData *mimeData = new QMimeData();
358 mimeData->setData(mMimeType, encodedData);
359
360 return mimeData;
361 }
362
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)363 bool ContentSelectorModel::ContentModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
364 {
365 if (action == Qt::IgnoreAction)
366 return true;
367
368 if (column > 0)
369 return false;
370
371 if (!data->hasFormat(mMimeType))
372 return false;
373
374 int beginRow = rowCount();
375
376 if (row != -1)
377 beginRow = row;
378
379 else if (parent.isValid())
380 beginRow = parent.row();
381
382 QByteArray encodedData = data->data(mMimeType);
383 QDataStream stream(&encodedData, QIODevice::ReadOnly);
384
385 while (!stream.atEnd())
386 {
387
388 QString value;
389 QStringList values;
390 QStringList gamefiles;
391
392 for (int i = 0; i < EsmFile::FileProperty_GameFile; ++i)
393 {
394 stream >> value;
395 values << value;
396 }
397
398 stream >> gamefiles;
399
400 insertRows(beginRow, 1);
401
402 QModelIndex idx = index(beginRow++, 0, QModelIndex());
403 setData(idx, QStringList() << values << gamefiles, Qt::EditRole);
404 }
405
406 return true;
407 }
408
addFile(EsmFile * file)409 void ContentSelectorModel::ContentModel::addFile(EsmFile *file)
410 {
411 beginInsertRows(QModelIndex(), mFiles.count(), mFiles.count());
412 mFiles.append(file);
413 endInsertRows();
414
415 QModelIndex idx = index (mFiles.size() - 2, 0, QModelIndex());
416
417 emit dataChanged (idx, idx);
418 }
419
addFiles(const QString & path)420 void ContentSelectorModel::ContentModel::addFiles(const QString &path)
421 {
422 QDir dir(path);
423 QStringList filters;
424 filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon";
425 dir.setNameFilters(filters);
426
427 for (const QString &path2 : dir.entryList())
428 {
429 QFileInfo info(dir.absoluteFilePath(path2));
430
431 if (item(info.fileName()))
432 continue;
433
434 try {
435 ESM::ESMReader fileReader;
436 ToUTF8::Utf8Encoder encoder =
437 ToUTF8::calculateEncoding(mEncoding.toStdString());
438 fileReader.setEncoder(&encoder);
439 fileReader.open(std::string(dir.absoluteFilePath(path2).toUtf8().constData()));
440
441 EsmFile *file = new EsmFile(path2);
442
443 for (std::vector<ESM::Header::MasterData>::const_iterator itemIter = fileReader.getGameFiles().begin();
444 itemIter != fileReader.getGameFiles().end(); ++itemIter)
445 file->addGameFile(QString::fromUtf8(itemIter->name.c_str()));
446
447 file->setAuthor (QString::fromUtf8(fileReader.getAuthor().c_str()));
448 file->setDate (info.lastModified());
449 file->setFormat (fileReader.getFormat());
450 file->setFilePath (info.absoluteFilePath());
451 file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str()));
452
453 // HACK
454 // Load order constraint of Bloodmoon.esm needing Tribunal.esm is missing
455 // from the file supplied by Bethesda, so we have to add it ourselves
456 if (file->fileName().compare("Bloodmoon.esm", Qt::CaseInsensitive) == 0)
457 {
458 file->addGameFile(QString::fromUtf8("Tribunal.esm"));
459 }
460
461 // Put the file in the table
462 addFile(file);
463
464 } catch(std::runtime_error &e) {
465 // An error occurred while reading the .esp
466 qWarning() << "Error reading addon file: " << e.what();
467 continue;
468 }
469
470 }
471
472 sortFiles();
473 }
474
clearFiles()475 void ContentSelectorModel::ContentModel::clearFiles()
476 {
477 const int filesCount = mFiles.count();
478
479 if (filesCount > 0) {
480 beginRemoveRows(QModelIndex(), 0, filesCount - 1);
481 mFiles.clear();
482 endRemoveRows();
483 }
484 }
485
gameFiles() const486 QStringList ContentSelectorModel::ContentModel::gameFiles() const
487 {
488 QStringList gameFiles;
489 for (const ContentSelectorModel::EsmFile *file : mFiles)
490 {
491 if (file->isGameFile())
492 {
493 gameFiles.append(file->fileName());
494 }
495 }
496 return gameFiles;
497 }
498
sortFiles()499 void ContentSelectorModel::ContentModel::sortFiles()
500 {
501 //first, sort the model such that all dependencies are ordered upstream (gamefile) first.
502 bool movedFiles = true;
503 int fileCount = mFiles.size();
504
505 //Dependency sort
506 //iterate until no sorting of files occurs
507 while (movedFiles)
508 {
509 movedFiles = false;
510 //iterate each file, obtaining a reference to it's gamefiles list
511 for (int i = 0; i < fileCount; i++)
512 {
513 QModelIndex idx1 = index (i, 0, QModelIndex());
514 const QStringList &gamefiles = mFiles.at(i)->gameFiles();
515 //iterate each file after the current file, verifying that none of it's
516 //dependencies appear.
517 for (int j = i + 1; j < fileCount; j++)
518 {
519 if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive)
520 || (!mFiles.at(i)->isGameFile() && gamefiles.isEmpty()
521 && mFiles.at(j)->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files
522 {
523 mFiles.move(j, i);
524
525 QModelIndex idx2 = index (j, 0, QModelIndex());
526
527 emit dataChanged (idx1, idx2);
528
529 movedFiles = true;
530 }
531 }
532 if (movedFiles)
533 break;
534 }
535 }
536 }
537
isChecked(const QString & filepath) const538 bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const
539 {
540 if (mCheckStates.contains(filepath))
541 return (mCheckStates[filepath] == Qt::Checked);
542
543 return false;
544 }
545
isEnabled(QModelIndex index) const546 bool ContentSelectorModel::ContentModel::isEnabled (QModelIndex index) const
547 {
548 return (flags(index) & Qt::ItemIsEnabled);
549 }
550
isLoadOrderError(const EsmFile * file) const551 bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile *file) const
552 {
553 return mPluginsWithLoadOrderError.contains(file->filePath());
554 }
555
setContentList(const QStringList & fileList)556 void ContentSelectorModel::ContentModel::setContentList(const QStringList &fileList)
557 {
558 mPluginsWithLoadOrderError.clear();
559 int previousPosition = -1;
560 for (const QString &filepath : fileList)
561 {
562 if (setCheckState(filepath, true))
563 {
564 // as necessary, move plug-ins in visible list to match sequence of supplied filelist
565 const EsmFile* file = item(filepath);
566 int filePosition = indexFromItem(file).row();
567 if (filePosition < previousPosition)
568 {
569 mFiles.move(filePosition, previousPosition);
570 emit dataChanged(index(filePosition, 0, QModelIndex()), index(previousPosition, 0, QModelIndex()));
571 }
572 else
573 {
574 previousPosition = filePosition;
575 }
576 }
577 }
578 checkForLoadOrderErrors();
579 }
580
checkForLoadOrderErrors()581 void ContentSelectorModel::ContentModel::checkForLoadOrderErrors()
582 {
583 for (int row = 0; row < mFiles.count(); ++row)
584 {
585 EsmFile* file = item(row);
586 bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0;
587 if (isRowInError)
588 {
589 mPluginsWithLoadOrderError.insert(file->filePath());
590 }
591 else
592 {
593 mPluginsWithLoadOrderError.remove(file->filePath());
594 }
595 }
596 }
597
checkForLoadOrderErrors(const EsmFile * file,int row) const598 QList<ContentSelectorModel::LoadOrderError> ContentSelectorModel::ContentModel::checkForLoadOrderErrors(const EsmFile *file, int row) const
599 {
600 QList<LoadOrderError> errors = QList<LoadOrderError>();
601 for (const QString &dependentfileName : file->gameFiles())
602 {
603 const EsmFile* dependentFile = item(dependentfileName);
604
605 if (!dependentFile)
606 {
607 errors.append(LoadOrderError(LoadOrderError::ErrorCode_MissingDependency, dependentfileName));
608 }
609 else
610 {
611 if (!isChecked(dependentFile->filePath()))
612 {
613 errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName));
614 }
615 if (row < indexFromItem(dependentFile).row())
616 {
617 errors.append(LoadOrderError(LoadOrderError::ErrorCode_LoadOrder, dependentfileName));
618 }
619 }
620 }
621 return errors;
622 }
623
toolTip(const EsmFile * file) const624 QString ContentSelectorModel::ContentModel::toolTip(const EsmFile *file) const
625 {
626 if (isLoadOrderError(file))
627 {
628 QString text("<b>");
629 int index = indexFromItem(item(file->filePath())).row();
630 for (const LoadOrderError& error : checkForLoadOrderErrors(file, index))
631 {
632 text += "<p>";
633 text += error.toolTip();
634 text += "</p>";
635 }
636 text += ("</b>");
637 text += file->toolTip();
638 return text;
639 }
640 else
641 {
642 return file->toolTip();
643 }
644 }
645
refreshModel()646 void ContentSelectorModel::ContentModel::refreshModel()
647 {
648 emit dataChanged (index(0,0), index(rowCount()-1,0));
649 }
650
setCheckState(const QString & filepath,bool checkState)651 bool ContentSelectorModel::ContentModel::setCheckState(const QString &filepath, bool checkState)
652 {
653 if (filepath.isEmpty())
654 return false;
655
656 const EsmFile *file = item(filepath);
657
658 if (!file)
659 return false;
660
661 Qt::CheckState state = Qt::Unchecked;
662
663 if (checkState)
664 state = Qt::Checked;
665
666 mCheckStates[filepath] = state;
667 emit dataChanged(indexFromItem(item(filepath)), indexFromItem(item(filepath)));
668
669 if (file->isGameFile())
670 refreshModel();
671
672 //if we're checking an item, ensure all "upstream" files (dependencies) are checked as well.
673 if (state == Qt::Checked)
674 {
675 for (const QString& upstreamName : file->gameFiles())
676 {
677 const EsmFile *upstreamFile = item(upstreamName);
678
679 if (!upstreamFile)
680 continue;
681
682 if (!isChecked(upstreamFile->filePath()))
683 mCheckStates[upstreamFile->filePath()] = Qt::Checked;
684
685 emit dataChanged(indexFromItem(upstreamFile), indexFromItem(upstreamFile));
686
687 }
688 }
689 //otherwise, if we're unchecking an item (or the file is a game file) ensure all downstream files are unchecked.
690 if (state == Qt::Unchecked)
691 {
692 for (const EsmFile *downstreamFile : mFiles)
693 {
694 QFileInfo fileInfo(filepath);
695 QString filename = fileInfo.fileName();
696
697 if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive))
698 {
699 if (mCheckStates.contains(downstreamFile->filePath()))
700 mCheckStates[downstreamFile->filePath()] = Qt::Unchecked;
701
702 emit dataChanged(indexFromItem(downstreamFile), indexFromItem(downstreamFile));
703 }
704 }
705 }
706
707 return true;
708 }
709
checkedItems() const710 ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checkedItems() const
711 {
712 ContentFileList list;
713
714 // TODO:
715 // First search for game files and next addons,
716 // so we get more or less correct game files vs addons order.
717 for (EsmFile *file : mFiles)
718 if (isChecked(file->filePath()))
719 list << file;
720
721 return list;
722 }
723
uncheckAll()724 void ContentSelectorModel::ContentModel::uncheckAll()
725 {
726 emit layoutAboutToBeChanged();
727 mCheckStates.clear();
728 emit layoutChanged();
729 }
730