diff --git a/CHANGES b/CHANGES
index 3128c6f46..a3dfca799 100644
--- a/CHANGES
+++ b/CHANGES
@@ -17,6 +17,7 @@ Features:
- 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
diff --git a/src/platform/qt/ArchiveInspector.cpp b/src/platform/qt/ArchiveInspector.cpp
index a9d346121..200abac2a 100644
--- a/src/platform/qt/ArchiveInspector.cpp
+++ b/src/platform/qt/ArchiveInspector.cpp
@@ -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 {
diff --git a/src/platform/qt/ArchiveInspector.ui b/src/platform/qt/ArchiveInspector.ui
index 55e0cbe6e..405c2748e 100644
--- a/src/platform/qt/ArchiveInspector.ui
+++ b/src/platform/qt/ArchiveInspector.ui
@@ -29,15 +29,15 @@
-
-
+
- QGBA::LibraryView
+ QGBA::LibraryController
QWidget
-
+ library/LibraryController.h
1
diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt
index 464b12652..25069a1dc 100644
--- a/src/platform/qt/CMakeLists.txt
+++ b/src/platform/qt/CMakeLists.txt
@@ -105,6 +105,7 @@ set(SOURCE_FILES
Swatch.cpp
TilePainter.cpp
TileView.cpp
+ utils.cpp
Window.cpp
VFileDevice.cpp
VideoView.cpp)
@@ -117,7 +118,6 @@ set(UI_FILES
DebuggerConsole.ui
GIFView.ui
IOViewer.ui
- LibraryView.ui
LoadSaveState.ui
LogView.ui
MemoryView.ui
@@ -138,8 +138,6 @@ set(GBA_SRC
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 @@ endif()
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)
@@ -239,6 +238,8 @@ if(Qt5LinguistTools_FOUND)
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}")
diff --git a/src/platform/qt/LibraryModel.cpp b/src/platform/qt/LibraryModel.cpp
deleted file mode 100644
index bea45c4d6..000000000
--- a/src/platform/qt/LibraryModel.cpp
+++ /dev/null
@@ -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
-
-#include
-
-using namespace QGBA;
-
-Q_DECLARE_METATYPE(mLibraryEntry);
-
-QMap LibraryModel::s_handles;
-QMap 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(m_constraints.base));
- }
- m_constraints.base = strdup(path.toUtf8().constData());
- reload();
-}
-
-void LibraryModel::clearConstraints() {
- if (m_constraints.base) {
- free(const_cast(m_constraints.base));
- }
- if (m_constraints.filename) {
- free(const_cast(m_constraints.filename));
- }
- if (m_constraints.title) {
- free(const_cast(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 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);
-}
diff --git a/src/platform/qt/LibraryModel.h b/src/platform/qt/LibraryModel.h
deleted file mode 100644
index 970b08f87..000000000
--- a/src/platform/qt/LibraryModel.h
+++ /dev/null
@@ -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
-#include
-#include
-
-#include
-
-#include
-
-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, int = Qt::AlignLeft);
- QString name;
- std::function 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 s_handles;
-
- mLibraryEntry m_constraints;
- mLibraryListing m_listings;
- QStringList m_queue;
-
- QList m_columns;
- static QMap 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
diff --git a/src/platform/qt/LibraryView.cpp b/src/platform/qt/LibraryView.cpp
deleted file mode 100644
index 44b447e0a..000000000
--- a/src/platform/qt/LibraryView.cpp
+++ /dev/null
@@ -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
-
-#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 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);
-}
diff --git a/src/platform/qt/LibraryView.h b/src/platform/qt/LibraryView.h
deleted file mode 100644
index d56ea7ada..000000000
--- a/src/platform/qt/LibraryView.h
+++ /dev/null
@@ -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 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
diff --git a/src/platform/qt/LibraryView.ui b/src/platform/qt/LibraryView.ui
deleted file mode 100644
index 96c2a2bf6..000000000
--- a/src/platform/qt/LibraryView.ui
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- LibraryView
-
-
-
- 0
- 0
- 400
- 300
-
-
-
- Library
-
-
-
- 0
-
- -
-
-
- QAbstractItemView::NoEditTriggers
-
-
- true
-
-
- QAbstractItemView::SelectRows
-
-
- false
-
-
- true
-
-
- false
-
-
- 0
-
-
-
-
-
-
-
-
diff --git a/src/platform/qt/SettingsView.cpp b/src/platform/qt/SettingsView.cpp
index 9f5b7527c..6848e4cd6 100644
--- a/src/platform/qt/SettingsView.cpp
+++ b/src/platform/qt/SettingsView.cpp
@@ -204,6 +204,7 @@ void SettingsView::updateConfig() {
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);
diff --git a/src/platform/qt/SettingsView.ui b/src/platform/qt/SettingsView.ui
index caabf1c4c..8fca6efaf 100644
--- a/src/platform/qt/SettingsView.ui
+++ b/src/platform/qt/SettingsView.ui
@@ -398,28 +398,28 @@
- -
-
+
-
+
- Allow opposing input directions
-
-
-
- -
-
-
- Suspend screensaver
+ Show when no game open
true
- -
-
-
- Pause when inactive
-
+
-
+
+
-
+
+ List view
+
+
+ -
+
+ Tree view
+
+
-
@@ -429,21 +429,7 @@
- -
-
-
- Show when no game open
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
+
-
false
@@ -453,6 +439,37 @@
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Allow opposing input directions
+
+
+
+ -
+
+
+ Suspend screensaver
+
+
+ true
+
+
+
+ -
+
+
+ Pause when inactive
+
+
+
diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp
index 98bfa13b1..d6f24e2e5 100644
--- a/src/platform/qt/Window.cpp
+++ b/src/platform/qt/Window.cpp
@@ -14,10 +14,12 @@
#include
#include
-#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 @@ Window::Window(ConfigController* config, int playerId, QWidget* parent)
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 @@ Window::Window(ConfigController* config, int playerId, QWidget* parent)
}
}, this);
m_config->updateOption("showLibrary");
+ ConfigOption* libraryStyle = m_config->addOption("libraryStyle");
+ libraryStyle->connect([this](const QVariant& value) {
+ m_libraryView->setViewStyle(static_cast(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 path = m_libraryView->selectedPath();
if (output) {
- m_controller->loadGame(output, path.first, path.second);
+ QPair path = m_libraryView->selectedPath();
+ m_controller->loadGame(output, path.second, path.first);
}
});
#elif defined(M_CORE_GBA)
diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h
index bd24621bd..c9a5d465c 100644
--- a/src/platform/qt/Window.h
+++ b/src/platform/qt/Window.h
@@ -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 @@ private:
#endif
#ifdef USE_SQLITE3
- LibraryView* m_libraryView;
+ LibraryController* m_libraryView;
#endif
};
diff --git a/src/platform/qt/library/LibraryController.cpp b/src/platform/qt/library/LibraryController.cpp
new file mode 100644
index 000000000..ecb8a1fab
--- /dev/null
+++ b/src/platform/qt/library/LibraryController.cpp
@@ -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 items) {
+ for (LibraryEntryRef o : items) {
+ addEntry(o);
+ }
+}
+void AbstractGameList::removeEntries(QList 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 LibraryController::selectedPath() {
+ LibraryEntryRef e = selectedEntry();
+ return e ? qMakePair(e->base(), e->filename()) : qMakePair("", "");
+}
+
+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 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(entry);
+ m_entries.insert(fullpath, libentry);
+ newEntries.append(libentry);
+ }
+ allEntries.append(fullpath);
+ }
+
+ // Check for entries that were removed
+ QList 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));
+ }
+}
+
+}
diff --git a/src/platform/qt/library/LibraryController.h b/src/platform/qt/library/LibraryController.h
new file mode 100644
index 000000000..3cc82c8b5
--- /dev/null
+++ b/src/platform/qt/library/LibraryController.h
@@ -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
+
+#include
+#include
+#include
+#include
+
+#include
+
+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 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 items);
+
+ virtual void removeEntry(LibraryEntryRef item) = 0;
+ virtual void removeEntries(QList 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 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 m_entries;
+
+ LibraryStyle m_currentStyle;
+ AbstractGameList* m_currentList = nullptr;
+
+ LibraryGrid* m_libraryGrid = nullptr;
+ LibraryTree* m_libraryTree = nullptr;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/library/LibraryGrid.cpp b/src/platform/qt/library/LibraryGrid.cpp
new file mode 100644
index 000000000..d4e4acacf
--- /dev/null
+++ b/src/platform/qt/library/LibraryGrid.cpp
@@ -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);
+}
+
+}
diff --git a/src/platform/qt/library/LibraryGrid.h b/src/platform/qt/library/LibraryGrid.h
new file mode 100644
index 000000000..98a0df630
--- /dev/null
+++ b/src/platform/qt/library/LibraryGrid.h
@@ -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
+
+#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 m_items;
+ LibraryStyle m_currentStyle;
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/library/LibraryTree.cpp b/src/platform/qt/library/LibraryTree.cpp
new file mode 100644
index 000000000..45b972d77
--- /dev/null
+++ b/src/platform/qt/library/LibraryTree.cpp
@@ -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
+#include
+
+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(&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 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);
+}
+
+}
diff --git a/src/platform/qt/library/LibraryTree.h b/src/platform/qt/library/LibraryTree.h
new file mode 100644
index 000000000..80e3b461d
--- /dev/null
+++ b/src/platform/qt/library/LibraryTree.h
@@ -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
+
+#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 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 m_items;
+ QMap m_pathNodes;
+
+ void rebuildTree();
+ void resizeAllCols();
+};
+
+}
+
+#endif
diff --git a/src/platform/qt/utils.cpp b/src/platform/qt/utils.cpp
new file mode 100644
index 000000000..1c16df932
--- /dev/null
+++ b/src/platform/qt/utils.cpp
@@ -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
+
+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("?");
+ }
+}
+
+}
diff --git a/src/platform/qt/utils.h b/src/platform/qt/utils.h
new file mode 100644
index 000000000..210e1f85a
--- /dev/null
+++ b/src/platform/qt/utils.h
@@ -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
+
+#include
+
+namespace QGBA {
+
+QString niceSizeFormat(size_t filesize);
+QString nicePlatformFormat(mPlatform platform);
+
+}
+
+#endif