diff --git a/src/core/library.c b/src/core/library.c index 24f3ddd42..9a7f1b636 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -222,7 +222,7 @@ struct mLibrary* mLibraryLoad(const char* path) { goto error; } - static const char insertRom[] = "INSERT INTO roms (crc32, md5, sha1, size, internalCode, platform, :models) VALUES (:crc32, :md5, :sha1, :size, :internalCode, :platform, :models);"; + static const char insertRom[] = "INSERT INTO roms (crc32, md5, sha1, size, internalCode, platform, models) VALUES (:crc32, :md5, :sha1, :size, :internalCode, :platform, :models);"; if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) { goto error; } diff --git a/src/platform/qt/library/LibraryController.cpp b/src/platform/qt/library/LibraryController.cpp index 198d14080..87d9ab5e2 100644 --- a/src/platform/qt/library/LibraryController.cpp +++ b/src/platform/qt/library/LibraryController.cpp @@ -61,13 +61,7 @@ LibraryController::LibraryController(QWidget* parent, const QString& path, Confi QObject::connect(m_libraryModel, &QAbstractItemModel::rowsInserted, &m_expandThrottle, qOverload<>(&QTimer::start)); LibraryStyle libraryStyle = LibraryStyle(m_config->getOption("libraryStyle", int(LibraryStyle::STYLE_LIST)).toInt()); - // Make sure setViewStyle does something - if (libraryStyle == LibraryStyle::STYLE_TREE) { - m_currentStyle = LibraryStyle::STYLE_LIST; - } else { - m_currentStyle = LibraryStyle::STYLE_TREE; - } - setViewStyle(libraryStyle); + updateViewStyle(libraryStyle); QVariant librarySort = m_config->getQtOption("librarySort"); QVariant librarySortOrder = m_config->getQtOption("librarySortOrder"); @@ -89,6 +83,10 @@ void LibraryController::setViewStyle(LibraryStyle newStyle) { if (m_currentStyle == newStyle) { return; } + updateViewStyle(newStyle); +} + +void LibraryController::updateViewStyle(LibraryStyle newStyle) { QString selected; if (m_currentView) { QModelIndex selectedIndex = m_currentView->selectionModel()->currentIndex(); @@ -266,16 +264,24 @@ void LibraryController::resizeEvent(QResizeEvent*) { resizeTreeView(false); } +// This function automatically reallocates the horizontal space between the +// columns in the view in a useful way when the window is resized. void LibraryController::resizeTreeView(bool expand) { + // When new items are added to the model, make sure they are revealed. if (expand) { m_treeView->expandAll(); } + // Start off by asking the view how wide it thinks each column should be. int viewportWidth = m_treeView->viewport()->width(); int totalWidth = m_treeView->header()->sectionSizeHint(LibraryModel::MAX_COLUMN); for (int column = 0; column < LibraryModel::MAX_COLUMN; column++) { totalWidth += m_treeView->columnWidth(column); } + + // If there would be empty space, ask the view to redistribute it. + // The final column is set to fill any remaining width, so this + // should (at least) fill the window. if (totalWidth < viewportWidth) { totalWidth = 0; for (int column = 0; column <= LibraryModel::MAX_COLUMN; column++) { @@ -283,6 +289,10 @@ void LibraryController::resizeTreeView(bool expand) { totalWidth += m_treeView->columnWidth(column); } } + + // If the columns would be too wide for the view now, try shrinking the + // "Location" column down to reduce horizontal scrolling, with a fixed + // minimum width of 100px. if (totalWidth > viewportWidth) { int locationWidth = m_treeView->columnWidth(LibraryModel::COL_LOCATION); if (locationWidth > 100) { @@ -291,7 +301,6 @@ void LibraryController::resizeTreeView(bool expand) { newLocationWidth = 100; } m_treeView->setColumnWidth(LibraryModel::COL_LOCATION, newLocationWidth); - totalWidth = totalWidth - locationWidth + newLocationWidth; } } } diff --git a/src/platform/qt/library/LibraryController.h b/src/platform/qt/library/LibraryController.h index 8a14f3a4b..483010c79 100644 --- a/src/platform/qt/library/LibraryController.h +++ b/src/platform/qt/library/LibraryController.h @@ -75,6 +75,7 @@ protected: private: void loadDirectory(const QString&, bool recursive = true); // Called on separate thread + void updateViewStyle(LibraryStyle newStyle); ConfigController* m_config = nullptr; std::shared_ptr m_library; diff --git a/src/platform/qt/library/LibraryEntry.cpp b/src/platform/qt/library/LibraryEntry.cpp index 25a5976c9..c2aeae1ab 100644 --- a/src/platform/qt/library/LibraryEntry.cpp +++ b/src/platform/qt/library/LibraryEntry.cpp @@ -12,7 +12,21 @@ using namespace QGBA; -static inline uint64_t checkHash(size_t filesize, uint32_t crc32) { +static inline uint64_t getSha1Prefix(const uint8_t* sha1) { + return *reinterpret_cast(sha1); +} + +static inline uint64_t getSha1Prefix(const QByteArray& sha1) { + if (sha1.size() < 8) { + return 0; + } + return getSha1Prefix((const uint8_t*)sha1.constData()); +} + +static inline uint64_t checkHash(size_t filesize, uint32_t crc32, uint64_t sha1Prefix) { + if (sha1Prefix) { + return sha1Prefix; + } return (uint64_t(filesize) << 32) ^ ((crc32 + 1ULL) * (uint32_t(filesize) + 1ULL)); } @@ -27,6 +41,7 @@ LibraryEntry::LibraryEntry(const mLibraryEntry* entry) , platformModels(entry->platformModels) , filesize(entry->filesize) , crc32(entry->crc32) + , sha1(reinterpret_cast(entry->sha1), sizeof(entry->sha1)) { } @@ -50,9 +65,9 @@ bool LibraryEntry::operator==(const LibraryEntry& other) const { } uint64_t LibraryEntry::checkHash() const { - return ::checkHash(filesize, crc32); + return ::checkHash(filesize, crc32, getSha1Prefix(sha1)); } uint64_t LibraryEntry::checkHash(const mLibraryEntry* entry) { - return ::checkHash(entry->filesize, entry->crc32); + return ::checkHash(entry->filesize, entry->crc32, getSha1Prefix(entry->sha1)); } diff --git a/src/platform/qt/library/LibraryEntry.h b/src/platform/qt/library/LibraryEntry.h index 3d8e6c8a8..cdd132840 100644 --- a/src/platform/qt/library/LibraryEntry.h +++ b/src/platform/qt/library/LibraryEntry.h @@ -37,6 +37,7 @@ struct LibraryEntry { int platformModels; size_t filesize; uint32_t crc32; + QByteArray sha1; LibraryEntry& operator=(const LibraryEntry&) = default; LibraryEntry& operator=(LibraryEntry&&) = default; diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp index db530c177..f700a18eb 100644 --- a/src/platform/qt/library/LibraryModel.cpp +++ b/src/platform/qt/library/LibraryModel.cpp @@ -22,25 +22,28 @@ static const QStringList iconSets{ "GBC", "GB", "SGB", - // "DS", }; +static QHash platformIcons; + 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.toLower()); - 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] = icon; + if (platformIcons.isEmpty()) { + for (const QString& platform : iconSets) { + QString pathTemplate = QStringLiteral(":/res/%1-icon%2").arg(platform.toLower()); + 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")); + platformIcons[platform] = icon; + } } } @@ -174,9 +177,13 @@ void LibraryModel::updateEntries(const QList& items) { } void LibraryModel::removeEntries(const QList& items) { - SpanSet removedRootSpans; + SpanSet removedRootSpans, removedGameSpans; QHash removedTreeSpans; int firstModifiedIndex = m_games.size(); + + // Remove the items from the game index and assemble a span + // set so that we can later inform the view of which rows + // were removed in an optimized way. for (const QString& item : items) { int pos = m_gameIndex.value(item, -1); Q_ASSERT(pos >= 0); @@ -189,12 +196,18 @@ void LibraryModel::removeEntries(const QList& items) { QList& pathItems = m_pathIndex[entry->base]; int pathPos = pathItems.indexOf(entry); Q_ASSERT(pathPos >= 0); + removedGameSpans.add(pos); removedTreeSpans[entry->base].add(pathPos); - if (!m_treeMode) { - removedRootSpans.add(pos); - } m_gameIndex.remove(item); } + + if (!m_treeMode) { + // If not using a tree view, all entries are root entries. + removedRootSpans = removedGameSpans; + } + + // Remove the paths from the path indexes. + // If it's a tree view, inform the view. for (const QString& base : removedTreeSpans.keys()) { SpanSet& spanSet = removedTreeSpans[base]; spanSet.merge(); @@ -223,6 +236,9 @@ void LibraryModel::removeEntries(const QList& items) { } } } + + // Remove the games from the backing store and path indexes, + // and tell the view to remove the root items. removedRootSpans.merge(); removedRootSpans.sort(true); for (const SpanSet::Span& span : removedRootSpans.spans) { @@ -233,10 +249,21 @@ void LibraryModel::removeEntries(const QList& items) { m_pathIndex.remove(base); } } else { + // In list view, remove games from the backing store immediately m_games.erase(m_games.begin() + span.left, m_games.begin() + span.right + 1); } endRemoveRows(); } + if (m_treeMode) { + // In tree view, remove them after cleaning up the path indexes. + removedGameSpans.merge(); + removedGameSpans.sort(true); + for (const SpanSet::Span& span : removedGameSpans.spans) { + m_games.erase(m_games.begin() + span.left, m_games.begin() + span.right + 1); + } + } + + // Finally, update the game index for the remaining items. for (int i = m_games.size() - 1; i >= firstModifiedIndex; i--) { m_gameIndex[m_games[i]->fullpath] = i; } @@ -294,8 +321,7 @@ int LibraryModel::rowCount(const QModelIndex& parent) const { return m_games.size(); } -QVariant LibraryModel::folderData(const QModelIndex& index, int role) const -{ +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); @@ -311,26 +337,34 @@ QVariant LibraryModel::folderData(const QModelIndex& index, int role) const } 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(); + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::TextAlignmentRole: + case FullPathRole: + break; + case Qt::ToolTipRole: + if (index.column() > COL_LOCATION) { + return QVariant(); + } + break; + case Qt::DecorationRole: + if (index.column() != COL_NAME) { + return QVariant(); + } + break; + default: + 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()) { @@ -341,26 +375,28 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const { } else if (!index.parent().isValid() && index.row() < (int)m_games.size()) { entry = m_games[index.row()].get(); } + 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); + case COL_NAME: + if (role == Qt::DecorationRole) { + return platformIcons.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(); } diff --git a/src/platform/qt/library/LibraryModel.h b/src/platform/qt/library/LibraryModel.h index 1d57c1c41..dd78026c8 100644 --- a/src/platform/qt/library/LibraryModel.h +++ b/src/platform/qt/library/LibraryModel.h @@ -17,6 +17,7 @@ #include "LibraryEntry.h" class QTreeView; +class LibraryModelTest; namespace QGBA { @@ -62,6 +63,8 @@ public: LibraryEntry entry(const QString& game) const; private: + friend class ::LibraryModelTest; + QModelIndex indexForPath(const QString& path); QModelIndex indexForPath(const QString& path) const; @@ -78,7 +81,6 @@ private: QStringList m_pathOrder; QHash> m_pathIndex; QHash m_gameIndex; - QHash m_icons; }; } diff --git a/src/platform/qt/test/library.cpp b/src/platform/qt/test/library.cpp index 2cda06694..c189f8259 100644 --- a/src/platform/qt/test/library.cpp +++ b/src/platform/qt/test/library.cpp @@ -128,13 +128,18 @@ private slots: void testList() { addTestGames1(); QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->m_games.size(), 3); addTestGames2(); QCOMPARE(model->rowCount(), 8); + QCOMPARE(model->m_games.size(), 8); updateGame(); + QCOMPARE(model->m_games.size(), 8); model->removeEntries({ "/gba/Another.gba", "/gb/Game 6.gb" }); QCOMPARE(model->rowCount(), 6); + QCOMPARE(model->m_games.size(), 6); model->removeEntries({ "/gb/Old Game.gb", "/gb/Game 7.gb" }); QCOMPARE(model->rowCount(), 4); + QCOMPARE(model->m_games.size(), 4); } void testTree() { @@ -144,19 +149,24 @@ private slots: QCOMPARE(model->rowCount(), 2); QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1); QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2); + QCOMPARE(model->m_games.size(), 3); addTestGames2(); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->rowCount(model->index(gbRow, 0)), 3); QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 5); + QCOMPARE(model->m_games.size(), 8); updateGame(); + QCOMPARE(model->m_games.size(), 8); removeGames1(); QCOMPARE(model->rowCount(), 2); QCOMPARE(model->rowCount(model->index(gbRow, 0)), 2); QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 4); + QCOMPARE(model->m_games.size(), 6); removeGames2(); QVERIFY2(!find("gb").isValid(), "did not remove gb folder"); QCOMPARE(model->rowCount(), 1); QCOMPARE(model->rowCount(model->index(0, 0)), 4); + QCOMPARE(model->m_games.size(), 4); } void modeSwitchTest1() { diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp index 1d12310ed..1df387e24 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -6,7 +6,9 @@ #include "utils.h" #include +#ifdef M_CORE_GB #include +#endif #include #include