mgba-ps3/src/platform/qt/library/LibraryModel.cpp
2025-03-31 10:18:54 -05:00

419 lines
12 KiB
C++

/* Copyright (c) 2013-2022 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LibraryModel.h"
#include "../utils.h"
#include <QApplication>
#include <QDir>
#include <QItemSelectionModel>
#include <QSortFilterProxyModel>
#include <QStyle>
#include <algorithm>
using namespace QGBA;
static const QStringList iconSets{
"GBA",
"GBC",
"GB",
"SGB",
// "DS",
};
LibraryModel::LibraryModel(QObject* parent)
: QAbstractItemModel(parent)
, m_treeMode(false)
, m_showFilename(false)
{
for (const QString& platform : iconSets) {
QString pathTemplate = QStringLiteral(":/res/%1-icon%2").arg(platform);
QIcon icon;
icon.addFile(pathTemplate.arg("-256.png"), QSize(256, 256));
icon.addFile(pathTemplate.arg("-128.png"), QSize(128, 128));
icon.addFile(pathTemplate.arg("-32.png"), QSize(32, 32));
icon.addFile(pathTemplate.arg("-24.png"), QSize(24, 24));
icon.addFile(pathTemplate.arg("-16.png"), QSize(16, 16));
// This will silently and harmlessly fail if QSvgIconEngine isn't compiled in.
icon.addFile(pathTemplate.arg(".svg"));
m_icons[platform.toLower()] = icon;
}
}
bool LibraryModel::treeMode() const {
return m_treeMode;
}
void LibraryModel::setTreeMode(bool tree) {
if (m_treeMode == tree) {
return;
}
beginResetModel();
m_treeMode = tree;
endResetModel();
}
bool LibraryModel::showFilename() const {
return m_showFilename;
}
void LibraryModel::setShowFilename(bool show) {
if (m_showFilename == show) {
return;
}
m_showFilename = show;
if (m_treeMode) {
int numPaths = m_pathOrder.size();
for (int i = 0; i < numPaths; i++) {
QModelIndex parent = index(i, 0);
emit dataChanged(index(0, 0, parent), index(m_pathIndex[m_pathOrder[i]].size() - 1, 0));
}
} else {
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
}
}
void LibraryModel::resetEntries(const QList<LibraryEntry>& items) {
beginResetModel();
blockSignals(true);
m_games.clear();
m_pathOrder.clear();
m_pathIndex.clear();
addEntriesList(items);
blockSignals(false);
endResetModel();
}
void LibraryModel::addEntries(const QList<LibraryEntry>& items) {
if (items.isEmpty()) {
return;
} else if (m_treeMode) {
addEntriesTree(items);
} else {
addEntriesList(items);
}
}
void LibraryModel::addEntryInternal(const LibraryEntry& item) {
m_gameIndex[item.fullpath] = m_games.size();
m_games << item;
if (!m_pathIndex.contains(item.base)) {
m_pathOrder << item.base;
}
m_pathIndex[item.base] << &m_games.back();
}
void LibraryModel::addEntriesList(const QList<LibraryEntry>& items) {
beginInsertRows(QModelIndex(), m_games.size(), m_games.size() + items.size() - 1);
for (const LibraryEntry& item : items) {
addEntryInternal(item);
}
endInsertRows();
}
void LibraryModel::addEntriesTree(const QList<LibraryEntry>& items) {
QHash<QString, QList<const LibraryEntry*>> byPath;
QHash<QString, QList<const LibraryEntry*>> newPaths;
for (const LibraryEntry& item : items) {
if (m_pathIndex.contains(item.base)) {
byPath[item.base] << &item;
} else {
newPaths[item.base] << &item;
}
}
if (newPaths.size() > 0) {
beginInsertRows(QModelIndex(), m_pathIndex.size(), m_pathIndex.size() + newPaths.size() - 1);
for (const QString& base : newPaths.keys()) {
for (const LibraryEntry* item : newPaths[base]) {
addEntryInternal(*item);
}
}
endInsertRows();
}
for (const QString& base : byPath.keys()) {
QList<const LibraryEntry*>& pathItems = m_pathIndex[base];
QList<const LibraryEntry*>& newItems = byPath[base];
QModelIndex parent = indexForPath(base);
beginInsertRows(parent, pathItems.size(), pathItems.size() + newItems.size() - 1);
for (const LibraryEntry* item : newItems) {
addEntryInternal(*item);
}
endInsertRows();
}
}
void LibraryModel::updateEntries(const QList<LibraryEntry>& items) {
QHash<QModelIndex, SpanSet> updatedSpans;
for (const LibraryEntry& item : items) {
QModelIndex idx = index(item.fullpath);
Q_ASSERT(idx.isValid());
int pos = m_gameIndex.value(item.fullpath, -1);
Q_ASSERT(pos >= 0);
m_games[pos] = item;
updatedSpans[idx.parent()].add(pos);
}
for (auto iter = updatedSpans.begin(); iter != updatedSpans.end(); iter++) {
QModelIndex parent = iter.key();
SpanSet spans = iter.value();
spans.merge();
for (const SpanSet::Span& span : spans.spans) {
QModelIndex topLeft = index(span.left, 0, parent);
QModelIndex bottomRight = index(span.right, MAX_COLUMN, parent);
emit dataChanged(topLeft, bottomRight);
}
}
}
void LibraryModel::removeEntries(const QList<QString>& items) {
SpanSet removedRootSpans;
QHash<QString, SpanSet> removedTreeSpans;
int firstModifiedIndex = m_games.size();
for (const QString& item : items) {
int pos = m_gameIndex.value(item, -1);
Q_ASSERT(pos >= 0);
if (pos < firstModifiedIndex) {
firstModifiedIndex = pos;
}
LibraryEntry* entry = &m_games[pos];
QModelIndex parent = indexForPath(entry->base);
Q_ASSERT(!m_treeMode || parent.isValid());
QList<const LibraryEntry*>& pathItems = m_pathIndex[entry->base];
removedTreeSpans[entry->base].add(pathItems.indexOf(entry));
if (!m_treeMode) {
removedRootSpans.add(pos);
}
m_gameIndex.remove(item);
}
for (const QString& base : removedTreeSpans.keys()) {
SpanSet& spanSet = removedTreeSpans[base];
spanSet.merge();
QList<const LibraryEntry*>& pathIndex = m_pathIndex[base];
if (spanSet.spans.size() == 1) {
SpanSet::Span span = spanSet.spans[0];
if (span.left == 0 && span.right == pathIndex.size() - 1) {
if (m_treeMode) {
removedRootSpans.add(m_pathOrder.indexOf(base));
} else {
m_pathIndex.remove(base);
m_pathOrder.removeAll(base);
}
continue;
}
}
QModelIndex parent = indexForPath(base);
spanSet.sort(true);
for (const SpanSet::Span& span : spanSet.spans) {
if (m_treeMode) {
beginRemoveRows(parent, span.left, span.right);
for (int i = span.left; i <= span.right; i++) {
}
}
pathIndex.erase(pathIndex.begin() + span.left, pathIndex.begin() + span.right + 1);
if (m_treeMode) {
endRemoveRows();
}
}
}
removedRootSpans.merge();
removedRootSpans.sort(true);
for (const SpanSet::Span& span : removedRootSpans.spans) {
beginRemoveRows(QModelIndex(), span.left, span.right);
if (m_treeMode) {
for (int i = span.right; i >= span.left; i--) {
QString base = m_pathOrder.takeAt(i);
m_pathIndex.remove(base);
}
} else {
m_games.erase(m_games.begin() + span.left, m_games.begin() + span.right + 1);
}
endRemoveRows();
}
for (int i = m_games.count() - 1; i >= firstModifiedIndex; i--) {
m_gameIndex[m_games[i].fullpath] = i;
}
}
QModelIndex LibraryModel::index(const QString& game) const {
int pos = m_gameIndex.value(game, -1);
if (pos < 0) {
return QModelIndex();
}
if (m_treeMode) {
const LibraryEntry& entry = m_games[pos];
return createIndex(m_pathIndex[entry.base].indexOf(&entry), 0, m_pathOrder.indexOf(entry.base));
}
return createIndex(pos, 0);
}
QModelIndex LibraryModel::index(int row, int column, const QModelIndex& parent) const {
if (!parent.isValid()) {
return createIndex(row, column, quintptr(0));
}
if (!m_treeMode || parent.internalId() || parent.column() != 0) {
return QModelIndex();
}
return createIndex(row, column, parent.row() + 1);
}
QModelIndex LibraryModel::parent(const QModelIndex& child) const {
if (!child.isValid() || child.internalId() == 0) {
return QModelIndex();
}
return createIndex(child.internalId() - 1, 0, quintptr(0));
}
int LibraryModel::columnCount(const QModelIndex& parent) const {
if (!parent.isValid() || (parent.column() == 0 && !parent.parent().isValid())) {
return MAX_COLUMN + 1;
}
return 0;
}
int LibraryModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
if (m_treeMode) {
if (parent.row() < 0 || parent.row() >= m_pathOrder.size() || parent.column() != 0) {
return 0;
}
return m_pathIndex[m_pathOrder[parent.row()]].size();
}
return 0;
}
if (m_treeMode) {
return m_pathOrder.size();
}
return m_games.size();
}
QVariant LibraryModel::folderData(const QModelIndex& index, int role) const
{
// Precondition: index and role must have already been validated
if (role == Qt::DecorationRole) {
return qApp->style()->standardIcon(QStyle::SP_DirOpenIcon);
}
if (role == FullPathRole || (index.column() == COL_LOCATION && role != Qt::DisplayRole)) {
return m_pathOrder[index.row()];
}
if (index.column() == COL_NAME) {
QString path = m_pathOrder[index.row()];
return path.section('/', -1);
}
return QVariant();
}
QVariant LibraryModel::data(const QModelIndex& index, int role) const {
if (role != Qt::DisplayRole &&
role != Qt::EditRole &&
role != Qt::ToolTipRole &&
role != Qt::DecorationRole &&
role != Qt::TextAlignmentRole &&
role != FullPathRole) {
return QVariant();
}
if (!checkIndex(index)) {
return QVariant();
}
if (role == Qt::ToolTipRole && index.column() > COL_LOCATION) {
return QVariant();
}
if (role == Qt::DecorationRole && index.column() != COL_NAME) {
return QVariant();
}
if (role == Qt::TextAlignmentRole) {
return index.column() == COL_SIZE ? (int)(Qt::AlignTrailing | Qt::AlignVCenter) : (int)(Qt::AlignLeading | Qt::AlignVCenter);
}
const LibraryEntry* entry = nullptr;
if (m_treeMode) {
if (!index.parent().isValid()) {
return folderData(index, role);
}
QString path = m_pathOrder[index.parent().row()];
entry = m_pathIndex[path][index.row()];
} else if (!index.parent().isValid() && index.row() < m_games.size()) {
entry = &m_games[index.row()];
}
if (entry) {
if (role == FullPathRole) {
return entry->fullpath;
}
switch (index.column()) {
case COL_NAME:
if (role == Qt::DecorationRole) {
return m_icons.value(entry->displayPlatform(), qApp->style()->standardIcon(QStyle::SP_FileIcon));
}
return entry->displayTitle(m_showFilename);
case COL_LOCATION:
return QDir::toNativeSeparators(entry->base);
case COL_PLATFORM:
return nicePlatformFormat(entry->platform);
case COL_SIZE:
return (role == Qt::DisplayRole) ? QVariant(niceSizeFormat(entry->filesize)) : QVariant(int(entry->filesize));
case COL_CRC32:
return (role == Qt::DisplayRole) ? QVariant(QStringLiteral("%0").arg(entry->crc32, 8, 16, QChar('0'))) : QVariant(entry->crc32);
}
}
return QVariant();
}
QVariant LibraryModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case COL_NAME:
return QApplication::translate("LibraryTree", "Name", nullptr);
case COL_LOCATION:
return QApplication::translate("LibraryTree", "Location", nullptr);
case COL_PLATFORM:
return QApplication::translate("LibraryTree", "Platform", nullptr);
case COL_SIZE:
return QApplication::translate("LibraryTree", "Size", nullptr);
case COL_CRC32:
return QApplication::translate("LibraryTree", "CRC32", nullptr);
};
}
return QVariant();
}
QModelIndex LibraryModel::indexForPath(const QString& path) {
int pos = m_pathOrder.indexOf(path);
if (pos < 0) {
pos = m_pathOrder.size();
beginInsertRows(QModelIndex(), pos, pos);
m_pathOrder << path;
m_pathIndex[path] = QList<const LibraryEntry*>();
endInsertRows();
}
if (!m_treeMode) {
return QModelIndex();
}
return index(pos, 0, QModelIndex());
}
QModelIndex LibraryModel::indexForPath(const QString& path) const {
if (!m_treeMode) {
return QModelIndex();
}
int pos = m_pathOrder.indexOf(path);
if (pos < 0) {
return QModelIndex();
}
return index(pos, 0, QModelIndex());
}
LibraryEntry LibraryModel::entry(const QString& game) const {
int pos = m_gameIndex.value(game, -1);
if (pos < 0) {
return {};
}
return m_games[pos];
}