Compare commits

...

6 Commits

Author SHA1 Message Date
ahigerd 43afb3e194
Merge f415084807 into 4007e19736 2024-12-31 01:26:41 +01:00
Vicki Pfau 4007e19736 CMake: Fix entitlements plist filename 2024-12-29 17:38:14 -08:00
Vicki Pfau ad0d3972a6 Updater: Fix rewriting folders and files on Windows (fixes #3384) 2024-12-28 22:47:13 -08:00
Adam Higerd f415084807 clean up according to PR comments 2024-11-03 19:59:07 -06:00
Adam Higerd f5c90ee34b Library: store platform models in database, render GBC/SGB icons 2024-11-03 19:59:07 -06:00
Adam Higerd 65fbf52d0a Library: rewrite Qt library frontend 2024-11-03 19:59:06 -06:00
18 changed files with 1251 additions and 420 deletions

View File

@ -48,6 +48,7 @@ Misc:
- Qt: Show a dummy shader settings tab if shaders aren't supported
- Res: Port NSO-gba-colors shader (closes mgba.io/i/2834)
- Scripting: Add `callbacks:oneshot` for single-call callbacks
- Updater: Fix rewriting folders and files on Windows (fixes mgba.io/i/3384)
0.10.4: (2024-12-07)
Emulation fixes:

View File

@ -15,6 +15,8 @@ CXX_GUARD_START
#include <mgba/core/core.h>
#include <mgba-util/vector.h>
#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

View File

@ -9,6 +9,11 @@
#include <mgba-util/string.h>
#include <mgba-util/vfs.h>
#ifdef M_CORE_GB
#include <mgba/gb/interface.h>
#include <mgba/internal/gb/gb.h>
#endif
#ifdef USE_SQLITE3
#include <sqlite3.h>
@ -33,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"
@ -92,12 +98,40 @@ 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) {
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);
}
}
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 +158,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 +176,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"
"\n ADD 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 +350,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 +471,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) {

View File

@ -220,6 +220,11 @@ set(GB_SRC
GBOverride.cpp
PrinterView.cpp)
set(TEST_QT_spanset_SRC
test/spanset.cpp
utils.cpp
VFileDevice.cpp)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libqt${QT_V}widgets${QT_V}")
set(AUDIO_SRC)
@ -272,8 +277,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)
@ -517,7 +529,7 @@ if(APPLE)
set(DEPLOY_OPTIONS ${DEPLOY_OPTIONS} -R "${CROSS_ROOT}")
endif()
if($ENV{CODESIGN_IDENTITY})
set(DEPLOY_OPTIONS ${DEPLOY_OPTIONS} -s "$ENV{CODESIGN_IDENTITY}" -E "${PROJECT_SOURCE_DIR}/res/entitlements.xml")
set(DEPLOY_OPTIONS ${DEPLOY_OPTIONS} -s "$ENV{CODESIGN_IDENTITY}" -E "${PROJECT_SOURCE_DIR}/res/entitlements.plist")
endif()
install(CODE "execute_process(COMMAND \"${PROJECT_SOURCE_DIR}/tools/deploy-mac.py\" -v ${DEPLOY_OPTIONS} \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${APPDIR}/${PROJECT_NAME}.app\")")
endif()
@ -547,6 +559,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(WARNING "${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)

View File

@ -8,39 +8,16 @@
#include "ConfigController.h"
#include "GBAApp.h"
#include "LibraryGrid.h"
#include "LibraryTree.h"
#include "LibraryModel.h"
#include <QHeaderView>
#include <QListView>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <QTreeView>
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<LibraryTree>(this);
addWidget(m_libraryTree->widget());
m_libraryModel = new LibraryModel(this);
m_libraryGrid = std::make_unique<LibraryGrid>(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<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;
}
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<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 {};
}
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<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);
}
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;
}
}
}

View File

@ -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,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<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;
};
}

View File

@ -0,0 +1,58 @@
/* 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 "utils.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)
, platformModels(entry->platformModels)
, 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;
}
QString LibraryEntry::displayPlatform() const {
return nicePlatformFormat(platform, platformModels);
}
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);
}

View File

@ -0,0 +1,49 @@
/* 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 displayPlatform() const;
QString base;
QString filename;
QString fullpath;
QString title;
QByteArray internalTitle;
QByteArray internalCode;
mPlatform platform;
int platformModels;
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);
};
};

View File

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

View File

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

View File

@ -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 <QApplication>
#include <QDir>
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<const LibraryTreeItem*>(&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<LibraryEntry>& items) {
m_deferredTreeRebuild = true;
m_entries.clear();
m_pathNodes.clear();
addEntries(items);
}
void LibraryTree::addEntries(const QList<LibraryEntry>& 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<LibraryEntry>& items) {
for (const auto& item : items) {
updateEntry(item);
}
}
void LibraryTree::updateEntry(const LibraryEntry& item) {
m_entries[item.fullpath] = item;
LibraryTreeItem* i = static_cast<LibraryTreeItem*>(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<QString>& 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<QString, QTreeWidgetItem*> 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);
}

View File

@ -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 <QTreeWidget>
#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<LibraryEntry>& items) override;
void addEntries(const QList<LibraryEntry>& items) override;
void updateEntries(const QList<LibraryEntry>& items) override;
void removeEntries(const QList<QString>& 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<QString, LibraryEntry> m_entries;
QHash<QString, QTreeWidgetItem*> m_items;
QHash<QString, int> m_pathNodes;
void rebuildTree();
void resizeAllCols();
};
}

View File

@ -7,6 +7,30 @@
<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/sgb-icon-256.png</file>
<file>../../../res/sgb-icon-128.png</file>
<file>../../../res/sgb-icon-32.png</file>
<file>../../../res/sgb-icon-24.png</file>
<file>../../../res/sgb-icon-16.png</file>
<file>../../../res/sgb-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>

View File

@ -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"

View File

@ -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"

View File

@ -5,7 +5,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "utils.h"
#include <mgba/core/library.h>
#include <mgba/gb/interface.h>
#include <QCoreApplication>
#include <QHostAddress>
#include <QKeySequence>
#include <QObject>
@ -29,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:
@ -37,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:
@ -177,4 +188,45 @@ QString keyName(int key) {
}
}
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());
}
}
}

View File

@ -13,6 +13,7 @@
#include <QRect>
#include <QSize>
#include <QString>
#include <QVector>
#include <algorithm>
#include <functional>
@ -30,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);
@ -117,4 +118,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;
};
}

View File

@ -133,14 +133,11 @@ bool extractArchive(struct VDir* archive, const char* root, bool prefix) {
errno = 0;
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
if (!vfOut) {
if (errno == EACCES) {
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
} else if (errno == EISDIR) {
int error = errno;
struct stat st;
if (error == EISDIR || (stat(path, &st) >= 0 && S_ISDIR(st.st_mode))) {
// Windows maps STATUS_FILE_IS_A_DIRECTORY to ERROR_ACCESS_DENIED,
// which then gets mapped to EACCESS, because everything is awful
fprintf(logfile, "rm -r %s\n", path);
if (!rmdirRecursive(VDirOpen(path))) {
return false;
@ -151,6 +148,13 @@ bool extractArchive(struct VDir* archive, const char* root, bool prefix) {
RemoveDirectoryW(wpath);
#else
rmdir(path);
#endif
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
} else if (error == EACCES || error == ETXTBSY) {
#ifdef _WIN32
Sleep(1000);
#else
sleep(1);
#endif
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
}