From ba8671d1d6a55c122186382625b2f4ecc6a0fe57 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Tue, 28 Jun 2022 14:10:47 -0500 Subject: [PATCH 1/6] Library: rewrite Qt library frontend --- src/platform/qt/CMakeLists.txt | 37 +- src/platform/qt/library/LibraryController.cpp | 231 ++++++---- src/platform/qt/library/LibraryController.h | 76 ++-- src/platform/qt/library/LibraryEntry.cpp | 51 +++ src/platform/qt/library/LibraryEntry.h | 47 ++ src/platform/qt/library/LibraryModel.cpp | 417 ++++++++++++++++++ src/platform/qt/library/LibraryModel.h | 81 ++++ src/platform/qt/library/LibraryTree.cpp | 212 --------- src/platform/qt/library/LibraryTree.h | 61 --- src/platform/qt/resources.qrc | 18 + src/platform/qt/test/library.cpp | 200 +++++++++ src/platform/qt/test/spanset.cpp | 61 +++ src/platform/qt/utils.cpp | 42 ++ src/platform/qt/utils.h | 17 + 14 files changed, 1143 insertions(+), 408 deletions(-) create mode 100644 src/platform/qt/library/LibraryEntry.cpp create mode 100644 src/platform/qt/library/LibraryEntry.h create mode 100644 src/platform/qt/library/LibraryModel.cpp create mode 100644 src/platform/qt/library/LibraryModel.h delete mode 100644 src/platform/qt/library/LibraryTree.cpp delete mode 100644 src/platform/qt/library/LibraryTree.h create mode 100644 src/platform/qt/test/library.cpp create mode 100644 src/platform/qt/test/spanset.cpp diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 9858fddb0..560a6fa44 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -220,6 +220,11 @@ set(GB_SRC GBOverride.cpp PrinterView.cpp) +set(TEST_QT_spanset_SRC + test/spanset.cpp + utils.cpp + VFileDevice.cpp) + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libqt${QT_V}widgets${QT_V}") set(AUDIO_SRC) @@ -274,8 +279,15 @@ if(USE_SQLITE3) list(APPEND SOURCE_FILES ArchiveInspector.cpp library/LibraryController.cpp - library/LibraryGrid.cpp - library/LibraryTree.cpp) + library/LibraryEntry.cpp + library/LibraryModel.cpp) + + set(TEST_QT_library_SRC + library/LibraryEntry.cpp + library/LibraryModel.cpp + test/library.cpp + utils.cpp + VFileDevice.cpp) endif() if(USE_DISCORD_RPC) @@ -496,7 +508,7 @@ if(APPLE) set_source_files_properties("${COREAUDIO}" PROPERTIES MACOSX_PACKAGE_LOCATION Contents/PlugIns) set_source_files_properties("${QTAVFSERVICE}" PROPERTIES MACOSX_PACKAGE_LOCATION Contents/PlugIns) endif() - + install(CODE " include(BundleUtilities) set(BU_CHMOD_BUNDLE_ITEMS ON) @@ -539,6 +551,25 @@ elseif(WIN32) endif() debug_strip(${BINARY_NAME}-qt) + +if(BUILD_SUITE) + enable_testing() + find_package(${QT}Test) + if(${QT}Test_FOUND) + get_property(ALL_TESTS DIRECTORY PROPERTY VARIABLES) + list(FILTER ALL_TESTS INCLUDE REGEX "^TEST_QT_.*_SRC$") + foreach(TEST_SRC ${ALL_TESTS}) + string(REGEX REPLACE "^TEST_QT_(.*)_SRC$" "\\1" TEST_NAME ${TEST_SRC}) + add_executable(test-qt-${TEST_NAME} WIN32 ${${TEST_SRC}}) + target_link_libraries(test-qt-${TEST_NAME} ${PLATFORM_LIBRARY} ${BINARY_NAME} ${QT_LIBRARIES} ${QT}::Test) + set_target_properties(test-qt-${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "${FEATURE_DEFINES};${FUNCTION_DEFINES};${OS_DEFINES};${QT_DEFINES}" COMPILE_OPTIONS "${FEATURE_FLAGS}") + add_test(platform-qt-${TEST_NAME} test-qt-${TEST_NAME}) + endforeach() + else() + message("${QT}Test not found") + endif() +endif() + install(TARGETS ${BINARY_NAME}-qt RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT ${BINARY_NAME}-qt BUNDLE DESTINATION ${APPDIR} COMPONENT ${BINARY_NAME}-qt) diff --git a/src/platform/qt/library/LibraryController.cpp b/src/platform/qt/library/LibraryController.cpp index 2a1ba1546..ed78ce410 100644 --- a/src/platform/qt/library/LibraryController.cpp +++ b/src/platform/qt/library/LibraryController.cpp @@ -8,39 +8,16 @@ #include "ConfigController.h" #include "GBAApp.h" -#include "LibraryGrid.h" -#include "LibraryTree.h" +#include "LibraryModel.h" + +#include +#include +#include +#include +#include using namespace QGBA; -LibraryEntry::LibraryEntry(const mLibraryEntry* entry) - : base(entry->base) - , filename(entry->filename) - , fullpath(QString("%1/%2").arg(entry->base, entry->filename)) - , title(entry->title) - , internalTitle(entry->internalTitle) - , internalCode(entry->internalCode) - , platform(entry->platform) - , filesize(entry->filesize) - , crc32(entry->crc32) -{ -} - -void AbstractGameList::addEntry(const LibraryEntry& item) { - addEntries({item}); -} - -void AbstractGameList::updateEntry(const LibraryEntry& item) { - updateEntries({item}); -} - -void AbstractGameList::removeEntry(const QString& item) { - removeEntries({item}); -} -void AbstractGameList::setShowFilename(bool showFilename) { - m_showFilename = showFilename; -} - LibraryController::LibraryController(QWidget* parent, const QString& path, ConfigController* config) : QStackedWidget(parent) , m_config(config) @@ -55,14 +32,53 @@ LibraryController::LibraryController(QWidget* parent, const QString& path, Confi mLibraryAttachGameDB(m_library.get(), GBAApp::app()->gameDB()); - m_libraryTree = std::make_unique(this); - addWidget(m_libraryTree->widget()); + m_libraryModel = new LibraryModel(this); - m_libraryGrid = std::make_unique(this); - addWidget(m_libraryGrid->widget()); + m_treeView = new QTreeView(this); + addWidget(m_treeView); + m_treeModel = new QSortFilterProxyModel(this); + m_treeModel->setSourceModel(m_libraryModel); + m_treeModel->setSortRole(Qt::EditRole); + m_treeView->setModel(m_treeModel); + m_treeView->setSortingEnabled(true); + m_treeView->setAlternatingRowColors(true); - m_currentStyle = LibraryStyle::STYLE_TREE; // Make sure setViewStyle does something - setViewStyle(LibraryStyle::STYLE_LIST); + m_listView = new QListView(this); + addWidget(m_listView); + m_listModel = new QSortFilterProxyModel(this); + m_listModel->setSourceModel(m_libraryModel); + m_listModel->setSortRole(Qt::EditRole); + m_listView->setModel(m_listModel); + + QObject::connect(m_treeView, &QAbstractItemView::activated, this, &LibraryController::startGame); + QObject::connect(m_listView, &QAbstractItemView::activated, this, &LibraryController::startGame); + QObject::connect(m_treeView->header(), &QHeaderView::sortIndicatorChanged, this, &LibraryController::sortChanged); + + m_expandThrottle.setInterval(100); + m_expandThrottle.setSingleShot(true); + QObject::connect(&m_expandThrottle, &QTimer::timeout, this, qOverload<>(&LibraryController::resizeTreeView)); + QObject::connect(m_libraryModel, &QAbstractItemModel::modelReset, &m_expandThrottle, qOverload<>(&QTimer::start)); + 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); + + QVariant librarySort = m_config->getQtOption("librarySort"); + QVariant librarySortOrder = m_config->getQtOption("librarySortOrder"); + if (librarySort.isNull() || !librarySort.canConvert()) { + librarySort = 0; + } + if (librarySortOrder.isNull() || !librarySortOrder.canConvert()) { + librarySortOrder = Qt::AscendingOrder; + } + m_treeModel->sort(librarySort.toInt(), librarySortOrder.value()); + m_listModel->sort(0, Qt::AscendingOrder); refresh(); } @@ -73,32 +89,59 @@ void LibraryController::setViewStyle(LibraryStyle newStyle) { if (m_currentStyle == newStyle) { return; } - m_currentStyle = newStyle; - - AbstractGameList* newCurrentList = nullptr; - if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) { - newCurrentList = m_libraryTree.get(); - } else { - newCurrentList = m_libraryGrid.get(); + QString selected; + if (m_currentView) { + QModelIndex selectedIndex = m_currentView->selectionModel()->currentIndex(); + if (selectedIndex.isValid()) { + selected = selectedIndex.data(LibraryModel::FullPathRole).toString(); + } } - newCurrentList->selectEntry(selectedEntry().fullpath); - newCurrentList->setViewStyle(newStyle); - setCurrentWidget(newCurrentList->widget()); - m_currentList = newCurrentList; + + m_currentStyle = newStyle; + m_libraryModel->setTreeMode(newStyle == LibraryStyle::STYLE_TREE); + + QAbstractItemView* newView = m_listView; + if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) { + newView = m_treeView; + } + + setCurrentWidget(newView); + m_currentView = newView; + selectEntry(selected); +} + +void LibraryController::sortChanged(int column, Qt::SortOrder order) { + m_config->setQtOption("librarySort", column); + m_config->setQtOption("librarySortOrder", order); } void LibraryController::selectEntry(const QString& fullpath) { - if (!m_currentList) { + if (!m_currentView) { return; } - m_currentList->selectEntry(fullpath); + QModelIndex index = m_libraryModel->index(fullpath); + + // If the model is proxied in the current view, map the index to the proxy + QAbstractProxyModel* proxy = qobject_cast(m_currentView->model()); + if (proxy) { + index = proxy->mapFromSource(index); + } + + if (index.isValid()) { + m_currentView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + } } LibraryEntry LibraryController::selectedEntry() { - if (!m_currentList) { + if (!m_currentView) { return {}; } - return m_entries.value(m_currentList->selectedEntry()); + QModelIndex index = m_currentView->selectionModel()->currentIndex(); + if (!index.isValid()) { + return {}; + } + QString fullpath = index.data(LibraryModel::FullPathRole).toString(); + return m_libraryModel->entry(fullpath); } VFile* LibraryController::selectedVFile() { @@ -149,42 +192,34 @@ void LibraryController::refresh() { setDisabled(true); - QHash removedEntries = m_entries; - QHash updatedEntries; + QSet removedEntries = QSet::fromList(m_knownGames.keys()); + QList updatedEntries; QList newEntries; mLibraryListing listing; mLibraryListingInit(&listing, 0); mLibraryGetEntries(m_library.get(), &listing, 0, 0, nullptr); for (size_t i = 0; i < mLibraryListingSize(&listing); i++) { - LibraryEntry entry = mLibraryListingGetConstPointer(&listing, i); - if (!m_entries.contains(entry.fullpath)) { + const mLibraryEntry* entry = mLibraryListingGetConstPointer(&listing, i); + uint64_t checkHash = LibraryEntry::checkHash(entry); + QString fullpath = QStringLiteral("%1/%2").arg(entry->base, entry->filename); + if (!m_knownGames.contains(fullpath)) { newEntries.append(entry); - } else { - updatedEntries[entry.fullpath] = entry; + } else if (checkHash != m_knownGames[fullpath]) { + updatedEntries.append(entry); } - m_entries[entry.fullpath] = entry; - removedEntries.remove(entry.fullpath); + removedEntries.remove(fullpath); + m_knownGames[fullpath] = checkHash; } // Check for entries that were removed - for (QString& path : removedEntries.keys()) { - m_entries.remove(path); + for (const QString& path : removedEntries) { + m_knownGames.remove(path); } - if (!removedEntries.size() && !newEntries.size()) { - m_libraryTree->updateEntries(updatedEntries.values()); - m_libraryGrid->updateEntries(updatedEntries.values()); - } else if (!updatedEntries.size()) { - m_libraryTree->removeEntries(removedEntries.keys()); - m_libraryGrid->removeEntries(removedEntries.keys()); - - m_libraryTree->addEntries(newEntries); - m_libraryGrid->addEntries(newEntries); - } else { - m_libraryTree->resetEntries(m_entries.values()); - m_libraryGrid->resetEntries(m_entries.values()); - } + m_libraryModel->removeEntries(removedEntries.toList()); + m_libraryModel->updateEntries(updatedEntries); + m_libraryModel->addEntries(newEntries); for (size_t i = 0; i < mLibraryListingSize(&listing); ++i) { mLibraryEntryFree(mLibraryListingGetPointer(&listing, i)); @@ -201,7 +236,7 @@ void LibraryController::selectLastBootedGame() { return; } const QString lastfile = m_config->getMRU().first(); - if (m_entries.contains(lastfile)) { + if (m_knownGames.contains(lastfile)) { selectEntry(lastfile); } } @@ -213,16 +248,50 @@ void LibraryController::loadDirectory(const QString& dir, bool recursive) { mLibraryLoadDirectory(library.get(), dir.toUtf8().constData(), recursive); m_libraryJob.testAndSetOrdered(libraryJob, -1); } + void LibraryController::setShowFilename(bool showFilename) { if (showFilename == m_showFilename) { return; } m_showFilename = showFilename; - if (m_libraryGrid) { - m_libraryGrid->setShowFilename(m_showFilename); - } - if (m_libraryTree) { - m_libraryTree->setShowFilename(m_showFilename); - } + m_libraryModel->setShowFilename(m_showFilename); refresh(); } + +void LibraryController::showEvent(QShowEvent*) { + resizeTreeView(false); +} + +void LibraryController::resizeEvent(QResizeEvent*) { + resizeTreeView(false); +} + +void LibraryController::resizeTreeView(bool expand) { + if (expand) { + m_treeView->expandAll(); + } + + 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 (totalWidth < viewportWidth) { + totalWidth = 0; + for (int column = 0; column <= LibraryModel::MAX_COLUMN; column++) { + m_treeView->resizeColumnToContents(column); + totalWidth += m_treeView->columnWidth(column); + } + } + if (totalWidth > viewportWidth) { + int locationWidth = m_treeView->columnWidth(LibraryModel::COL_LOCATION); + if (locationWidth > 100) { + int newLocationWidth = m_treeView->viewport()->width() - (totalWidth - locationWidth); + if (newLocationWidth < 100) { + 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 1b5d06572..8a14f3a4b 100644 --- a/src/platform/qt/library/LibraryController.h +++ b/src/platform/qt/library/LibraryController.h @@ -12,15 +12,21 @@ #include #include #include +#include #include +#include "LibraryEntry.h" + +class QAbstractItemView; +class QListView; +class QSortFilterProxyModel; +class QTreeView; + namespace QGBA { -// Predefinitions -class LibraryGrid; -class LibraryTree; class ConfigController; +class LibraryModel; enum class LibraryStyle { STYLE_LIST = 0, @@ -29,50 +35,6 @@ enum class LibraryStyle { STYLE_ICON }; -struct LibraryEntry { - LibraryEntry() {} - LibraryEntry(const mLibraryEntry* entry); - - bool isNull() const { return fullpath.isNull(); } - - QString displayTitle() const { return title.isNull() ? filename : title; } - - QString base; - QString filename; - QString fullpath; - QString title; - QByteArray internalTitle; - QByteArray internalCode; - mPlatform platform; - size_t filesize; - uint32_t crc32; - - bool operator==(const LibraryEntry& other) const { return other.fullpath == fullpath; } -}; - -class AbstractGameList { -public: - virtual QString selectedEntry() = 0; - virtual void selectEntry(const QString& fullpath) = 0; - - virtual void setViewStyle(LibraryStyle newStyle) = 0; - - virtual void resetEntries(const QList&) = 0; - virtual void addEntries(const QList&) = 0; - virtual void updateEntries(const QList&) = 0; - virtual void removeEntries(const QList&) = 0; - - virtual void addEntry(const LibraryEntry&); - virtual void updateEntry(const LibraryEntry&); - virtual void removeEntry(const QString&); - virtual void setShowFilename(bool showFilename); - - virtual QWidget* widget() = 0; - -protected: - bool m_showFilename = false; -}; - class LibraryController final : public QStackedWidget { Q_OBJECT @@ -103,6 +65,13 @@ signals: private slots: void refresh(); + void sortChanged(int column, Qt::SortOrder order); + inline void resizeTreeView() { resizeTreeView(true); } + void resizeTreeView(bool expand); + +protected: + void showEvent(QShowEvent*) override; + void resizeEvent(QResizeEvent*) override; private: void loadDirectory(const QString&, bool recursive = true); // Called on separate thread @@ -110,14 +79,19 @@ private: ConfigController* m_config = nullptr; std::shared_ptr m_library; QAtomicInteger m_libraryJob = -1; - QHash m_entries; LibraryStyle m_currentStyle; - AbstractGameList* m_currentList = nullptr; - std::unique_ptr m_libraryGrid; - std::unique_ptr m_libraryTree; + QHash m_knownGames; + LibraryModel* m_libraryModel; + QSortFilterProxyModel* m_listModel; + QSortFilterProxyModel* m_treeModel; + QListView* m_listView; + QTreeView* m_treeView; + QAbstractItemView* m_currentView = nullptr; bool m_showFilename = false; + + QTimer m_expandThrottle; }; } diff --git a/src/platform/qt/library/LibraryEntry.cpp b/src/platform/qt/library/LibraryEntry.cpp new file mode 100644 index 000000000..27feb64e8 --- /dev/null +++ b/src/platform/qt/library/LibraryEntry.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2014-2017 waddlesplash + * Copyright (c) 2013-2021 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 "LibraryEntry.h" + +#include + +using namespace QGBA; + +static inline uint64_t checkHash(size_t filesize, uint32_t crc32) { + return (uint64_t(filesize) << 32) ^ ((crc32 + 1ULL) * (uint32_t(filesize) + 1ULL)); +} + +LibraryEntry::LibraryEntry(const mLibraryEntry* entry) + : base(entry->base) + , filename(entry->filename) + , fullpath(QString("%1/%2").arg(entry->base, entry->filename)) + , title(entry->title) + , internalTitle(entry->internalTitle) + , internalCode(entry->internalCode) + , platform(entry->platform) + , filesize(entry->filesize) + , crc32(entry->crc32) +{ +} + +bool LibraryEntry::isNull() const { + return fullpath.isNull(); +} + +QString LibraryEntry::displayTitle(bool showFilename) const { + if (showFilename || title.isNull()) { + return filename; + } + return title; +} + +bool LibraryEntry::operator==(const LibraryEntry& other) const { + return other.fullpath == fullpath; +} + +uint64_t LibraryEntry::checkHash() const { + return ::checkHash(filesize, crc32); +} + +uint64_t LibraryEntry::checkHash(const mLibraryEntry* entry) { + return ::checkHash(entry->filesize, entry->crc32); +} diff --git a/src/platform/qt/library/LibraryEntry.h b/src/platform/qt/library/LibraryEntry.h new file mode 100644 index 000000000..2a733db4a --- /dev/null +++ b/src/platform/qt/library/LibraryEntry.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2014-2017 waddlesplash + * Copyright (c) 2013-2021 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/. */ +#pragma once + +#include +#include +#include + +#include + +struct mLibraryEntry; + +namespace QGBA { + +struct LibraryEntry { + LibraryEntry() = default; + LibraryEntry(const LibraryEntry&) = default; + LibraryEntry(LibraryEntry&&) = default; + LibraryEntry(const mLibraryEntry* entry); + + bool isNull() const; + + QString displayTitle(bool showFilename = false) const; + + QString base; + QString filename; + QString fullpath; + QString title; + QByteArray internalTitle; + QByteArray internalCode; + mPlatform platform; + size_t filesize; + uint32_t crc32; + + LibraryEntry& operator=(const LibraryEntry&) = default; + LibraryEntry& operator=(LibraryEntry&&) = default; + bool operator==(const LibraryEntry& other) const; + + uint64_t checkHash() const; + static uint64_t checkHash(const mLibraryEntry* entry); +}; + +}; diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp new file mode 100644 index 000000000..bc08228c1 --- /dev/null +++ b/src/platform/qt/library/LibraryModel.cpp @@ -0,0 +1,417 @@ +/* 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 +#include +#include +#include +#include + +#include + +using namespace QGBA; + +static const QStringList iconSets{ + "GBA", + "GBC", + "GB", + // "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& items) { + beginResetModel(); + blockSignals(true); + + m_games.clear(); + m_pathOrder.clear(); + m_pathIndex.clear(); + addEntriesList(items); + + blockSignals(false); + endResetModel(); +} + +void LibraryModel::addEntries(const QList& 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& 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& items) { + QHash> byPath; + QHash> 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& pathItems = m_pathIndex[base]; + QList& 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& items) { + QHash 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& items) { + SpanSet removedRootSpans; + QHash 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& 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& 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(nicePlatformFormat(entry->platform), 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(); + 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]; +} diff --git a/src/platform/qt/library/LibraryModel.h b/src/platform/qt/library/LibraryModel.h new file mode 100644 index 000000000..db220392f --- /dev/null +++ b/src/platform/qt/library/LibraryModel.h @@ -0,0 +1,81 @@ +/* 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/. */ +#pragma once + +#include +#include +#include + +#include + +#include "LibraryEntry.h" + +class QTreeView; + +namespace QGBA { + +class LibraryModel final : public QAbstractItemModel { +Q_OBJECT + +public: + enum Columns { + COL_NAME = 0, + COL_LOCATION = 1, + COL_PLATFORM = 2, + COL_SIZE = 3, + COL_CRC32 = 4, + MAX_COLUMN = 4, + }; + + enum ItemDataRole { + FullPathRole = Qt::UserRole + 1, + }; + + explicit LibraryModel(QObject* parent = nullptr); + + bool treeMode() const; + void setTreeMode(bool tree); + + bool showFilename() const; + void setShowFilename(bool show); + + void resetEntries(const QList& items); + void addEntries(const QList& items); + void updateEntries(const QList& items); + void removeEntries(const QList& items); + + QModelIndex index(const QString& game) const; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + LibraryEntry entry(const QString& game) const; + +private: + QModelIndex indexForPath(const QString& path); + QModelIndex indexForPath(const QString& path) const; + + QVariant folderData(const QModelIndex& index, int role = Qt::DisplayRole) const; + + void addEntriesList(const QList& items); + void addEntriesTree(const QList& items); + void addEntryInternal(const LibraryEntry& item); + + bool m_treeMode; + bool m_showFilename; + + QList m_games; + QStringList m_pathOrder; + QHash> m_pathIndex; + QHash m_gameIndex; + QHash m_icons; +}; + +} diff --git a/src/platform/qt/library/LibraryTree.cpp b/src/platform/qt/library/LibraryTree.cpp deleted file mode 100644 index c9029e963..000000000 --- a/src/platform/qt/library/LibraryTree.cpp +++ /dev/null @@ -1,212 +0,0 @@ -/* Copyright (c) 2014-2017 waddlesplash - * 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 "LibraryTree.h" - -#include "utils.h" - -#include -#include - -using namespace QGBA; - -namespace QGBA { - -class LibraryTreeItem : public QTreeWidgetItem { -public: - LibraryTreeItem(QTreeWidget* parent = nullptr) : QTreeWidgetItem(parent) {} - void setFilesize(size_t size); - - virtual bool operator<(const QTreeWidgetItem& other) const override; -protected: - size_t m_size = 0; -}; - -} - -void LibraryTreeItem::setFilesize(size_t size) { - m_size = size; - setText(LibraryTree::COL_SIZE, niceSizeFormat(size)); -} - -bool LibraryTreeItem::operator<(const QTreeWidgetItem& other) const { - const int column = treeWidget()->sortColumn(); - return ((column == LibraryTree::COL_SIZE) ? - m_size < dynamic_cast(&other)->m_size : - QTreeWidgetItem::operator<(other)); -} - -LibraryTree::LibraryTree(LibraryController* parent) - : m_widget(new QTreeWidget(parent)) - , m_controller(parent) -{ - m_widget->setObjectName("LibraryTree"); - m_widget->setSortingEnabled(true); - m_widget->setAlternatingRowColors(true); - - QTreeWidgetItem* header = new QTreeWidgetItem({ - QApplication::translate("QGBA::LibraryTree", "Name", nullptr), - QApplication::translate("QGBA::LibraryTree", "Location", nullptr), - QApplication::translate("QGBA::LibraryTree", "Platform", nullptr), - QApplication::translate("QGBA::LibraryTree", "Size", nullptr), - QApplication::translate("QGBA::LibraryTree", "CRC32", nullptr), - }); - header->setTextAlignment(3, Qt::AlignTrailing | Qt::AlignVCenter); - m_widget->setHeaderItem(header); - - setViewStyle(LibraryStyle::STYLE_TREE); - m_widget->sortByColumn(COL_NAME, Qt::AscendingOrder); - - QObject::connect(m_widget, &QTreeWidget::itemActivated, [this](QTreeWidgetItem* item, int) -> void { - if (m_items.values().contains(item)) { - emit m_controller->startGame(); - } - }); -} - -LibraryTree::~LibraryTree() { - m_widget->clear(); -} - -void LibraryTree::resizeAllCols() { - for (int i = 0; i < m_widget->columnCount(); i++) { - m_widget->resizeColumnToContents(i); - } -} - -QString LibraryTree::selectedEntry() { - if (!m_widget->selectedItems().empty()) { - return m_items.key(m_widget->selectedItems().at(0)); - } else { - return {}; - } -} - -void LibraryTree::selectEntry(const QString& game) { - if (game.isNull()) { - return; - } - m_widget->setCurrentItem(m_items.value(game)); -} - -void LibraryTree::setViewStyle(LibraryStyle newStyle) { - if (newStyle == LibraryStyle::STYLE_LIST) { - m_widget->setIndentation(0); - } else { - m_widget->setIndentation(20); - } - m_currentStyle = newStyle; - rebuildTree(); -} - -void LibraryTree::resetEntries(const QList& items) { - m_deferredTreeRebuild = true; - m_entries.clear(); - m_pathNodes.clear(); - addEntries(items); -} - -void LibraryTree::addEntries(const QList& items) { - m_deferredTreeRebuild = true; - for (const auto& item : items) { - addEntry(item); - } - m_deferredTreeRebuild = false; - rebuildTree(); -} - -void LibraryTree::addEntry(const LibraryEntry& item) { - m_entries[item.fullpath] = item; - - QString folder = item.base; - if (!m_pathNodes.contains(folder)) { - m_pathNodes.insert(folder, 1); - } else { - ++m_pathNodes[folder]; - } - - rebuildTree(); -} - -void LibraryTree::updateEntries(const QList& items) { - for (const auto& item : items) { - updateEntry(item); - } -} - -void LibraryTree::updateEntry(const LibraryEntry& item) { - m_entries[item.fullpath] = item; - - LibraryTreeItem* i = static_cast(m_items.value(item.fullpath)); - i->setText(COL_NAME, m_showFilename ? item.filename : item.displayTitle()); - i->setText(COL_PLATFORM, nicePlatformFormat(item.platform)); - i->setFilesize(item.filesize); - i->setText(COL_CRC32, QString("%0").arg(item.crc32, 8, 16, QChar('0'))); -} - -void LibraryTree::removeEntries(const QList& items) { - m_deferredTreeRebuild = true; - for (const auto& item : items) { - removeEntry(item); - } - m_deferredTreeRebuild = false; - rebuildTree(); -} - -void LibraryTree::removeEntry(const QString& item) { - if (!m_entries.contains(item)) { - return; - } - QString folder = m_entries.value(item).base; - --m_pathNodes[folder]; - if (m_pathNodes[folder] <= 0) { - m_pathNodes.remove(folder); - } - - m_entries.remove(item); - rebuildTree(); -} - -void LibraryTree::rebuildTree() { - if (m_deferredTreeRebuild) { - return; - } - - QString currentGame = selectedEntry(); - m_widget->clear(); - m_items.clear(); - - QHash pathNodes; - if (m_currentStyle == LibraryStyle::STYLE_TREE) { - for (const QString& folder : m_pathNodes.keys()) { - QTreeWidgetItem* i = new LibraryTreeItem; - pathNodes.insert(folder, i); - i->setText(0, folder.section("/", -1)); - m_widget->addTopLevelItem(i); - } - } - - for (const auto& item : m_entries.values()) { - LibraryTreeItem* i = new LibraryTreeItem; - i->setText(COL_NAME, item.displayTitle()); - i->setText(COL_LOCATION, QDir::toNativeSeparators(item.base)); - i->setText(COL_PLATFORM, nicePlatformFormat(item.platform)); - i->setFilesize(item.filesize); - i->setTextAlignment(COL_SIZE, Qt::AlignTrailing | Qt::AlignVCenter); - i->setText(COL_CRC32, QString("%0").arg(item.crc32, 8, 16, QChar('0'))); - m_items.insert(item.fullpath, i); - - if (m_currentStyle == LibraryStyle::STYLE_TREE) { - pathNodes.value(item.base)->addChild(i); - } else { - m_widget->addTopLevelItem(i); - } - } - - m_widget->expandAll(); - resizeAllCols(); - selectEntry(currentGame); -} diff --git a/src/platform/qt/library/LibraryTree.h b/src/platform/qt/library/LibraryTree.h deleted file mode 100644 index 28354f3ec..000000000 --- a/src/platform/qt/library/LibraryTree.h +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright (c) 2014-2017 waddlesplash - * - * 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/. */ -#pragma once - -#include - -#include "LibraryController.h" - -namespace QGBA { - -class LibraryTreeItem; - -class LibraryTree final : public AbstractGameList { - -public: - enum Columns { - COL_NAME = 0, - COL_LOCATION = 1, - COL_PLATFORM = 2, - COL_SIZE = 3, - COL_CRC32 = 4, - }; - - explicit LibraryTree(LibraryController* parent = nullptr); - ~LibraryTree(); - - QString selectedEntry() override; - void selectEntry(const QString& fullpath) override; - - void setViewStyle(LibraryStyle newStyle) override; - - void resetEntries(const QList& items) override; - void addEntries(const QList& items) override; - void updateEntries(const QList& items) override; - void removeEntries(const QList& items) override; - - void addEntry(const LibraryEntry& items) override; - void updateEntry(const LibraryEntry& items) override; - void removeEntry(const QString& items) override; - - QWidget* widget() override { return m_widget; } - -private: - QTreeWidget* m_widget; - LibraryStyle m_currentStyle; - - LibraryController* m_controller; - - bool m_deferredTreeRebuild = false; - QHash m_entries; - QHash m_items; - QHash m_pathNodes; - - void rebuildTree(); - void resizeAllCols(); -}; - -} diff --git a/src/platform/qt/resources.qrc b/src/platform/qt/resources.qrc index e4b8fab5f..271440f67 100644 --- a/src/platform/qt/resources.qrc +++ b/src/platform/qt/resources.qrc @@ -7,6 +7,24 @@ ../../../res/keymap.qpic ../../../res/patrons.txt ../../../res/no-cam.png + ../../../res/gb-icon-256.png + ../../../res/gb-icon-128.png + ../../../res/gb-icon-32.png + ../../../res/gb-icon-24.png + ../../../res/gb-icon-16.png + ../../../res/gb-icon.svg + ../../../res/gbc-icon-256.png + ../../../res/gbc-icon-128.png + ../../../res/gbc-icon-32.png + ../../../res/gbc-icon-24.png + ../../../res/gbc-icon-16.png + ../../../res/gbc-icon.svg + ../../../res/gba-icon-256.png + ../../../res/gba-icon-128.png + ../../../res/gba-icon-32.png + ../../../res/gba-icon-24.png + ../../../res/gba-icon-16.png + ../../../res/gba-icon.svg ../../../res/exe4/chip-names.txt diff --git a/src/platform/qt/test/library.cpp b/src/platform/qt/test/library.cpp new file mode 100644 index 000000000..2cda06694 --- /dev/null +++ b/src/platform/qt/test/library.cpp @@ -0,0 +1,200 @@ +/* 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 "platform/qt/library/LibraryModel.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) +#include +#endif + +#include +#include + +#define FIND_GBA_ROW(gba, gb) \ + int gba = findGBARow(); \ + if (gba < 0) QFAIL("Could not find gba row"); \ + int gb = 1 - gba; + +using namespace QGBA; + +class LibraryModelTest : public QObject { +Q_OBJECT + +private: + LibraryModel* model = nullptr; + + int findGBARow() { + for (int i = 0; i < model->rowCount(); i++) { + if (model->index(i, 0).data() == "gba") { + return i; + } + } + return -1; + } + + LibraryEntry makeGBA(const QString& name, uint32_t crc) { + LibraryEntry entry; + entry.base = "/gba"; + entry.filename = name + ".gba"; + entry.fullpath = entry.base + "/" + entry.filename; + entry.title = name; + entry.internalTitle = name.toUpper().toUtf8(); + entry.internalCode = entry.internalTitle.replace(" ", "").left(4); + entry.platform = mPLATFORM_GBA; + entry.filesize = entry.fullpath.size() * 4; + entry.crc32 = crc; + return entry; + } + + LibraryEntry makeGB(const QString& name, uint32_t crc) { + LibraryEntry entry = makeGBA(name, crc); + entry.base = "/gb"; + entry.filename = entry.filename.replace("gba", "gb"); + entry.fullpath = entry.fullpath.replace("gba", "gb"); + entry.platform = mPLATFORM_GB; + entry.filesize /= 4; + return entry; + } + + void addTestGames1() { + model->addEntries({ + makeGBA("Test Game", 0x12345678), + makeGBA("Another", 0x23456789), + makeGB("Old Game", 0x87654321), + }); + } + + void addTestGames2() { + model->addEntries({ + makeGBA("Game 3", 0x12345679), + makeGBA("Game 4", 0x2345678A), + makeGBA("Game 5", 0x2345678B), + makeGB("Game 6", 0x87654322), + makeGB("Game 7", 0x87654323), + }); + } + + void updateGame() { + LibraryEntry game = makeGBA("Another", 0x88888888); + model->updateEntries({ game }); + QModelIndex idx = find("Another"); + QVERIFY2(idx.isValid(), "game not found"); + QCOMPARE(idx.siblingAtColumn(LibraryModel::COL_CRC32).data(Qt::EditRole).toInt(), 0x88888888); + } + + void removeGames1() { + model->removeEntries({ "/gba/Another.gba", "/gb/Game 6.gb" }); + QVERIFY2(!find("Another").isValid(), "game not removed"); + QVERIFY2(!find("Game 6").isValid(), "game not removed"); + } + + void removeGames2() { + model->removeEntries({ "/gb/Old Game.gb", "/gb/Game 7.gb" }); + QVERIFY2(!find("Old Game").isValid(), "game not removed"); + QVERIFY2(!find("Game 7").isValid(), "game not removed"); + } + + QModelIndex find(const QString& name) { + for (int i = 0; i < model->rowCount(); i++) { + QModelIndex idx = model->index(i, 0); + if (idx.data().toString() == name) { + return idx; + } + for (int j = 0; j < model->rowCount(idx); j++) { + QModelIndex child = model->index(j, 0, idx); + if (child.data().toString() == name) { + return child; + } + } + } + return QModelIndex(); + } + +private slots: + void init() { + model = new LibraryModel(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + new QAbstractItemModelTester(model, QAbstractItemModelTester::FailureReportingMode::QtTest, model); +#endif + } + + void cleanup() { + delete model; + model = nullptr; + } + + void testList() { + addTestGames1(); + QCOMPARE(model->rowCount(), 3); + addTestGames2(); + QCOMPARE(model->rowCount(), 8); + updateGame(); + model->removeEntries({ "/gba/Another.gba", "/gb/Game 6.gb" }); + QCOMPARE(model->rowCount(), 6); + model->removeEntries({ "/gb/Old Game.gb", "/gb/Game 7.gb" }); + QCOMPARE(model->rowCount(), 4); + } + + void testTree() { + model->setTreeMode(true); + addTestGames1(); + FIND_GBA_ROW(gbaRow, gbRow); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2); + addTestGames2(); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 3); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 5); + updateGame(); + removeGames1(); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 2); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 4); + removeGames2(); + QVERIFY2(!find("gb").isValid(), "did not remove gb folder"); + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->rowCount(model->index(0, 0)), 4); + } + + void modeSwitchTest1() { + addTestGames1(); + { + QSignalSpy resetSpy(model, SIGNAL(modelReset())); + model->setTreeMode(true); + QVERIFY(resetSpy.count()); + } + FIND_GBA_ROW(gbaRow, gbRow); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2); + { + QSignalSpy resetSpy(model, SIGNAL(modelReset())); + model->setTreeMode(false); + QVERIFY(resetSpy.count()); + } + addTestGames2(); + QCOMPARE(model->rowCount(), 8); + } + + void modeSwitchTest2() { + model->setTreeMode(false); + addTestGames1(); + model->setTreeMode(true); + FIND_GBA_ROW(gbaRow, gbRow); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 1); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 2); + addTestGames2(); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->rowCount(model->index(gbRow, 0)), 3); + QCOMPARE(model->rowCount(model->index(gbaRow, 0)), 5); + model->setTreeMode(false); + QCOMPARE(model->rowCount(), 8); + } +}; + +QTEST_MAIN(LibraryModelTest) +#include "library.moc" diff --git a/src/platform/qt/test/spanset.cpp b/src/platform/qt/test/spanset.cpp new file mode 100644 index 000000000..45a2a1e25 --- /dev/null +++ b/src/platform/qt/test/spanset.cpp @@ -0,0 +1,61 @@ +/* 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 "platform/qt/utils.h" + +#include + +using namespace QGBA; + +class SpanSetTest : public QObject { +Q_OBJECT + +private: + void debugSpans(const SpanSet& spanSet) { + QStringList debug; + for (auto span : spanSet.spans) { + debug << QStringLiteral("[%1, %2]").arg(span.left).arg(span.right); + } + qDebug() << QStringLiteral("SpanSet{%1}").arg(debug.join(", ")); + } + +private slots: + void oneSpan() { + SpanSet spanSet; + spanSet.add(1); + spanSet.add(2); + spanSet.add(3); + QCOMPARE(spanSet.spans.size(), 1); + spanSet.merge(); + QCOMPARE(spanSet.spans.size(), 1); + } + + void twoSpans() { + SpanSet spanSet; + spanSet.add(1); + spanSet.add(2); + spanSet.add(4); + QCOMPARE(spanSet.spans.size(), 2); + spanSet.merge(); + QCOMPARE(spanSet.spans.size(), 2); + } + + void mergeSpans() { + SpanSet spanSet; + spanSet.add(1); + spanSet.add(3); + spanSet.add(2); + spanSet.add(5); + spanSet.add(4); + spanSet.add(7); + spanSet.add(8); + QCOMPARE(spanSet.spans.size(), 4); + spanSet.merge(); + QCOMPARE(spanSet.spans.size(), 2); + } +}; + +QTEST_APPLESS_MAIN(SpanSetTest) +#include "spanset.moc" diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp index c445c41b8..e6d3bbe99 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -6,6 +6,7 @@ #include "utils.h" #include +#include #include #include @@ -174,6 +175,47 @@ QString keyName(int key) { return QObject::tr("Menu"); default: return QKeySequence(key).toString(QKeySequence::NativeText); + } +} + +void SpanSet::add(int pos) { + for (Span& span : spans) { + if (pos == span.left - 1) { + span.left = pos; + return; + } else if (pos == span.right + 1) { + span.right = pos; + return; + } + } + spans << Span{ pos, pos }; +} + +void SpanSet::merge() { + int numSpans = spans.size(); + if (!numSpans) { + return; + } + sort(); + QVector merged({ spans[0] }); + int lastRight = merged[0].right; + for (int i = 1; i < numSpans; i++) { + int right = spans[i].right; + if (spans[i].left - 1 <= lastRight) { + merged.back().right = right; + } else { + merged << spans[i]; + } + lastRight = right; + } + spans = merged; +} + +void SpanSet::sort(bool reverse) { + if (reverse) { + std::sort(spans.begin(), spans.end(), std::greater()); + } else { + std::sort(spans.begin(), spans.end()); } } diff --git a/src/platform/qt/utils.h b/src/platform/qt/utils.h index cad2595b5..e6cb81929 100644 --- a/src/platform/qt/utils.h +++ b/src/platform/qt/utils.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -117,4 +118,20 @@ bool extractMatchingFile(VDir* dir, std::function filter); QString keyName(int key); +struct SpanSet { + struct Span { + int left; + int right; + + inline bool operator<(const Span& other) const { return left < other.left; } + inline bool operator>(const Span& other) const { return left > other.left; } + }; + + void add(int pos); + void merge(); + void sort(bool reverse = false); + + QVector spans; +}; + } From 447054674d0368f942d85347fff4173b98ff36b9 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Sat, 2 Jul 2022 14:10:48 -0500 Subject: [PATCH 2/6] Library: store platform models in database, render GBC/SGB icons --- include/mgba/core/library.h | 3 ++ src/core/library.c | 64 +++++++++++++++++++++++- src/platform/qt/library/LibraryEntry.cpp | 7 +++ src/platform/qt/library/LibraryEntry.h | 2 + src/platform/qt/library/LibraryModel.cpp | 3 +- src/platform/qt/resources.qrc | 6 +++ src/platform/qt/utils.cpp | 12 ++++- src/platform/qt/utils.h | 2 +- 8 files changed, 94 insertions(+), 5 deletions(-) diff --git a/include/mgba/core/library.h b/include/mgba/core/library.h index cfdd4cc60..eb819361e 100644 --- a/include/mgba/core/library.h +++ b/include/mgba/core/library.h @@ -15,6 +15,8 @@ CXX_GUARD_START #include #include +#define M_LIBRARY_MODEL_UNKNOWN -1 + struct mLibraryEntry { const char* base; const char* filename; @@ -26,6 +28,7 @@ struct mLibraryEntry { uint32_t crc32; uint8_t md5[16]; uint8_t sha1[20]; + int platformModels; }; #ifdef USE_SQLITE3 diff --git a/src/core/library.c b/src/core/library.c index 638f49880..8112ffc1a 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -9,6 +9,11 @@ #include #include +#ifdef M_CORE_GB +#include +#include +#endif + #ifdef USE_SQLITE3 #include @@ -108,12 +113,38 @@ static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry sqlite3_bind_int(statement, useIndex, 1); sqlite3_bind_int(statement, index, constraints->platform); } + + if (constraints->platformModels != M_LIBRARY_MODEL_UNKNOWN) { + index = sqlite3_bind_parameter_index(statement, ":models"); + sqlite3_bind_int(statement, index, constraints->platformModels); + } } struct mLibrary* mLibraryCreateEmpty(void) { return mLibraryLoad(":memory:"); } +static int _mLibraryTableVersion(struct mLibrary* library, const char* tableName) { + int version = -1; + + static const char getVersion[] = "SELECT version FROM version WHERE tname=?"; + sqlite3_stmt* getVersionStmt; + if (sqlite3_prepare_v2(library->db, getVersion, -1, &getVersionStmt, NULL)) { + goto error; + } + + sqlite3_clear_bindings(getVersionStmt); + sqlite3_reset(getVersionStmt); + sqlite3_bind_text(getVersionStmt, 1, tableName, -1, SQLITE_TRANSIENT); + if (sqlite3_step(getVersionStmt) != SQLITE_DONE) { + version = sqlite3_column_int(getVersionStmt, 0); + } + +error: + sqlite3_finalize(getVersionStmt); + return version; +} + struct mLibrary* mLibraryLoad(const char* path) { struct mLibrary* library = malloc(sizeof(*library)); memset(library, 0, sizeof(*library)); @@ -140,6 +171,7 @@ struct mLibrary* mLibraryLoad(const char* path) { "\n internalTitle TEXT," "\n internalCode TEXT," "\n platform INTEGER NOT NULL DEFAULT -1," + "\n models INTEGER NULL," "\n size INTEGER," "\n crc32 INTEGER," "\n md5 BLOB," @@ -159,18 +191,35 @@ struct mLibrary* mLibraryLoad(const char* path) { "\n CREATE INDEX IF NOT EXISTS sha1 ON roms (sha1);" "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('version', 1);" "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roots', 1);" - "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 1);" + "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 2);" "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('paths', 1);"; if (sqlite3_exec(library->db, createTables, NULL, NULL, NULL)) { goto error; } + int romsTableVersion = _mLibraryTableVersion(library, "roms"); + if (romsTableVersion < 0) { + goto error; + } else if (romsTableVersion < 2) { + static const char upgradeRomsTable[] = + " ALTER TABLE roms" + "\nADD COLUMN models INTEGER NULL"; + if (sqlite3_exec(library->db, upgradeRomsTable, NULL, NULL, NULL)) { + goto error; + } + + static const char updateRomsTableVersion[] = "UPDATE version SET version=2 WHERE tname='roms'"; + if (sqlite3_exec(library->db, updateRomsTableVersion, NULL, NULL, NULL)) { + goto error; + } + } + static const char insertPath[] = "INSERT INTO paths (romid, path, customTitle, rootid) VALUES (?, ?, ?, ?);"; if (sqlite3_prepare_v2(library->db, insertPath, -1, &library->insertPath, NULL)) { goto error; } - static const char insertRom[] = "INSERT INTO roms (crc32, md5, sha1, size, internalCode, platform) VALUES (:crc32, :md5, :sha1, :size, :internalCode, :platform);"; + 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; } @@ -318,6 +367,15 @@ bool _mLibraryAddEntry(struct mLibrary* library, const char* filename, const cha core->checksum(core, &entry.md5, mCHECKSUM_MD5); core->checksum(core, &entry.sha1, mCHECKSUM_SHA1); entry.platform = core->platform(core); + entry.platformModels = M_LIBRARY_MODEL_UNKNOWN; +#ifdef M_CORE_GB + if (entry.platform == mPLATFORM_GB) { + struct GB* gb = (struct GB*) core->board; + if (gb->memory.rom) { + entry.platformModels = GBValidModels(gb->memory.rom); + } + } +#endif entry.title = NULL; entry.base = base; entry.filename = filename; @@ -448,6 +506,8 @@ size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, } } else if (strcmp(colName, "platform") == 0) { entry->platform = sqlite3_column_int(library->select, i); + } else if (strcmp(colName, "models") == 0) { + entry->platformModels = sqlite3_column_int(library->select, i); } else if (strcmp(colName, "size") == 0) { entry->filesize = sqlite3_column_int64(library->select, i); } else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) { diff --git a/src/platform/qt/library/LibraryEntry.cpp b/src/platform/qt/library/LibraryEntry.cpp index 27feb64e8..25a5976c9 100644 --- a/src/platform/qt/library/LibraryEntry.cpp +++ b/src/platform/qt/library/LibraryEntry.cpp @@ -6,6 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "LibraryEntry.h" +#include "utils.h" + #include using namespace QGBA; @@ -22,6 +24,7 @@ LibraryEntry::LibraryEntry(const mLibraryEntry* entry) , internalTitle(entry->internalTitle) , internalCode(entry->internalCode) , platform(entry->platform) + , platformModels(entry->platformModels) , filesize(entry->filesize) , crc32(entry->crc32) { @@ -38,6 +41,10 @@ QString LibraryEntry::displayTitle(bool showFilename) const { return title; } +QString LibraryEntry::displayPlatform() const { + return nicePlatformFormat(platform, platformModels); +} + bool LibraryEntry::operator==(const LibraryEntry& other) const { return other.fullpath == fullpath; } diff --git a/src/platform/qt/library/LibraryEntry.h b/src/platform/qt/library/LibraryEntry.h index 2a733db4a..3d8e6c8a8 100644 --- a/src/platform/qt/library/LibraryEntry.h +++ b/src/platform/qt/library/LibraryEntry.h @@ -25,6 +25,7 @@ struct LibraryEntry { bool isNull() const; QString displayTitle(bool showFilename = false) const; + QString displayPlatform() const; QString base; QString filename; @@ -33,6 +34,7 @@ struct LibraryEntry { QByteArray internalTitle; QByteArray internalCode; mPlatform platform; + int platformModels; size_t filesize; uint32_t crc32; diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp index bc08228c1..ebb8f97e6 100644 --- a/src/platform/qt/library/LibraryModel.cpp +++ b/src/platform/qt/library/LibraryModel.cpp @@ -21,6 +21,7 @@ static const QStringList iconSets{ "GBA", "GBC", "GB", + "SGB", // "DS", }; @@ -348,7 +349,7 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const { switch (index.column()) { case COL_NAME: if (role == Qt::DecorationRole) { - return m_icons.value(nicePlatformFormat(entry->platform), qApp->style()->standardIcon(QStyle::SP_FileIcon)); + return m_icons.value(entry->displayPlatform(), qApp->style()->standardIcon(QStyle::SP_FileIcon)); } return entry->displayTitle(m_showFilename); case COL_LOCATION: diff --git a/src/platform/qt/resources.qrc b/src/platform/qt/resources.qrc index 271440f67..4c9fe9098 100644 --- a/src/platform/qt/resources.qrc +++ b/src/platform/qt/resources.qrc @@ -19,6 +19,12 @@ ../../../res/gbc-icon-24.png ../../../res/gbc-icon-16.png ../../../res/gbc-icon.svg + ../../../res/sgb-icon-256.png + ../../../res/sgb-icon-128.png + ../../../res/sgb-icon-32.png + ../../../res/sgb-icon-24.png + ../../../res/sgb-icon-16.png + ../../../res/sgb-icon.svg ../../../res/gba-icon-256.png ../../../res/gba-icon-128.png ../../../res/gba-icon-32.png diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp index e6d3bbe99..0f013e296 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -5,6 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "utils.h" +#include +#include + #include #include #include @@ -30,7 +33,7 @@ QString niceSizeFormat(size_t filesize) { return unit.arg(size, 0, 'f', int(size * 10) % 10 ? 1 : 0); } -QString nicePlatformFormat(mPlatform platform) { +QString nicePlatformFormat(mPlatform platform, int validModels) { switch (platform) { #ifdef M_CORE_GBA case mPLATFORM_GBA: @@ -38,6 +41,13 @@ QString nicePlatformFormat(mPlatform platform) { #endif #ifdef M_CORE_GB case mPLATFORM_GB: + if (validModels != M_LIBRARY_MODEL_UNKNOWN) { + if (validModels & GB_MODEL_CGB) { + return QObject::tr("GBC"); + } else if (validModels & GB_MODEL_SGB) { + return QObject::tr("SGB"); + } + } return QObject::tr("GB"); #endif default: diff --git a/src/platform/qt/utils.h b/src/platform/qt/utils.h index e6cb81929..d1e1a7063 100644 --- a/src/platform/qt/utils.h +++ b/src/platform/qt/utils.h @@ -31,7 +31,7 @@ enum class Endian { }; QString niceSizeFormat(size_t filesize); -QString nicePlatformFormat(mPlatform platform); +QString nicePlatformFormat(mPlatform platform, int validModels = 0); bool convertAddress(const QHostAddress* input, Address* output); From 578709254f555058b5126450d7a5a2b589257c24 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Wed, 26 Apr 2023 21:45:18 -0500 Subject: [PATCH 3/6] clean up according to PR comments --- src/core/library.c | 7 +++++-- src/platform/qt/CMakeLists.txt | 2 +- src/platform/qt/library/LibraryModel.cpp | 14 +++++++------- src/platform/qt/utils.cpp | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core/library.c b/src/core/library.c index 8112ffc1a..24f3ddd42 100644 --- a/src/core/library.c +++ b/src/core/library.c @@ -38,6 +38,7 @@ struct mLibrary { #define CONSTRAINTS_ROMONLY \ "CASE WHEN :useSize THEN roms.size = :size ELSE 1 END AND " \ "CASE WHEN :usePlatform THEN roms.platform = :platform ELSE 1 END AND " \ + "CASE WHEN :useModels THEN roms.models & :models ELSE 1 END AND " \ "CASE WHEN :useCrc32 THEN roms.crc32 = :crc32 ELSE 1 END AND " \ "CASE WHEN :useMd5 THEN roms.md5 = :md5 ELSE 1 END AND " \ "CASE WHEN :useSha1 THEN roms.sha1 = :sha1 ELSE 1 END AND " \ @@ -115,7 +116,9 @@ static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry } if (constraints->platformModels != M_LIBRARY_MODEL_UNKNOWN) { + useIndex = sqlite3_bind_parameter_index(statement, ":useModels"); index = sqlite3_bind_parameter_index(statement, ":models"); + sqlite3_bind_int(statement, useIndex, 1); sqlite3_bind_int(statement, index, constraints->platformModels); } } @@ -202,8 +205,8 @@ struct mLibrary* mLibraryLoad(const char* path) { goto error; } else if (romsTableVersion < 2) { static const char upgradeRomsTable[] = - " ALTER TABLE roms" - "\nADD COLUMN models INTEGER NULL"; + " ALTER TABLE roms" + "\n ADD COLUMN models INTEGER NULL"; if (sqlite3_exec(library->db, upgradeRomsTable, NULL, NULL, NULL)) { goto error; } diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 560a6fa44..433e8c061 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -566,7 +566,7 @@ if(BUILD_SUITE) add_test(platform-qt-${TEST_NAME} test-qt-${TEST_NAME}) endforeach() else() - message("${QT}Test not found") + message(WARNING "${QT}Test not found") endif() endif() diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp index ebb8f97e6..0a4379bbb 100644 --- a/src/platform/qt/library/LibraryModel.cpp +++ b/src/platform/qt/library/LibraryModel.cpp @@ -340,13 +340,13 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const { 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()]; + entry = &m_games[index.row()]; + } + if (entry) { + if (role == FullPathRole) { + return entry->fullpath; } - if (entry) { - if (role == FullPathRole) { - return entry->fullpath; - } - switch (index.column()) { + switch (index.column()) { case COL_NAME: if (role == Qt::DecorationRole) { return m_icons.value(entry->displayPlatform(), qApp->style()->standardIcon(QStyle::SP_FileIcon)); @@ -360,8 +360,8 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const { 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/utils.cpp b/src/platform/qt/utils.cpp index 0f013e296..6c7d2d52b 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -185,7 +185,7 @@ QString keyName(int key) { return QObject::tr("Menu"); default: return QKeySequence(key).toString(QKeySequence::NativeText); - } + } } void SpanSet::add(int pos) { From 130319494a1631e9de3cf5df306820a2db02d6b7 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Sun, 30 Mar 2025 17:21:01 -0500 Subject: [PATCH 4/6] switch away from APIs removed / compatibility-broken by upstream --- src/platform/qt/library/LibraryController.cpp | 4 +-- src/platform/qt/library/LibraryModel.cpp | 27 +++++++++---------- src/platform/qt/library/LibraryModel.h | 5 +++- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/platform/qt/library/LibraryController.cpp b/src/platform/qt/library/LibraryController.cpp index ed78ce410..198d14080 100644 --- a/src/platform/qt/library/LibraryController.cpp +++ b/src/platform/qt/library/LibraryController.cpp @@ -192,7 +192,7 @@ void LibraryController::refresh() { setDisabled(true); - QSet removedEntries = QSet::fromList(m_knownGames.keys()); + QSet removedEntries(m_knownGames.keyBegin(), m_knownGames.keyEnd()); QList updatedEntries; QList newEntries; @@ -217,7 +217,7 @@ void LibraryController::refresh() { m_knownGames.remove(path); } - m_libraryModel->removeEntries(removedEntries.toList()); + m_libraryModel->removeEntries(QList(removedEntries.begin(), removedEntries.end())); m_libraryModel->updateEntries(updatedEntries); m_libraryModel->addEntries(newEntries); diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp index 0a4379bbb..dcd72a33b 100644 --- a/src/platform/qt/library/LibraryModel.cpp +++ b/src/platform/qt/library/LibraryModel.cpp @@ -102,11 +102,11 @@ void LibraryModel::addEntries(const QList& items) { void LibraryModel::addEntryInternal(const LibraryEntry& item) { m_gameIndex[item.fullpath] = m_games.size(); - m_games << item; + m_games.emplace_back(new LibraryEntry(item)); if (!m_pathIndex.contains(item.base)) { m_pathOrder << item.base; } - m_pathIndex[item.base] << &m_games.back(); + m_pathIndex[item.base] << m_games.back().get(); } void LibraryModel::addEntriesList(const QList& items) { @@ -158,7 +158,7 @@ void LibraryModel::updateEntries(const QList& items) { Q_ASSERT(idx.isValid()); int pos = m_gameIndex.value(item.fullpath, -1); Q_ASSERT(pos >= 0); - m_games[pos] = item; + *m_games[pos] = item; updatedSpans[idx.parent()].add(pos); } for (auto iter = updatedSpans.begin(); iter != updatedSpans.end(); iter++) { @@ -183,11 +183,13 @@ void LibraryModel::removeEntries(const QList& items) { if (pos < firstModifiedIndex) { firstModifiedIndex = pos; } - LibraryEntry* entry = &m_games[pos]; + LibraryEntry* entry = m_games[pos].get(); QModelIndex parent = indexForPath(entry->base); Q_ASSERT(!m_treeMode || parent.isValid()); QList& pathItems = m_pathIndex[entry->base]; - removedTreeSpans[entry->base].add(pathItems.indexOf(entry)); + int pathPos = pathItems.indexOf(entry); + Q_ASSERT(pathPos >= 0); + removedTreeSpans[entry->base].add(pathPos); if (!m_treeMode) { removedRootSpans.add(pos); } @@ -214,9 +216,6 @@ void LibraryModel::removeEntries(const QList& items) { 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) { @@ -238,8 +237,8 @@ void LibraryModel::removeEntries(const QList& items) { } endRemoveRows(); } - for (int i = m_games.count() - 1; i >= firstModifiedIndex; i--) { - m_gameIndex[m_games[i].fullpath] = i; + for (int i = m_games.size() - 1; i >= firstModifiedIndex; i--) { + m_gameIndex[m_games[i]->fullpath] = i; } } @@ -249,7 +248,7 @@ QModelIndex LibraryModel::index(const QString& game) const { return QModelIndex(); } if (m_treeMode) { - const LibraryEntry& entry = m_games[pos]; + const LibraryEntry& entry = *m_games[pos]; return createIndex(m_pathIndex[entry.base].indexOf(&entry), 0, m_pathOrder.indexOf(entry.base)); } return createIndex(pos, 0); @@ -339,8 +338,8 @@ QVariant LibraryModel::data(const QModelIndex& index, int role) const { } 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()]; + } else if (!index.parent().isValid() && index.row() < (int)m_games.size()) { + entry = m_games[index.row()].get(); } if (entry) { if (role == FullPathRole) { @@ -414,5 +413,5 @@ LibraryEntry LibraryModel::entry(const QString& game) const { if (pos < 0) { return {}; } - return m_games[pos]; + return *m_games[pos]; } diff --git a/src/platform/qt/library/LibraryModel.h b/src/platform/qt/library/LibraryModel.h index db220392f..1d57c1c41 100644 --- a/src/platform/qt/library/LibraryModel.h +++ b/src/platform/qt/library/LibraryModel.h @@ -11,6 +11,9 @@ #include +#include +#include + #include "LibraryEntry.h" class QTreeView; @@ -71,7 +74,7 @@ private: bool m_treeMode; bool m_showFilename; - QList m_games; + std::vector> m_games; QStringList m_pathOrder; QHash> m_pathIndex; QHash m_gameIndex; From 165cce1a6ccca3bd3b54de053e9131b9388efd43 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Sun, 30 Mar 2025 18:25:07 -0500 Subject: [PATCH 5/6] fix library icons --- src/platform/qt/library/LibraryModel.cpp | 4 ++-- src/platform/qt/utils.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/platform/qt/library/LibraryModel.cpp b/src/platform/qt/library/LibraryModel.cpp index dcd72a33b..db530c177 100644 --- a/src/platform/qt/library/LibraryModel.cpp +++ b/src/platform/qt/library/LibraryModel.cpp @@ -31,7 +31,7 @@ LibraryModel::LibraryModel(QObject* parent) , m_showFilename(false) { for (const QString& platform : iconSets) { - QString pathTemplate = QStringLiteral(":/res/%1-icon%2").arg(platform); + 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)); @@ -40,7 +40,7 @@ LibraryModel::LibraryModel(QObject* parent) 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; + m_icons[platform] = icon; } } diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp index 6c7d2d52b..1d12310ed 100644 --- a/src/platform/qt/utils.cpp +++ b/src/platform/qt/utils.cpp @@ -37,21 +37,21 @@ QString nicePlatformFormat(mPlatform platform, int validModels) { switch (platform) { #ifdef M_CORE_GBA case mPLATFORM_GBA: - return QObject::tr("GBA"); + return "GBA"; #endif #ifdef M_CORE_GB case mPLATFORM_GB: if (validModels != M_LIBRARY_MODEL_UNKNOWN) { if (validModels & GB_MODEL_CGB) { - return QObject::tr("GBC"); + return "GBC"; } else if (validModels & GB_MODEL_SGB) { - return QObject::tr("SGB"); + return "SGB"; } } - return QObject::tr("GB"); + return "GB"; #endif default: - return QObject::tr("?"); + return "?"; } } From 86df2543e63b28a08906cefcce0bef3e7c1d2f80 Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Mon, 31 Mar 2025 12:08:32 -0500 Subject: [PATCH 6/6] PR review updates --- src/core/library.c | 2 +- src/platform/qt/library/LibraryController.cpp | 25 ++-- src/platform/qt/library/LibraryController.h | 1 + src/platform/qt/library/LibraryEntry.cpp | 21 ++- src/platform/qt/library/LibraryEntry.h | 1 + src/platform/qt/library/LibraryModel.cpp | 124 +++++++++++------- src/platform/qt/library/LibraryModel.h | 4 +- src/platform/qt/test/library.cpp | 10 ++ src/platform/qt/utils.cpp | 2 + 9 files changed, 133 insertions(+), 57 deletions(-) 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