Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Library: rewrite Qt library frontend
Browse files Browse the repository at this point in the history
ahigerd committed Feb 16, 2023

Verified

This commit was signed with the committer’s verified signature.
bonjourmauko Mauko Quiroga-Alvarado
1 parent ea345ca commit 8430779
Showing 14 changed files with 1,138 additions and 404 deletions.
34 changes: 32 additions & 2 deletions src/platform/qt/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -199,6 +199,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},libqt5widgets5")

set(AUDIO_SRC)
@@ -251,8 +256,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)
@@ -498,3 +510,21 @@ if(DISTBUILD AND NOT APPLE)
add_custom_command(TARGET ${BINARY_NAME}-qt POST_BUILD COMMAND "${STRIP}" "$<TARGET_FILE:${BINARY_NAME}-qt>")
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()
225 changes: 147 additions & 78 deletions src/platform/qt/library/LibraryController.cpp
Original file line number Diff line number Diff line change
@@ -8,38 +8,15 @@

#include "ConfigController.h"
#include "GBAApp.h"
#include "LibraryGrid.h"
#include "LibraryTree.h"
#include "LibraryModel.h"

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});
}
#include <QHeaderView>
#include <QListView>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <QTreeView>

void AbstractGameList::updateEntry(const LibraryEntry& item) {
updateEntries({item});
}

void AbstractGameList::removeEntry(const QString& item) {
removeEntries({item});
}
void AbstractGameList::setShowFilename(bool showFilename) {
m_showFilename = showFilename;
}
using namespace QGBA;

LibraryController::LibraryController(QWidget* parent, const QString& path, ConfigController* config)
: QStackedWidget(parent)
@@ -55,14 +32,53 @@ LibraryController::LibraryController(QWidget* parent, const QString& path, Confi

mLibraryAttachGameDB(m_library.get(), GBAApp::app()->gameDB());

m_libraryTree = std::make_unique<LibraryTree>(this);
addWidget(m_libraryTree->widget());

m_libraryGrid = std::make_unique<LibraryGrid>(this);
addWidget(m_libraryGrid->widget());
m_libraryModel = new LibraryModel(this);

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_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);

m_currentStyle = LibraryStyle::STYLE_TREE; // Make sure setViewStyle does something
setViewStyle(LibraryStyle::STYLE_LIST);
QVariant librarySort = m_config->getQtOption("librarySort");
QVariant librarySortOrder = m_config->getQtOption("librarySortOrder");
if (librarySort.isNull() || !librarySort.canConvert<int>()) {
librarySort = 0;
}
if (librarySortOrder.isNull() || !librarySortOrder.canConvert<Qt::SortOrder>()) {
librarySortOrder = Qt::AscendingOrder;
}
m_treeModel->sort(librarySort.toInt(), librarySortOrder.value<Qt::SortOrder>());
m_listModel->sort(0, Qt::AscendingOrder);
refresh();
}

@@ -73,32 +89,59 @@ void LibraryController::setViewStyle(LibraryStyle newStyle) {
if (m_currentStyle == newStyle) {
return;
}
QString selected;
if (m_currentView) {
QModelIndex selectedIndex = m_currentView->selectionModel()->currentIndex();
if (selectedIndex.isValid()) {
selected = selectedIndex.data(LibraryModel::FullPathRole).toString();
}
}

m_currentStyle = newStyle;
m_libraryModel->setTreeMode(newStyle == LibraryStyle::STYLE_TREE);

AbstractGameList* newCurrentList = nullptr;
QAbstractItemView* newView = m_listView;
if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) {
newCurrentList = m_libraryTree.get();
} else {
newCurrentList = m_libraryGrid.get();
newView = m_treeView;
}
newCurrentList->selectEntry(selectedEntry().fullpath);
newCurrentList->setViewStyle(newStyle);
setCurrentWidget(newCurrentList->widget());
m_currentList = newCurrentList;

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<QAbstractProxyModel*>(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 {};
}
QModelIndex index = m_currentView->selectionModel()->currentIndex();
if (!index.isValid()) {
return {};
}
return m_entries.value(m_currentList->selectedEntry());
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<QString, LibraryEntry> removedEntries = m_entries;
QHash<QString, LibraryEntry> updatedEntries;
QSet<QString> removedEntries = QSet<QString>::fromList(m_knownGames.keys());
QList<LibraryEntry> updatedEntries;
QList<LibraryEntry> 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);
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();
}
if (m_libraryTree) {
m_libraryTree->setShowFilename(m_showFilename);

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;
}
}
refresh();
}
76 changes: 25 additions & 51 deletions src/platform/qt/library/LibraryController.h
Original file line number Diff line number Diff line change
@@ -12,15 +12,21 @@
#include <QHash>
#include <QList>
#include <QStackedWidget>
#include <QTimer>

#include <mgba/core/library.h>

#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<LibraryEntry>&) = 0;
virtual void addEntries(const QList<LibraryEntry>&) = 0;
virtual void updateEntries(const QList<LibraryEntry>&) = 0;
virtual void removeEntries(const QList<QString>&) = 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,21 +65,33 @@ public slots:

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

ConfigController* m_config = nullptr;
std::shared_ptr<mLibrary> m_library;
QAtomicInteger<qint64> m_libraryJob = -1;
QHash<QString, LibraryEntry> m_entries;

LibraryStyle m_currentStyle;
AbstractGameList* m_currentList = nullptr;

std::unique_ptr<LibraryGrid> m_libraryGrid;
std::unique_ptr<LibraryTree> m_libraryTree;
QHash<QString, uint64_t> 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;
};

}
51 changes: 51 additions & 0 deletions src/platform/qt/library/LibraryEntry.cpp
Original file line number Diff line number Diff line change
@@ -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 <mgba/core/library.h>

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);
}
47 changes: 47 additions & 0 deletions src/platform/qt/library/LibraryEntry.h
Original file line number Diff line number Diff line change
@@ -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 <QByteArray>
#include <QList>
#include <QString>

#include <mgba/core/core.h>

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);
};

};
417 changes: 417 additions & 0 deletions src/platform/qt/library/LibraryModel.cpp

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions src/platform/qt/library/LibraryModel.h
Original file line number Diff line number Diff line change
@@ -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 <QAbstractItemModel>
#include <QIcon>
#include <QTreeView>

#include <mgba/core/library.h>

#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<LibraryEntry>& items);
void addEntries(const QList<LibraryEntry>& items);
void updateEntries(const QList<LibraryEntry>& items);
void removeEntries(const QList<QString>& 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<LibraryEntry>& items);
void addEntriesTree(const QList<LibraryEntry>& items);
void addEntryInternal(const LibraryEntry& item);

bool m_treeMode;
bool m_showFilename;

QList<LibraryEntry> m_games;
QStringList m_pathOrder;
QHash<QString, QList<const LibraryEntry*>> m_pathIndex;
QHash<QString, int> m_gameIndex;
QHash<QString, QIcon> m_icons;
};

}
212 changes: 0 additions & 212 deletions src/platform/qt/library/LibraryTree.cpp

This file was deleted.

61 changes: 0 additions & 61 deletions src/platform/qt/library/LibraryTree.h

This file was deleted.

18 changes: 18 additions & 0 deletions src/platform/qt/resources.qrc
Original file line number Diff line number Diff line change
@@ -7,6 +7,24 @@
<file>../../../res/keymap.qpic</file>
<file>../../../res/patrons.txt</file>
<file>../../../res/no-cam.png</file>
<file>../../../res/gb-icon-256.png</file>
<file>../../../res/gb-icon-128.png</file>
<file>../../../res/gb-icon-32.png</file>
<file>../../../res/gb-icon-24.png</file>
<file>../../../res/gb-icon-16.png</file>
<file>../../../res/gb-icon.svg</file>
<file>../../../res/gbc-icon-256.png</file>
<file>../../../res/gbc-icon-128.png</file>
<file>../../../res/gbc-icon-32.png</file>
<file>../../../res/gbc-icon-24.png</file>
<file>../../../res/gbc-icon-16.png</file>
<file>../../../res/gbc-icon.svg</file>
<file>../../../res/gba-icon-256.png</file>
<file>../../../res/gba-icon-128.png</file>
<file>../../../res/gba-icon-32.png</file>
<file>../../../res/gba-icon-24.png</file>
<file>../../../res/gba-icon-16.png</file>
<file>../../../res/gba-icon.svg</file>
</qresource>
<qresource prefix="/exe">
<file alias="exe4/chip-names.txt">../../../res/exe4/chip-names.txt</file>
200 changes: 200 additions & 0 deletions src/platform/qt/test/library.cpp
Original file line number Diff line number Diff line change
@@ -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 <QAbstractItemModelTester>
#endif

#include <QSignalSpy>
#include <QTest>

#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"
61 changes: 61 additions & 0 deletions src/platform/qt/test/spanset.cpp
Original file line number Diff line number Diff line change
@@ -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 <QTest>

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"
42 changes: 42 additions & 0 deletions src/platform/qt/utils.cpp
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
#include "utils.h"

#include <QCoreApplication>
#include <QHostAddress>
#include <QKeySequence>
#include <QObject>

@@ -150,6 +151,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<Span> 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<Span>());
} else {
std::sort(spans.begin(), spans.end());
}
}

17 changes: 17 additions & 0 deletions src/platform/qt/utils.h
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
#include <QRect>
#include <QSize>
#include <QString>
#include <QVector>

#include <algorithm>
#include <functional>
@@ -77,4 +78,20 @@ bool extractMatchingFile(VDir* dir, std::function<QString (VDirEntry*)> 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<Span> spans;
};

}

0 comments on commit 8430779

Please sign in to comment.