mirror of https://github.com/mgba-emu/mgba.git
Compare commits
6 Commits
14e6f3850f
...
43afb3e194
Author | SHA1 | Date |
---|---|---|
ahigerd | 43afb3e194 | |
Vicki Pfau | 4007e19736 | |
Vicki Pfau | ad0d3972a6 | |
Adam Higerd | f415084807 | |
Adam Higerd | f5c90ee34b | |
Adam Higerd | 65fbf52d0a |
1
CHANGES
1
CHANGES
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
};
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue