From 65fbf52d0aadd17a865bf4b7f46983032c00414c Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Tue, 28 Jun 2022 14:10:47 -0500 Subject: [PATCH 1/3] Library: rewrite Qt library frontend --- src/platform/qt/CMakeLists.txt | 34 +- 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, 1141 insertions(+), 407 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 3cfe83ad0..336b8f8ef 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -221,6 +221,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) @@ -273,8 +278,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) @@ -548,6 +560,24 @@ if(DISTBUILD AND NOT APPLE) endif() endif() +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 f5c90ee34bd25ee4dcb67afbc2e1b2cf42e3dd5f Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Sat, 2 Jul 2022 14:10:48 -0500 Subject: [PATCH 2/3] 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 ededd5463..e3d10fcb6 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; @@ -24,6 +26,7 @@ struct mLibraryEntry { enum mPlatform platform; size_t filesize; uint32_t crc32; + int platformModels; }; #ifdef USE_SQLITE3 diff --git a/src/core/library.c b/src/core/library.c index 1c92a0cbc..a8fbff79d 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 @@ -92,12 +97,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)); @@ -124,6 +155,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," @@ -141,18 +173,35 @@ struct mLibrary* mLibraryLoad(const char* path) { "\n CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);" "\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, size, internalCode, platform) VALUES (:crc32, :size, :internalCode, :platform);"; + static const char insertRom[] = "INSERT INTO roms (crc32, size, internalCode, platform, models) VALUES (:crc32, :size, :internalCode, :platform, :models);"; if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) { goto error; } @@ -298,6 +347,15 @@ bool _mLibraryAddEntry(struct mLibrary* library, const char* filename, const cha strlcpy(entry.internalTitle, info.title, sizeof(entry.internalTitle)); core->checksum(core, &entry.crc32, mCHECKSUM_CRC32); 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; @@ -410,6 +468,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 f41508480791900310acae47048323eaa53aa83d Mon Sep 17 00:00:00 2001 From: Adam Higerd Date: Wed, 26 Apr 2023 21:45:18 -0500 Subject: [PATCH 3/3] 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 a8fbff79d..7a3913316 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 :useInternalCode THEN roms.internalCode = :internalCode ELSE 1 END" @@ -99,7 +100,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); } } @@ -184,8 +187,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 336b8f8ef..e13fb9648 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -574,7 +574,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) {