Qt: New library system (#549)
jump to
@@ -17,6 +17,7 @@ - Add option to allow preloading the entire ROM before running
- GB: Video/audio channel enabling/disabling - Add option to lock video to integer scaling - Video log recording for testing and bug reporting + - Library view Bugfixes: - LR35902: Fix core never exiting with certain event patterns - GB Timer: Improve DIV reset behavior
@@ -13,11 +13,12 @@ ArchiveInspector::ArchiveInspector(const QString& filename, QWidget* parent)
: QDialog(parent) { m_ui.setupUi(this); - connect(m_ui.archiveView, &LibraryView::doneLoading, [this]() { + connect(m_ui.archiveView, &LibraryController::doneLoading, [this]() { m_ui.loading->hide(); }); - connect(m_ui.archiveView, SIGNAL(accepted()), this, SIGNAL(accepted())); - m_ui.archiveView->setDirectory(filename); + connect(m_ui.archiveView, &LibraryController::startGame, this, &ArchiveInspector::accepted); + m_ui.archiveView->setViewStyle(LibraryStyle::STYLE_LIST); + m_ui.archiveView->addDirectory(filename); } VFile* ArchiveInspector::selectedVFile() const {
@@ -29,15 +29,15 @@ </property>
</widget> </item> <item row="0" column="0" colspan="2"> - <widget class="QGBA::LibraryView" name="archiveView" native="true"/> + <widget class="QGBA::LibraryController" name="archiveView" native="true"/> </item> </layout> </widget> <customwidgets> <customwidget> - <class>QGBA::LibraryView</class> + <class>QGBA::LibraryController</class> <extends>QWidget</extends> - <header>LibraryView.h</header> + <header>library/LibraryController.h</header> <container>1</container> </customwidget> </customwidgets>
@@ -105,6 +105,7 @@ ShortcutView.cpp
Swatch.cpp TilePainter.cpp TileView.cpp + utils.cpp Window.cpp VFileDevice.cpp VideoView.cpp)@@ -117,7 +118,6 @@ CheatsView.ui
DebuggerConsole.ui GIFView.ui IOViewer.ui - LibraryView.ui LoadSaveState.ui LogView.ui MemoryView.ui@@ -137,8 +137,6 @@ GBAOverride.cpp)
set(GB_SRC GBOverride.cpp) - -qt5_wrap_ui(UI_SRC ${UI_FILES}) set(QT_LIBRARIES) set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libqt5widgets5,libqt5opengl5")@@ -189,8 +187,9 @@
if(USE_SQLITE3) list(APPEND SOURCE_FILES ArchiveInspector.cpp - LibraryModel.cpp - LibraryView.cpp) + library/LibraryController.cpp + library/LibraryGrid.cpp + library/LibraryTree.cpp) endif() qt5_add_resources(RESOURCES resources.qrc)@@ -238,6 +237,8 @@ DEPENDS ${TRANSLATION_FILES})
qt5_add_resources(TRANSLATION_RESOURCES ${TRANSLATION_QRC}) list(APPEND RESOURCES ${TRANSLATION_RESOURCES}) endif() + +qt5_wrap_ui(UI_SRC ${UI_FILES}) add_executable(${BINARY_NAME}-qt WIN32 MACOSX_BUNDLE main.cpp ${CMAKE_SOURCE_DIR}/res/mgba.icns ${SOURCE_FILES} ${PLATFORM_SRC} ${UI_SRC} ${AUDIO_SRC} ${RESOURCES}) set_target_properties(${BINARY_NAME}-qt PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/res/info.plist.in COMPILE_DEFINITIONS "${FEATURE_DEFINES};${FUNCTION_DEFINES};${OS_DEFINES};${QT_DEFINES}")
@@ -1,320 +0,0 @@
-/* Copyright (c) 2013-2016 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 <QFontMetrics> - -#include <mgba-util/vfs.h> - -using namespace QGBA; - -Q_DECLARE_METATYPE(mLibraryEntry); - -QMap<QString, LibraryModel::LibraryHandle*> LibraryModel::s_handles; -QMap<QString, LibraryModel::LibraryColumn> LibraryModel::s_columns; - -LibraryModel::LibraryModel(const QString& path, QObject* parent) - : QAbstractItemModel(parent) -{ - if (s_columns.empty()) { - s_columns["name"] = { - tr("Name"), - [](const mLibraryEntry& e) -> QString { - if (e.title) { - return QString::fromUtf8(e.title); - } - return QString::fromUtf8(e.filename); - } - }; - s_columns["filename"] = { - tr("Filename"), - [](const mLibraryEntry& e) -> QString { - return QString::fromUtf8(e.filename); - } - }; - s_columns["size"] = { - tr("Size"), - [](const mLibraryEntry& e) -> QString { - double size = e.filesize; - QString unit = "B"; - if (size >= 1024.0) { - size /= 1024.0; - unit = "kiB"; - } - if (size >= 1024.0) { - size /= 1024.0; - unit = "MiB"; - } - return QString("%0 %1").arg(size, 0, 'f', 1).arg(unit); - }, - Qt::AlignRight - }; - s_columns["platform"] = { - tr("Platform"), - [](const mLibraryEntry& e) -> QString { - int platform = e.platform; - switch (platform) { -#ifdef M_CORE_GBA - case PLATFORM_GBA: - return tr("GBA"); -#endif -#ifdef M_CORE_GB - case PLATFORM_GB: - return tr("GB"); -#endif - default: - return tr("?"); - } - } - }; - s_columns["location"] = { - tr("Location"), - [](const mLibraryEntry& e) -> QString { - return QString::fromUtf8(e.base); - } - }; - s_columns["crc32"] = { - tr("CRC32"), - [](const mLibraryEntry& e) -> QString { - return QString("%0").arg(e.crc32, 8, 16, QChar('0')); - } - }; - } - if (!path.isNull()) { - if (s_handles.contains(path)) { - m_library = s_handles[path]; - m_library->ref(); - } else { - m_library = new LibraryHandle(mLibraryLoad(path.toUtf8().constData()), path); - if (m_library->library) { - s_handles[path] = m_library; - } else { - delete m_library; - m_library = new LibraryHandle(mLibraryCreateEmpty()); - } - } - } else { - m_library = new LibraryHandle(mLibraryCreateEmpty()); - } - mLibraryListingInit(&m_listings, 0); - memset(&m_constraints, 0, sizeof(m_constraints)); - m_constraints.platform = PLATFORM_NONE; - m_columns.append(s_columns["name"]); - m_columns.append(s_columns["location"]); - m_columns.append(s_columns["platform"]); - m_columns.append(s_columns["size"]); - m_columns.append(s_columns["crc32"]); - - connect(m_library->loader, SIGNAL(directoryLoaded(const QString&)), this, SLOT(directoryLoaded(const QString&))); -} - -LibraryModel::~LibraryModel() { - clearConstraints(); - mLibraryListingDeinit(&m_listings); - if (!m_library->deref()) { - s_handles.remove(m_library->path); - delete m_library; - } -} - -void LibraryModel::loadDirectory(const QString& path) { - m_queue.append(path); - QMetaObject::invokeMethod(m_library->loader, "loadDirectory", Q_ARG(const QString&, path)); -} - -bool LibraryModel::entryAt(int row, mLibraryEntry* out) const { - if (mLibraryListingSize(&m_listings) <= row) { - return false; - } - *out = *mLibraryListingGetConstPointer(&m_listings, row); - return true; -} - -VFile* LibraryModel::openVFile(const QModelIndex& index) const { - mLibraryEntry entry; - if (!entryAt(index.row(), &entry)) { - return nullptr; - } - return mLibraryOpenVFile(m_library->library, &entry); -} - -QString LibraryModel::filename(const QModelIndex& index) const { - mLibraryEntry entry; - if (!entryAt(index.row(), &entry)) { - return QString(); - } - return QString::fromUtf8(entry.filename); -} - -QString LibraryModel::location(const QModelIndex& index) const { - mLibraryEntry entry; - if (!entryAt(index.row(), &entry)) { - return QString(); - } - return QString::fromUtf8(entry.base); -} - -QVariant LibraryModel::data(const QModelIndex& index, int role) const { - if (!index.isValid()) { - return QVariant(); - } - mLibraryEntry entry; - if (!entryAt(index.row(), &entry)) { - return QVariant(); - } - if (role == Qt::UserRole) { - return QVariant::fromValue(entry); - } - if (index.column() >= m_columns.count()) { - return QVariant(); - } - switch (role) { - case Qt::DisplayRole: - return m_columns[index.column()].value(entry); - case Qt::SizeHintRole: { - QFontMetrics fm((QFont())); - return fm.size(Qt::TextSingleLine, m_columns[index.column()].value(entry)); - } - case Qt::TextAlignmentRole: - return m_columns[index.column()].alignment; - default: - return QVariant(); - } -} - -QVariant LibraryModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (role != Qt::DisplayRole) { - return QAbstractItemModel::headerData(section, orientation, role); - } - if (orientation == Qt::Horizontal) { - if (section >= m_columns.count()) { - return QVariant(); - } - return m_columns[section].name; - } - return section; -} - -QModelIndex LibraryModel::index(int row, int column, const QModelIndex& parent) const { - if (parent.isValid()) { - return QModelIndex(); - } - return createIndex(row, column, nullptr); -} - -QModelIndex LibraryModel::parent(const QModelIndex&) const { - return QModelIndex(); -} - -int LibraryModel::columnCount(const QModelIndex& parent) const { - if (parent.isValid()) { - return 0; - } - return m_columns.count(); -} - -int LibraryModel::rowCount(const QModelIndex& parent) const { - if (parent.isValid()) { - return 0; - } - return mLibraryCount(m_library->library, &m_constraints); -} - -void LibraryModel::attachGameDB(const NoIntroDB* gameDB) { - mLibraryAttachGameDB(m_library->library, gameDB); -} - -void LibraryModel::constrainBase(const QString& path) { - clearConstraints(); - if (m_constraints.base) { - free(const_cast<char*>(m_constraints.base)); - } - m_constraints.base = strdup(path.toUtf8().constData()); - reload(); -} - -void LibraryModel::clearConstraints() { - if (m_constraints.base) { - free(const_cast<char*>(m_constraints.base)); - } - if (m_constraints.filename) { - free(const_cast<char*>(m_constraints.filename)); - } - if (m_constraints.title) { - free(const_cast<char*>(m_constraints.title)); - } - memset(&m_constraints, 0, sizeof(m_constraints)); - size_t i; - for (i = 0; i < mLibraryListingSize(&m_listings); ++i) { - mLibraryEntryFree(mLibraryListingGetPointer(&m_listings, i)); - } - mLibraryListingClear(&m_listings); -} - -void LibraryModel::reload() { - mLibraryGetEntries(m_library->library, &m_listings, 0, 0, m_constraints.base ? &m_constraints : nullptr); -} - -void LibraryModel::directoryLoaded(const QString& path) { - m_queue.removeOne(path); - beginResetModel(); - endResetModel(); - if (m_queue.empty()) { - emit doneLoading(); - } -} - -LibraryModel::LibraryColumn::LibraryColumn() { -} - -LibraryModel::LibraryColumn::LibraryColumn(const QString& name, std::function<QString(const mLibraryEntry&)> value, int alignment) - : name(name) - , value(value) - , alignment(alignment) -{ -} - -LibraryModel::LibraryHandle::LibraryHandle(mLibrary* lib, const QString& p) - : library(lib) - , loader(new LibraryLoader(library)) - , path(p) - , m_ref(1) -{ - if (!library) { - return; - } - loader->moveToThread(&m_loaderThread); - m_loaderThread.setObjectName("Library Loader Thread"); - m_loaderThread.start(); -} - -LibraryModel::LibraryHandle::~LibraryHandle() { - m_loaderThread.quit(); - m_loaderThread.wait(); - if (library) { - mLibraryDestroy(library); - } -} - -void LibraryModel::LibraryHandle::ref() { - ++m_ref; -} - -bool LibraryModel::LibraryHandle::deref() { - --m_ref; - return m_ref > 0; -} - -LibraryLoader::LibraryLoader(mLibrary* library, QObject* parent) - : QObject(parent) - , m_library(library) -{ -} - -void LibraryLoader::loadDirectory(const QString& path) { - mLibraryLoadDirectory(m_library, path.toUtf8().constData()); - emit directoryLoaded(path); -}
@@ -1,115 +0,0 @@
-/* Copyright (c) 2013-2016 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/. */ -#ifndef QGBA_LIBRARY_MODEL -#define QGBA_LIBRARY_MODEL - -#include <QAbstractItemModel> -#include <QStringList> -#include <QThread> - -#include <mgba/core/library.h> - -#include <functional> - -struct VDir; -struct VFile; -struct NoIntroDB; - -namespace QGBA { - -class LibraryLoader; -class LibraryModel : public QAbstractItemModel { -Q_OBJECT - -public: - LibraryModel(const QString& path, QObject* parent = nullptr); - virtual ~LibraryModel(); - - bool entryAt(int row, mLibraryEntry* out) const; - VFile* openVFile(const QModelIndex& index) const; - QString filename(const QModelIndex& index) const; - QString location(const QModelIndex& index) const; - - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - - virtual QModelIndex index(int row, int column, const QModelIndex& parent) const override; - virtual QModelIndex parent(const QModelIndex& index) const override; - - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; - - void attachGameDB(const NoIntroDB* gameDB); - -signals: - void doneLoading(); - -public slots: - void loadDirectory(const QString& path); - - void constrainBase(const QString& path); - void clearConstraints(); - void reload(); - -private slots: - void directoryLoaded(const QString& path); - -private: - struct LibraryColumn { - LibraryColumn(); - LibraryColumn(const QString&, std::function<QString(const mLibraryEntry&)>, int = Qt::AlignLeft); - QString name; - std::function<QString(const mLibraryEntry&)> value; - int alignment; - }; - - class LibraryHandle { - public: - LibraryHandle(mLibrary*, const QString& path = QString()); - ~LibraryHandle(); - - mLibrary* const library; - LibraryLoader* const loader; - const QString path; - - void ref(); - bool deref(); - - private: - QThread m_loaderThread; - size_t m_ref; - }; - - LibraryHandle* m_library; - static QMap<QString, LibraryHandle*> s_handles; - - mLibraryEntry m_constraints; - mLibraryListing m_listings; - QStringList m_queue; - - QList<LibraryColumn> m_columns; - static QMap<QString, LibraryColumn> s_columns; -}; - -class LibraryLoader : public QObject { -Q_OBJECT - -public: - LibraryLoader(mLibrary* library, QObject* parent = nullptr); - -public slots: - void loadDirectory(const QString& path); - -signals: - void directoryLoaded(const QString& path); - -private: - mLibrary* m_library; -}; - -} - -#endif
@@ -1,58 +0,0 @@
-/* Copyright (c) 2013-2017 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 "LibraryView.h" - -#include <mgba-util/vfs.h> - -#include "ConfigController.h" -#include "GBAApp.h" - -using namespace QGBA; - -LibraryView::LibraryView(QWidget* parent) - : QWidget(parent) - , m_model(ConfigController::configDir() + "/library.sqlite3") -{ - m_ui.setupUi(this); - m_model.attachGameDB(GBAApp::app()->gameDB()); - connect(&m_model, SIGNAL(doneLoading()), this, SIGNAL(doneLoading())); - connect(&m_model, SIGNAL(doneLoading()), this, SLOT(resizeColumns())); - connect(m_ui.listing, SIGNAL(activated(const QModelIndex&)), this, SIGNAL(accepted())); - m_ui.listing->horizontalHeader()->setSectionsMovable(true); - m_ui.listing->setModel(&m_model); - m_ui.listing->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_model.reload(); - resizeColumns(); -} - -void LibraryView::setDirectory(const QString& filename) { - m_model.loadDirectory(filename); - m_model.constrainBase(filename); -} - -void LibraryView::addDirectory(const QString& filename) { - m_model.loadDirectory(filename); -} - -VFile* LibraryView::selectedVFile() const { - QModelIndex index = m_ui.listing->selectionModel()->currentIndex(); - if (!index.isValid()) { - return nullptr; - } - return m_model.openVFile(index); -} - -QPair<QString, QString> LibraryView::selectedPath() const { - QModelIndex index = m_ui.listing->selectionModel()->currentIndex(); - if (!index.isValid()) { - return qMakePair(QString(), QString()); - } - return qMakePair(m_model.filename(index), m_model.location(index)); -} - -void LibraryView::resizeColumns() { - m_ui.listing->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents); -}
@@ -1,45 +0,0 @@
-/* Copyright (c) 2013-2017 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/. */ -#ifndef QGBA_LIBRARY_VIEW -#define QGBA_LIBRARY_VIEW - -#include "LibraryModel.h" - -#include "ui_LibraryView.h" - -struct VFile; - -namespace QGBA { - -class LibraryView : public QWidget { -Q_OBJECT - -public: - LibraryView(QWidget* parent = nullptr); - - VFile* selectedVFile() const; - QPair<QString, QString> selectedPath() const; - -signals: - void doneLoading(); - void accepted(); - -public slots: - void setDirectory(const QString&); - void addDirectory(const QString&); - -private slots: - void resizeColumns(); - -private: - Ui::LibraryView m_ui; - - LibraryModel m_model; -}; - -} - -#endif
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>LibraryView</class> - <widget class="QWidget" name="LibraryView"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>300</height> - </rect> - </property> - <property name="windowTitle"> - <string>Library</string> - </property> - <layout class="QGridLayout"> - <property name="margin"> - <number>0</number> - </property> - <item row="0" column="0"> - <widget class="QTableView" name="listing"> - <property name="editTriggers"> - <set>QAbstractItemView::NoEditTriggers</set> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="showGrid"> - <bool>false</bool> - </property> - <attribute name="horizontalHeaderStretchLastSection"> - <bool>true</bool> - </attribute> - <attribute name="verticalHeaderVisible"> - <bool>false</bool> - </attribute> - <attribute name="verticalHeaderMinimumSectionSize"> - <number>0</number> - </attribute> - </widget> - </item> - </layout> - </widget> - <resources/> - <connections/> -</ui>
@@ -204,6 +204,7 @@ saveSetting("savegamePath", m_ui.savegamePath);
saveSetting("savestatePath", m_ui.savestatePath); saveSetting("screenshotPath", m_ui.screenshotPath); saveSetting("patchPath", m_ui.patchPath); + saveSetting("libraryStyle", m_ui.libraryStyle->currentIndex()); saveSetting("showLibrary", m_ui.showLibrary); saveSetting("preload", m_ui.preload);
@@ -398,28 +398,28 @@ </layout>
</widget> <widget class="QWidget" name="interface_2"> <layout class="QFormLayout" name="formLayout_4"> - <item row="3" column="1"> - <widget class="QCheckBox" name="allowOpposingDirections"> + <item row="1" column="1"> + <widget class="QCheckBox" name="showLibrary"> <property name="text"> - <string>Allow opposing input directions</string> - </property> - </widget> - </item> - <item row="4" column="1"> - <widget class="QCheckBox" name="suspendScreensaver"> - <property name="text"> - <string>Suspend screensaver</string> + <string>Show when no game open</string> </property> <property name="checked"> <bool>true</bool> </property> </widget> </item> - <item row="5" column="1"> - <widget class="QCheckBox" name="pauseOnFocusLost"> - <property name="text"> - <string>Pause when inactive</string> - </property> + <item row="0" column="1"> + <widget class="QComboBox" name="libraryStyle"> + <item> + <property name="text"> + <string>List view</string> + </property> + </item> + <item> + <property name="text"> + <string>Tree view</string> + </property> + </item> </widget> </item> <item row="0" column="0">@@ -429,27 +429,44 @@ <string>Library:</string>
</property> </widget> </item> - <item row="0" column="1"> - <widget class="QCheckBox" name="showLibrary"> + <item row="2" column="1"> + <widget class="QPushButton" name="clearCache"> + <property name="enabled"> + <bool>false</bool> + </property> <property name="text"> - <string>Show when no game open</string> + <string>Clear cache</string> </property> </widget> </item> - <item row="2" column="0" colspan="2"> + <item row="3" column="0" colspan="2"> <widget class="Line" name="line_8"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> </widget> </item> - <item row="1" column="1"> - <widget class="QPushButton" name="clearCache"> - <property name="enabled"> - <bool>false</bool> + <item row="4" column="1"> + <widget class="QCheckBox" name="allowOpposingDirections"> + <property name="text"> + <string>Allow opposing input directions</string> </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QCheckBox" name="suspendScreensaver"> <property name="text"> - <string>Clear cache</string> + <string>Suspend screensaver</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QCheckBox" name="pauseOnFocusLost"> + <property name="text"> + <string>Pause when inactive</string> </property> </widget> </item>
@@ -14,10 +14,12 @@ #include <QMimeData>
#include <QPainter> #include <QStackedLayout> -#include "AboutScreen.h" #ifdef USE_SQLITE3 #include "ArchiveInspector.h" +#include "library/LibraryController.h" #endif + +#include "AboutScreen.h" #include "CheatsView.h" #include "ConfigController.h" #include "DebuggerConsole.h"@@ -108,7 +110,7 @@ m_savedScale = multiplier.toInt();
i = m_savedScale; } #ifdef USE_SQLITE3 - m_libraryView = new LibraryView(); + m_libraryView = new LibraryController(nullptr, ConfigController::configDir() + "/library.sqlite3", m_config); ConfigOption* showLibrary = m_config->addOption("showLibrary"); showLibrary->connect([this](const QVariant& value) { if (value.toBool()) {@@ -122,12 +124,17 @@ detachWidget(m_libraryView);
} }, this); m_config->updateOption("showLibrary"); + ConfigOption* libraryStyle = m_config->addOption("libraryStyle"); + libraryStyle->connect([this](const QVariant& value) { + m_libraryView->setViewStyle(static_cast<LibraryStyle>(value.toInt())); + }, this); + m_config->updateOption("libraryStyle"); - connect(m_libraryView, &LibraryView::accepted, [this]() { + connect(m_libraryView, &LibraryController::startGame, [this]() { VFile* output = m_libraryView->selectedVFile(); - QPair<QString, QString> path = m_libraryView->selectedPath(); if (output) { - m_controller->loadGame(output, path.first, path.second); + QPair<QString, QString> path = m_libraryView->selectedPath(); + m_controller->loadGame(output, path.second, path.first); } }); #elif defined(M_CORE_GBA)
@@ -28,7 +28,7 @@ class Display;
class GameController; class GDBController; class GIFView; -class LibraryView; +class LibraryController; class LogView; class ShaderSelector; class ShortcutController;@@ -199,7 +199,7 @@ GDBController* m_gdbController;
#endif #ifdef USE_SQLITE3 - LibraryView* m_libraryView; + LibraryController* m_libraryView; #endif };
@@ -0,0 +1,193 @@
+/* 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/. */ +#include "LibraryController.h" + +#include "../GBAApp.h" +#include "LibraryGrid.h" +#include "LibraryTree.h" + +namespace QGBA { + +LibraryEntry::LibraryEntry(mLibraryEntry* entry) + : entry(entry) + , m_fullpath(QString("%1/%2").arg(entry->base, entry->filename)) +{ +} + +void AbstractGameList::addEntries(QList<LibraryEntryRef> items) { + for (LibraryEntryRef o : items) { + addEntry(o); + } +} +void AbstractGameList::removeEntries(QList<LibraryEntryRef> items) { + for (LibraryEntryRef o : items) { + addEntry(o); + } +} + +LibraryLoaderThread::LibraryLoaderThread(QObject* parent) + : QThread(parent) +{ +} + +void LibraryLoaderThread::run() { + mLibraryLoadDirectory(m_library, m_directory.toUtf8().constData()); + m_directory = QString(); +} + +LibraryController::LibraryController(QWidget* parent, const QString& path, ConfigController* config) + : QStackedWidget(parent) + , m_config(config) +{ + mLibraryListingInit(&m_listing, 0); + + if (!path.isNull()) { + m_library = mLibraryLoad(path.toUtf8().constData()); + } else { + m_library = mLibraryCreateEmpty(); + } + + mLibraryAttachGameDB(m_library, GBAApp::app()->gameDB()); + + m_libraryTree = new LibraryTree(this); + addWidget(m_libraryTree->widget()); + + m_libraryGrid = new LibraryGrid(this); + addWidget(m_libraryGrid->widget()); + + connect(&m_loaderThread, &QThread::finished, this, &LibraryController::refresh, Qt::QueuedConnection); + + setViewStyle(LibraryStyle::STYLE_LIST); + refresh(); +} + +LibraryController::~LibraryController() { + mLibraryListingDeinit(&m_listing); + + if (m_loaderThread.isRunning()) { + m_loaderThread.wait(); + } + if (!m_loaderThread.isRunning() && m_loaderThread.m_library) { + m_library = m_loaderThread.m_library; + m_loaderThread.m_library = nullptr; + } + if (m_library) { + mLibraryDestroy(m_library); + } +} + +void LibraryController::setViewStyle(LibraryStyle newStyle) { + m_currentStyle = newStyle; + + AbstractGameList* newCurrentList = nullptr; + if (newStyle == LibraryStyle::STYLE_LIST || newStyle == LibraryStyle::STYLE_TREE) { + newCurrentList = m_libraryTree; + } else { + newCurrentList = m_libraryGrid; + } + newCurrentList->selectEntry(selectedEntry()); + newCurrentList->setViewStyle(newStyle); + setCurrentWidget(newCurrentList->widget()); + m_currentList = newCurrentList; +} + +void LibraryController::selectEntry(LibraryEntryRef entry) { + if (!m_currentList) { + return; + } + m_currentList->selectEntry(entry); +} + +LibraryEntryRef LibraryController::selectedEntry() { + if (!m_currentList) { + return LibraryEntryRef(); + } + return m_currentList->selectedEntry(); +} + +VFile* LibraryController::selectedVFile() { + LibraryEntryRef entry = selectedEntry(); + if (entry) { + return mLibraryOpenVFile(m_library, entry->entry); + } else { + return nullptr; + } +} + +QPair<QString, QString> LibraryController::selectedPath() { + LibraryEntryRef e = selectedEntry(); + return e ? qMakePair(e->base(), e->filename()) : qMakePair<QString, QString>("", ""); +} + +void LibraryController::addDirectory(const QString& dir) { + m_loaderThread.m_directory = dir; + m_loaderThread.m_library = m_library; + // The m_loaderThread temporarily owns the library + m_library = nullptr; + m_loaderThread.start(); +} + +void LibraryController::refresh() { + if (!m_library) { + if (!m_loaderThread.isRunning() && m_loaderThread.m_library) { + m_library = m_loaderThread.m_library; + m_loaderThread.m_library = nullptr; + } else { + return; + } + } + + setDisabled(true); + + QStringList allEntries; + QList<LibraryEntryRef> newEntries; + + mLibraryListingClear(&m_listing); + mLibraryGetEntries(m_library, &m_listing, 0, 0, nullptr); + for (size_t i = 0; i < mLibraryListingSize(&m_listing); i++) { + mLibraryEntry* entry = mLibraryListingGetPointer(&m_listing, i); + QString fullpath = QString("%1/%2").arg(entry->base, entry->filename); + if (m_entries.contains(fullpath)) { + m_entries.value(fullpath)->entry = entry; + } else { + LibraryEntryRef libentry = std::make_shared<LibraryEntry>(entry); + m_entries.insert(fullpath, libentry); + newEntries.append(libentry); + } + allEntries.append(fullpath); + } + + // Check for entries that were removed + QList<LibraryEntryRef> removedEntries; + for (QString& path : m_entries.keys()) { + if (!allEntries.contains(path)) { + removedEntries.append(m_entries.value(path)); + m_entries.remove(path); + } + } + + m_libraryTree->addEntries(newEntries); + m_libraryGrid->addEntries(newEntries); + + m_libraryTree->removeEntries(removedEntries); + m_libraryGrid->removeEntries(removedEntries); + + setDisabled(false); + selectLastBootedGame(); + emit doneLoading(); +} + +void LibraryController::selectLastBootedGame() { + if (!m_config) { + return; + } + const QString lastfile = m_config->getMRU().first(); + if (m_entries.contains(lastfile)) { + selectEntry(m_entries.value(lastfile)); + } +} + +}
@@ -0,0 +1,126 @@
+/* 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/. */ +#ifndef QGBA_LIBRARY_CONTROLLER +#define QGBA_LIBRARY_CONTROLLER + +#include <memory> + +#include <QList> +#include <QMap> +#include <QThread> +#include <QStackedWidget> + +#include <mgba/core/library.h> + +namespace QGBA { + +// Predefinitions +class LibraryGrid; +class LibraryTree; +class ConfigController; + +enum class LibraryStyle { + STYLE_LIST = 0, + STYLE_TREE, + STYLE_GRID, + STYLE_ICON +}; + +class LibraryEntry final { +public: + LibraryEntry(mLibraryEntry* entry); + + QString displayTitle() const { return title().isNull() ? filename() : title(); } + + QString base() const { return QString(entry->base); } + QString filename() const { return QString(entry->filename); } + QString fullpath() const { return m_fullpath; } + QString title() const { return QString(entry->title); } + QByteArray internalTitle() const { return QByteArray(entry->internalTitle); } + QByteArray internalCode() const { return QByteArray(entry->internalCode); } + mPlatform platform() const { return entry->platform; } + size_t filesize() const { return entry->filesize; } + uint32_t crc32() const { return entry->crc32; } + + const mLibraryEntry* entry; +private: + const QString m_fullpath; +}; +typedef std::shared_ptr<LibraryEntry> LibraryEntryRef; + +class AbstractGameList { +public: + virtual LibraryEntryRef selectedEntry() = 0; + virtual void selectEntry(LibraryEntryRef game) = 0; + + virtual void setViewStyle(LibraryStyle newStyle) = 0; + + virtual void addEntry(LibraryEntryRef item) = 0; + virtual void addEntries(QList<LibraryEntryRef> items); + + virtual void removeEntry(LibraryEntryRef item) = 0; + virtual void removeEntries(QList<LibraryEntryRef> items); + + virtual QWidget* widget() = 0; +}; + +class LibraryLoaderThread final : public QThread { +Q_OBJECT + +public: + LibraryLoaderThread(QObject* parent = nullptr); + + mLibrary* m_library = nullptr; + QString m_directory; + +protected: + virtual void run() override; +}; + +class LibraryController final : public QStackedWidget { +Q_OBJECT + +public: + LibraryController(QWidget* parent = nullptr, const QString& path = QString(), + ConfigController* config = nullptr); + ~LibraryController(); + + LibraryStyle viewStyle() const { return m_currentStyle; } + void setViewStyle(LibraryStyle newStyle); + + void selectEntry(LibraryEntryRef entry); + LibraryEntryRef selectedEntry(); + VFile* selectedVFile(); + QPair<QString, QString> selectedPath(); + + void selectLastBootedGame(); + + void addDirectory(const QString& dir); + +signals: + void startGame(); + void doneLoading(); + +private slots: + void refresh(); + +private: + ConfigController* m_config = nullptr; + LibraryLoaderThread m_loaderThread; + mLibrary* m_library = nullptr; + mLibraryListing m_listing; + QMap<QString, LibraryEntryRef> m_entries; + + LibraryStyle m_currentStyle; + AbstractGameList* m_currentList = nullptr; + + LibraryGrid* m_libraryGrid = nullptr; + LibraryTree* m_libraryTree = nullptr; +}; + +} + +#endif
@@ -0,0 +1,79 @@
+/* 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/. */ +#include "LibraryGrid.h" + +namespace QGBA { + +LibraryGrid::LibraryGrid(LibraryController* parent) + : m_widget(new QListWidget(parent)) +{ + m_widget->setObjectName("LibraryGrid"); + m_widget->setWrapping(true); + m_widget->setResizeMode(QListView::Adjust); + m_widget->setUniformItemSizes(true); + setViewStyle(LibraryStyle::STYLE_GRID); + + QObject::connect(m_widget, &QListWidget::itemActivated, parent, &LibraryController::startGame); +} + +LibraryGrid::~LibraryGrid() { + delete m_widget; +} + +LibraryEntryRef LibraryGrid::selectedEntry() { + if (!m_widget->selectedItems().empty()) { + return m_items.key(m_widget->selectedItems().at(0)); + } else { + return LibraryEntryRef(); + } +} + +void LibraryGrid::selectEntry(LibraryEntryRef game) { + if (!game) { + return; + } + if (!m_widget->selectedItems().empty()) { + m_widget->selectedItems().at(0)->setSelected(false); + } + m_items.value(game)->setSelected(true); +} + +void LibraryGrid::setViewStyle(LibraryStyle newStyle) { + if (newStyle == LibraryStyle::STYLE_GRID) { + m_currentStyle = LibraryStyle::STYLE_GRID; + m_widget->setIconSize(QSize(GRID_BANNER_WIDTH, GRID_BANNER_HEIGHT)); + m_widget->setViewMode(QListView::IconMode); + } else { + m_currentStyle = LibraryStyle::STYLE_ICON; + m_widget->setIconSize(QSize(ICON_BANNER_WIDTH, ICON_BANNER_HEIGHT)); + m_widget->setViewMode(QListView::ListMode); + } + + // QListView resets this when you change the view mode, so let's set it again + m_widget->setDragEnabled(false); +} + +void LibraryGrid::addEntry(LibraryEntryRef item) { + if (m_items.contains(item)) { + return; + } + + QListWidgetItem* i = new QListWidgetItem; + i->setText(item->displayTitle()); + + m_widget->addItem(i); + m_items.insert(item, i); +} + +void LibraryGrid::removeEntry(LibraryEntryRef entry) { + if (!m_items.contains(entry)) { + return; + } + + delete m_items.take(entry); +} + +}
@@ -0,0 +1,50 @@
+/* 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/. */ +#ifndef QGBA_LIBRARY_GRID +#define QGBA_LIBRARY_GRID + +#include <QListWidget> + +#include "LibraryController.h" + +namespace QGBA { + +class LibraryGrid final : public AbstractGameList { +public: + explicit LibraryGrid(LibraryController* parent = nullptr); + ~LibraryGrid(); + + // AbstractGameList stuff + virtual LibraryEntryRef selectedEntry() override; + virtual void selectEntry(LibraryEntryRef game) override; + + virtual void setViewStyle(LibraryStyle newStyle) override; + + virtual void addEntry(LibraryEntryRef item) override; + virtual void removeEntry(LibraryEntryRef entry) override; + + virtual QWidget* widget() override { return m_widget; } + +signals: + void startGame(); + +private: + QListWidget* m_widget; + + // Game banner image size + const quint32 GRID_BANNER_WIDTH = 320; + const quint32 GRID_BANNER_HEIGHT = 240; + + const quint32 ICON_BANNER_WIDTH = 64; + const quint32 ICON_BANNER_HEIGHT = 64; + + QMap<LibraryEntryRef, QListWidgetItem*> m_items; + LibraryStyle m_currentStyle; +}; + +} + +#endif
@@ -0,0 +1,179 @@
+/* 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/. */ +#include "LibraryTree.h" + +#include "utils.h" + +#include <QApplication> +#include <QDir> + +namespace QGBA { + +class TreeWidgetItem : public QTreeWidgetItem { +public: + TreeWidgetItem(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 TreeWidgetItem::setFilesize(size_t size) { + m_size = size; + setText(LibraryTree::COL_SIZE, niceSizeFormat(size)); +} + +bool TreeWidgetItem::operator<(const QTreeWidgetItem& other) const { + const int column = treeWidget()->sortColumn(); + return ((column == LibraryTree::COL_SIZE) ? + m_size < dynamic_cast<const TreeWidgetItem*>(&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("LibraryTree", "Name", nullptr), + QApplication::translate("LibraryTree", "Location", nullptr), + QApplication::translate("LibraryTree", "Platform", nullptr), + QApplication::translate("LibraryTree", "Size", nullptr), + QApplication::translate("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_pathNodes.values().contains(item)) { + emit m_controller->startGame(); + } + }); +} + +void LibraryTree::resizeAllCols() { + for (int i = 0; i < m_widget->columnCount(); i++) { + m_widget->resizeColumnToContents(i); + } +} + +LibraryEntryRef LibraryTree::selectedEntry() { + if (!m_widget->selectedItems().empty()) { + return m_items.key(m_widget->selectedItems().at(0)); + } else { + return LibraryEntryRef(); + } +} + +void LibraryTree::selectEntry(LibraryEntryRef game) { + if (!game) { + return; + } + if (!m_widget->selectedItems().empty()) { + m_widget->selectedItems().at(0)->setSelected(false); + } + m_items.value(game)->setSelected(true); +} + +void LibraryTree::setViewStyle(LibraryStyle newStyle) { + if (newStyle == LibraryStyle::STYLE_LIST) { + m_currentStyle = LibraryStyle::STYLE_LIST; + m_widget->setIndentation(0); + rebuildTree(); + } else { + m_currentStyle = LibraryStyle::STYLE_TREE; + m_widget->setIndentation(20); + rebuildTree(); + } +} + +void LibraryTree::addEntries(QList<LibraryEntryRef> items) { + m_deferredTreeRebuild = true; + AbstractGameList::addEntries(items); + m_deferredTreeRebuild = false; + rebuildTree(); +} + +void LibraryTree::addEntry(LibraryEntryRef item) { + if (m_items.contains(item)) { + return; + } + + QString folder = item->base(); + if (!m_pathNodes.contains(folder)) { + QTreeWidgetItem* i = new TreeWidgetItem; + i->setText(0, folder.section("/", -1)); + m_pathNodes.insert(folder, i); + if (m_currentStyle == LibraryStyle::STYLE_TREE) { + m_widget->addTopLevelItem(i); + } + } + + TreeWidgetItem* i = new TreeWidgetItem; + 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::AlignRight); + i->setText(COL_CRC32, QString("%0").arg(item->crc32(), 8, 16, QChar('0'))); + m_items.insert(item, i); + + rebuildTree(); +} + +void LibraryTree::removeEntry(LibraryEntryRef item) { + if (!m_items.contains(item)) { + return; + } + delete m_items.take(item); +} + +void LibraryTree::rebuildTree() { + if (m_deferredTreeRebuild) { + return; + } + + LibraryEntryRef currentGame = selectedEntry(); + + int count = m_widget->topLevelItemCount(); + for (int a = 0; a < count; a++) { + m_widget->takeTopLevelItem(0); + } + + for (QTreeWidgetItem* i : m_pathNodes.values()) { + count = i->childCount(); + for (int a = 0; a < count; a++) { + i->takeChild(0); + } + } + + if (m_currentStyle == LibraryStyle::STYLE_TREE) { + for (QTreeWidgetItem* i : m_pathNodes.values()) { + m_widget->addTopLevelItem(i); + } + for (QTreeWidgetItem* i : m_items.values()) { + m_pathNodes.value(m_items.key(i)->base())->addChild(i); + } + } else { + for (QTreeWidgetItem* i : m_items.values()) { + m_widget->addTopLevelItem(i); + } + } + + m_widget->expandAll(); + resizeAllCols(); + selectEntry(currentGame); +} + +}
@@ -0,0 +1,57 @@
+/* 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/. */ +#ifndef QGBA_LIBRARY_TREE +#define QGBA_LIBRARY_TREE + +#include <QTreeWidget> + +#include "LibraryController.h" + +namespace QGBA { + +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(); + + // AbstractGameList stuff + virtual LibraryEntryRef selectedEntry() override; + virtual void selectEntry(LibraryEntryRef game) override; + + virtual void setViewStyle(LibraryStyle newStyle) override; + + virtual void addEntries(QList<LibraryEntryRef> items) override; + virtual void addEntry(LibraryEntryRef item) override; + virtual void removeEntry(LibraryEntryRef item) override; + + virtual QWidget* widget() override { return m_widget; } + +private: + QTreeWidget* m_widget; + LibraryStyle m_currentStyle; + + LibraryController* m_controller; + + bool m_deferredTreeRebuild = false; + QMap<LibraryEntryRef, QTreeWidgetItem*> m_items; + QMap<QString, QTreeWidgetItem*> m_pathNodes; + + void rebuildTree(); + void resizeAllCols(); +}; + +} + +#endif
@@ -0,0 +1,40 @@
+/* Copyright (c) 2013-2017 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 "utils.h" + +#include <QObject> + +namespace QGBA { + +QString niceSizeFormat(size_t filesize) { + double size = filesize; + QString unit = "B"; + if (size >= 1024.0) { + size /= 1024.0; + unit = "kiB"; + } + if (size >= 1024.0) { + size /= 1024.0; + unit = "MiB"; + } + return QString("%0 %1").arg(size, 0, 'f', 1).arg(unit); +} +QString nicePlatformFormat(mPlatform platform) { + switch (platform) { +#ifdef M_CORE_GBA + case PLATFORM_GBA: + return QObject::tr("GBA"); +#endif +#ifdef M_CORE_GB + case PLATFORM_GB: + return QObject::tr("GB"); +#endif + default: + return QObject::tr("?"); + } +} + +}
@@ -0,0 +1,20 @@
+/* Copyright (c) 2013-2017 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/. */ +#ifndef QGBA_UTILS_H +#define QGBA_UTILS_H + +#include <mgba/core/core.h> + +#include <QString> + +namespace QGBA { + +QString niceSizeFormat(size_t filesize); +QString nicePlatformFormat(mPlatform platform); + +} + +#endif