Qt: Add forwarder UI and Vita backend (closes #2267)

This commit is contained in:
Vicki Pfau 2022-10-29 01:40:52 -07:00
parent fec87062ca
commit 56c9065f70
15 changed files with 1217 additions and 0 deletions

View File

@ -94,6 +94,11 @@ set(SOURCE_FILES
Display.cpp
DisplayGL.cpp
DisplayQt.cpp
ForwarderController.cpp
ForwarderGenerator.cpp
ForwarderGenerator3DS.cpp
ForwarderGeneratorVita.cpp
ForwarderView.cpp
FrameView.cpp
GBAApp.cpp
GBAKeyEditor.cpp
@ -151,6 +156,7 @@ set(UI_FILES
CheatsView.ui
DebuggerConsole.ui
DolphinConnector.ui
ForwarderView.ui
FrameView.ui
GIFView.ui
IOViewer.ui

View File

@ -0,0 +1,114 @@
/* 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 "ForwarderController.h"
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "ConfigController.h"
#include <mgba/core/version.h>
#include <mgba/feature/updater.h>
using namespace QGBA;
ForwarderController::ForwarderController(QObject* parent)
: QObject(parent)
, m_netman(new QNetworkAccessManager(this))
{
m_netman->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(this, &ForwarderController::buildFailed, this, [this]() {
m_inProgress = false;
});
connect(this, &ForwarderController::buildComplete, this, [this]() {
m_inProgress = false;
});
}
void ForwarderController::startBuild(const QString& outFilename) {
if (m_inProgress) {
return;
}
m_inProgress = true;
m_outFilename = outFilename;
downloadManifest();
}
void ForwarderController::downloadManifest() {
QNetworkReply* reply = m_netman->get(QNetworkRequest(QUrl("https://mgba.io/latest.ini")));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
gotManifest(reply);
});
connect(reply, &QNetworkReply::errorOccurred, this, [this, reply]() {
emit buildFailed();
});
}
void ForwarderController::gotManifest(QNetworkReply* reply) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
emit buildFailed();
return;
}
QByteArray manifest = reply->readAll();
QString platform = m_generator->systemName();
mUpdaterContext context;
if (!mUpdaterInit(&context, manifest.constData())) {
emit buildFailed();
return;
}
QString bucket = QLatin1String(mUpdaterGetBucket(&context));
mUpdate update;
mUpdaterGetUpdateForChannel(&context, platform.toUtf8().constData(), m_channel.toUtf8().constData(), &update);
downloadBuild({bucket + update.path});
mUpdaterDeinit(&context);
}
void ForwarderController::downloadBuild(const QUrl& url) {
QString extension(QFileInfo(url.path()).suffix());
// TODO: cache this
QString configDir(ConfigController::configDir());
m_sourceFile.setFileName(QString("%1/%2-%3-%4.%5").arg(configDir)
.arg(projectName)
.arg(m_generator->systemName())
.arg(channel())
.arg(extension));
if (!m_sourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
emit buildFailed();
return;
}
QNetworkReply* reply = m_netman->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
gotBuild(reply);
});
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
QByteArray data = reply->readAll();
m_sourceFile.write(data);
});
}
void ForwarderController::gotBuild(QNetworkReply* reply) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
emit buildFailed();
return;
}
QByteArray data = reply->readAll();
m_sourceFile.write(data);
m_sourceFile.close();
if (!m_generator->rebuild(m_sourceFile.fileName(), m_outFilename)) {
emit buildFailed();
} else {
emit buildComplete();
}
m_sourceFile.remove();
}

View File

@ -0,0 +1,54 @@
/* 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 <QFile>
#include <QObject>
#include "ForwarderGenerator.h"
#include <memory>
class QNetworkAccessManager;
class QNetworkReply;
namespace QGBA {
class ForwarderController : public QObject {
Q_OBJECT
public:
ForwarderController(QObject* parent = nullptr);
void setGenerator(std::unique_ptr<ForwarderGenerator>&& generator) { m_generator = std::move(generator); }
ForwarderGenerator* generator() { return m_generator.get(); }
QString channel() const { return m_channel; }
public slots:
void startBuild(const QString& outFilename);
signals:
void buildComplete();
void buildFailed();
private slots:
void gotManifest(QNetworkReply*);
void gotBuild(QNetworkReply*);
private:
void downloadManifest();
void downloadBuild(const QUrl&);
QString m_channel{"dev"};
QString m_outFilename;
QNetworkAccessManager* m_netman;
std::unique_ptr<ForwarderGenerator> m_generator;
QFile m_sourceFile;
bool m_inProgress = false;
};
}

View File

@ -0,0 +1,74 @@
/* 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 "ForwarderGenerator.h"
#include <QCryptographicHash>
#include <QFile>
#include "ForwarderGenerator3DS.h"
#include "ForwarderGeneratorVita.h"
using namespace QGBA;
std::unique_ptr<ForwarderGenerator> ForwarderGenerator::createForSystem(System system) {
switch (system) {
case System::N3DS:
return std::make_unique<ForwarderGenerator3DS>();
case System::VITA:
return std::make_unique<ForwarderGeneratorVita>();
}
return nullptr;
}
ForwarderGenerator::ForwarderGenerator(int imageTypes, QObject* parent)
: QObject(parent)
{
m_images.resize(imageTypes);
}
void ForwarderGenerator::setImage(int index, const QImage& image) {
if (index < 0 || index >= m_images.count()) {
return;
}
m_images[index] = image;
}
QImage ForwarderGenerator::image(int index) const {
if (index >= m_images.size()) {
return {};
}
return m_images[index];
}
QByteArray ForwarderGenerator::hashRom() const {
if (m_romPath.isEmpty()) {
return {};
}
QFile romFile(m_romPath);
if (!romFile.open(QIODevice::ReadOnly)) {
return {};
}
QCryptographicHash hash(QCryptographicHash::Sha256);
if (!hash.addData(&romFile)) {
return {};
}
return hash.result();
}
QString ForwarderGenerator::systemName(ForwarderGenerator::System system) {
switch (system) {
case ForwarderGenerator::System::N3DS:
return QLatin1String("3ds");
case ForwarderGenerator::System::VITA:
return QLatin1String("vita");
}
return {};
}

View File

@ -0,0 +1,57 @@
/* 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 <QByteArray>
#include <QImage>
#include <QObject>
#include <QString>
#include <QVector>
#include <memory>
namespace QGBA {
class ForwarderGenerator : public QObject {
Q_OBJECT
public:
enum class System {
N3DS,
VITA,
};
static std::unique_ptr<ForwarderGenerator> createForSystem(System);
void setTitle(const QString& title) { m_title = title; }
void setRom(const QString& path) { m_romPath = path; }
void setImage(int index, const QImage&);
QString title() const { return m_title; }
QString rom() const { return m_romPath; }
QImage image(int index) const;
QByteArray hashRom() const;
virtual QList<QPair<QString, QSize>> imageTypes() const = 0;
virtual System system() const = 0;
QString systemName() const { return systemName(system()); }
virtual QString extension() const = 0;
static QString systemName(System);
virtual bool rebuild(const QString& source, const QString& target) = 0;
protected:
ForwarderGenerator(int imageTypes, QObject* parent = nullptr);
private:
QString m_title;
QString m_romPath;
QVector<QImage> m_images;
};
}

View File

@ -0,0 +1,24 @@
/* 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 "ForwarderGenerator3DS.h"
using namespace QGBA;
ForwarderGenerator3DS::ForwarderGenerator3DS()
: ForwarderGenerator(2)
{
}
QList<QPair<QString, QSize>> ForwarderGenerator3DS::imageTypes() const {
return {
{ tr("Icon"), QSize(48, 48) },
{ tr("Banner"), QSize(256, 128) }
};
}
bool ForwarderGenerator3DS::rebuild(const QString& source, const QString& target) {
return false;
}

View File

@ -0,0 +1,25 @@
/* 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 "ForwarderGenerator.h"
namespace QGBA {
class ForwarderGenerator3DS final : public ForwarderGenerator {
Q_OBJECT
public:
ForwarderGenerator3DS();
QList<QPair<QString, QSize>> imageTypes() const override;
System system() const override { return System::N3DS; }
QString extension() const override { return QLatin1String("cia"); }
bool rebuild(const QString& source, const QString& target) override;
};
}

View File

@ -0,0 +1,187 @@
/* 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 "ForwarderGeneratorVita.h"
#include <QFileInfo>
#include <QTemporaryFile>
#include "VFileDevice.h"
#include <mgba-util/sfo.h>
#include <mgba-util/vfs.h>
using namespace QGBA;
ForwarderGeneratorVita::ForwarderGeneratorVita()
: ForwarderGenerator(3)
{
}
QList<QPair<QString, QSize>> ForwarderGeneratorVita::imageTypes() const {
return {
{ tr("Bubble"), QSize(128, 128) },
{ tr("Background"), QSize(840, 500) },
{ tr("Startup"), QSize(280, 158) }
};
}
bool ForwarderGeneratorVita::rebuild(const QString& source, const QString& target) {
QString vpk = dumpVpk(source);
if (vpk.isNull()) {
return false;
}
QFile vpkFile(vpk);
VDir* outdir = VDirOpenZip(target.toLocal8Bit().constData(), O_WRONLY | O_CREAT | O_TRUNC);
if (outdir && !copyAssets(vpk, outdir)) {
outdir->close(outdir);
outdir = nullptr;
}
vpkFile.remove();
if (!outdir) {
return false;
}
VFile* sfo = outdir->openFile(outdir, "sce_sys/param.sfo", O_WRONLY);
writeSfo(sfo);
sfo->close(sfo);
QFileInfo info(rom());
QByteArray buffer(info.fileName().toUtf8());
VFile* filename = outdir->openFile(outdir, "filename", O_WRONLY);
filename->write(filename, buffer.constData(), buffer.size());
filename->close(filename);
VFile* romfileOut = outdir->openFile(outdir, buffer.constData(), O_WRONLY);
VFileDevice romfileIn(rom(), QIODevice::ReadOnly);
VFileDevice::copyFile(romfileIn, romfileOut);
romfileIn.close();
romfileOut->close(romfileOut);
if (!image(0).isNull()) {
injectImage(outdir, "sce_sys/icon0.png", 0);
}
if (!image(1).isNull()) {
injectImage(outdir, "sce_sys/livearea/contents/bg.png", 1);
}
if (!image(2).isNull()) {
injectImage(outdir, "sce_sys/livearea/contents/startup.png", 2);
}
outdir->close(outdir);
return true;
}
QString ForwarderGeneratorVita::dumpVpk(const QString& archive) {
bool gotFile = false;
VDir* inArchive = VFileDevice::openArchive(archive);
if (!inArchive) {
return {};
}
for (VDirEntry* dirent = inArchive->listNext(inArchive); dirent; dirent = inArchive->listNext(inArchive)) {
if (dirent->type(dirent) != VFS_FILE) {
continue;
}
QString filename(dirent->name(dirent));
if (!filename.endsWith(".vpk")) {
continue;
}
VFile* outfile = VFileOpen("tmp.vpk", O_WRONLY | O_TRUNC | O_CREAT);
VFile* vpk = inArchive->openFile(inArchive, dirent->name(dirent), O_RDONLY);
VFileDevice::copyFile(vpk, outfile);
vpk->close(vpk);
outfile->close(outfile);
gotFile = true;
break;
}
inArchive->close(inArchive);
if (gotFile) {
return QLatin1String("tmp.vpk");
}
return {};
}
bool ForwarderGeneratorVita::copyAssets(const QString& vpk, VDir* outdir) {
VDir* indir = VDirOpenZip(vpk.toLocal8Bit().constData(), O_RDONLY);
if (!indir) {
return false;
}
bool ok = true;
for (VDirEntry* dirent = indir->listNext(indir); dirent; dirent = indir->listNext(indir)) {
if (dirent->name(dirent) == QLatin1String("sce_sys/param.sfo")) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/icon0.png") && !image(0).isNull()) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/livearea/contents/bg.png") && !image(1).isNull()) {
continue;
}
if (dirent->name(dirent) == QLatin1String("sce_sys/livearea/contents/startup.png") && !image(2).isNull()) {
continue;
}
if (dirent->type(dirent) != VFS_FILE) {
continue;
}
VFile* infile = indir->openFile(indir, dirent->name(dirent), O_RDONLY);
if (!infile) {
ok = false;
break;
}
VFile* outfile = outdir->openFile(outdir, dirent->name(dirent), O_WRONLY);
if (!outfile) {
infile->close(infile);
ok = false;
break;
}
VFileDevice::copyFile(infile, outfile);
infile->close(infile);
outfile->close(outfile);
}
indir->close(indir);
return ok;
}
QString ForwarderGeneratorVita::makeSerial() const {
QByteArray hash = hashRom();
quint32 hashBits = (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3];
QString serial("MFXXXXXXX");
for (int i = 0; i < 7; ++i) {
static const char alphabet[37] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
serial[i + 2] = alphabet[hashBits % 36];
hashBits /= 36;
}
return serial;
}
void ForwarderGeneratorVita::writeSfo(VFile* out) {
Table sfo;
QByteArray serial(makeSerial().toLocal8Bit());
QByteArray titleBytes(title().toUtf8());
SfoInit(&sfo);
SfoSetTitle(&sfo, titleBytes.constData());
SfoAddStrValue(&sfo, "TITLE_ID", serial.constData());
SfoWrite(&sfo, out);
SfoDeinit(&sfo);
}
void ForwarderGeneratorVita::injectImage(VDir* out, const char* name, int index) {
VFile* outfile = out->openFile(out, name, O_WRONLY);
VFileDevice outdev(outfile);
image(index).convertToFormat(QImage::Format_Indexed8).save(&outdev, "PNG");
}

View File

@ -0,0 +1,35 @@
/* 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 "ForwarderGenerator.h"
struct VDir;
struct VFile;
namespace QGBA {
class ForwarderGeneratorVita final : public ForwarderGenerator {
Q_OBJECT
public:
ForwarderGeneratorVita();
QList<QPair<QString, QSize>> imageTypes() const override;
System system() const override { return System::VITA; }
QString extension() const override { return QLatin1String("vpk"); }
bool rebuild(const QString& source, const QString& target) override;
private:
QString dumpVpk(const QString& archive);
bool copyAssets(const QString& vpk, VDir* out);
QString makeSerial() const;
void writeSfo(VFile* out);
void injectImage(VDir* out, const char* name, int index);
};
}

View File

@ -0,0 +1,159 @@
/* 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 "ForwarderView.h"
#include <QMessageBox>
#include <QResizeEvent>
#include "ForwarderGenerator.h"
#include "GBAApp.h"
#include "utils.h"
using namespace QGBA;
ForwarderView::ForwarderView(QWidget* parent)
: QDialog(parent)
{
m_ui.setupUi(this);
connectBrowseButton(m_ui.romBrowse, m_ui.romFilename, tr("Select ROM file"), false, romFilters());
connectBrowseButton(m_ui.outputBrowse, m_ui.outputFilename, tr("Select output filename"), true);
connectBrowseButton(m_ui.baseBrowse, m_ui.baseFilename, tr("Select base file"));
connect(m_ui.romFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.outputFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.baseFilename, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.title, &QLineEdit::textChanged, this, &ForwarderView::validate);
connect(m_ui.baseType, qOverload<int>(&QComboBox::currentIndexChanged), this, &ForwarderView::validate);
connect(m_ui.imageSelect, qOverload<int>(&QComboBox::currentIndexChanged), this, &ForwarderView::setActiveImage);
connect(m_ui.imageBrowse, &QAbstractButton::clicked, this, &ForwarderView::selectImage);
connect(&m_controller, &ForwarderController::buildComplete, this, &QDialog::accept);
connect(&m_controller, &ForwarderController::buildFailed, this, [this]() {
QMessageBox* error = new QMessageBox(QMessageBox::Critical, tr("Build failed"),
tr("Failed to build forwarder"),
QMessageBox::Ok, this, Qt::Sheet);
error->setAttribute(Qt::WA_DeleteOnClose);
error->show();
});
connect(m_ui.system3DS, &QAbstractButton::clicked, this, [this]() {
setSystem(ForwarderGenerator::System::N3DS);
});
connect(m_ui.systemVita, &QAbstractButton::clicked, this, [this]() {
setSystem(ForwarderGenerator::System::VITA);
});
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(m_ui.buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, [this]() {
m_controller.generator()->setRom(m_ui.romFilename->text());
m_controller.startBuild(m_ui.outputFilename->text());
});
}
void ForwarderView::build() {
if (!m_controller.generator()) {
return;
}
m_controller.generator()->setTitle(m_ui.title->text());
m_controller.generator()->setRom(m_ui.romFilename->text());
m_controller.startBuild(m_ui.outputFilename->text());
}
void ForwarderView::validate() {
bool valid = true;
if (m_ui.romFilename->text().isEmpty()) {
valid = false;
} else if (!QFileInfo(m_ui.romFilename->text()).exists()) {
valid = false;
}
if (m_ui.outputFilename->text().isEmpty()) {
valid = false;
}
if (m_ui.title->text().isEmpty()) {
valid = false;
}
if (!m_ui.system->checkedButton()) {
valid = false;
}
if (m_ui.baseType->currentIndex() != 1) {
valid = false;
}
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
}
void ForwarderView::setSystem(ForwarderGenerator::System system) {
m_controller.setGenerator(ForwarderGenerator::createForSystem(system));
auto types = m_controller.generator()->imageTypes();
m_images.clear();
m_images.resize(types.count());
m_ui.imageSelect->clear();
for (const auto& pair : types) {
m_ui.imageSelect->addItem(pair.first);
}
m_ui.imageSelect->setEnabled(true);
m_ui.imagePreview->setEnabled(true);
m_ui.imageBrowse->setEnabled(true);
m_ui.imagesLabel->setEnabled(true);
m_ui.preferredLabel->setEnabled(true);
m_ui.preferredWidth->setEnabled(true);
m_ui.preferredX->setEnabled(true);
m_ui.preferredHeight->setEnabled(true);
}
void ForwarderView::connectBrowseButton(QAbstractButton* button, QLineEdit* lineEdit, const QString& title, bool save, const QString& filter) {
connect(button, &QAbstractButton::clicked, lineEdit, [this, lineEdit, save, title, filter]() {
QString filename;
if (save) {
filename = GBAApp::app()->getSaveFileName(this, title, filter);
} else {
filename = GBAApp::app()->getOpenFileName(this, title, filter);
}
if (filename.isEmpty()) {
return;
}
lineEdit->setText(filename);
});
}
void ForwarderView::selectImage() {
QString filename = GBAApp::app()->getOpenFileName(this, tr("Select an image"), {});
if (filename.isEmpty()) {
return;
}
QImage image(filename);
if (image.isNull()) {
return;
}
image = image.scaled(m_activeSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
m_ui.imagePreview->setPixmap(QPixmap::fromImage(image));
m_ui.useDefaultImage->setChecked(false);
m_controller.generator()->setImage(m_currentImage, image);
}
void ForwarderView::setActiveImage(int index) {
if (index < 0) {
m_currentImage = -1;
m_activeSize = QSize();
return;
}
if (!m_controller.generator()) {
return;
}
auto types = m_controller.generator()->imageTypes();
if (index >= types.count()) {
return;
}
m_currentImage = index;
m_activeSize = types[index].second;
m_ui.preferredWidth->setText(QString::number(m_activeSize.width()));
m_ui.preferredHeight->setText(QString::number(m_activeSize.height()));
m_ui.imagePreview->setMaximumSize(m_activeSize);
m_ui.imagePreview->setPixmap(QPixmap::fromImage(m_controller.generator()->image(index)));
}

View File

@ -0,0 +1,42 @@
/* 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 "ForwarderController.h"
#include "ForwarderGenerator.h"
#include <QImage>
#include <QVector>
#include "ui_ForwarderView.h"
namespace QGBA {
class ForwarderView : public QDialog {
Q_OBJECT
public:
ForwarderView(QWidget* parent = nullptr);
private slots:
void build();
void validate();
private:
void setSystem(ForwarderGenerator::System);
void connectBrowseButton(QAbstractButton* button, QLineEdit* lineEdit, const QString& title, bool save = false, const QString& filter = {});
void selectImage();
void setActiveImage(int);
ForwarderController m_controller;
QVector<QImage> m_images;
int m_currentImage;
QSize m_activeSize;
Ui::ForwarderView m_ui;
};
}

View File

@ -0,0 +1,426 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QGBA::ForwarderView</class>
<widget class="QDialog" name="QGBA::ForwarderView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>710</width>
<height>465</height>
</rect>
</property>
<property name="windowTitle">
<string>Create forwarder</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>System</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item alignment="Qt::AlignHCenter">
<widget class="QRadioButton" name="system3DS">
<property name="text">
<string>3DS</string>
</property>
<attribute name="buttonGroup">
<string notr="true">system</string>
</attribute>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QRadioButton" name="systemVita">
<property name="text">
<string>Vita</string>
</property>
<attribute name="buttonGroup">
<string notr="true">system</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Files</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>ROM file:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="romFilename"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="romBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output filename:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="outputFilename"/>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="outputBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Forwarder base:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="baseType">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Latest stable verison</string>
</property>
</item>
<item>
<property name="text">
<string>Latest development build</string>
</property>
</item>
<item>
<property name="text">
<string>Specific file</string>
</property>
</item>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Base file:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="baseFilename">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QPushButton" name="baseBrowse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Presentation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="title"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="imagesLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Images:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="imageSelect">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="useDefaultImage">
<property name="text">
<string>Use default image</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,1,0" columnstretch="0,1,0">
<item row="0" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLabel" name="imagePreview">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="2,1,0,1,0">
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="preferredLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Preferred size:</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="preferredWidth">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">0</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="preferredX">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">×</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="preferredHeight">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string notr="true">0</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="imageBrowse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Select image file</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QGBA::ForwarderView</receiver>
<slot>deleteLater()</slot>
<hints>
<hint type="sourcelabel">
<x>354</x>
<y>439</y>
</hint>
<hint type="destinationlabel">
<x>354</x>
<y>232</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="system"/>
</buttongroups>
</ui>

View File

@ -184,6 +184,15 @@ VDir* VFileDevice::openArchive(const QString& path) {
return VDirOpenArchive(path.toUtf8().constData());
}
bool VFileDevice::copyFile(VFile* input, VFile* output) {
uint8_t buffer[0x800];
ssize_t size;
while ((size = input->read(input, buffer, sizeof(buffer))) > 0) {
output->write(output, buffer, size);
}
return size >= 0;
}
VFileAbstractWrapper::VFileAbstractWrapper(QIODevice* iodev)
: m_iodev(iodev)
{

View File

@ -42,6 +42,8 @@ public:
static VDir* openDir(const QString& path);
static VDir* openArchive(const QString& path);
static bool copyFile(VFile* input, VFile* output);
protected:
virtual qint64 readData(char* data, qint64 maxSize) override;
virtual qint64 writeData(const char* data, qint64 maxSize) override;

View File

@ -30,6 +30,7 @@
#include "Display.h"
#include "DolphinConnector.h"
#include "CoreController.h"
#include "ForwarderView.h"
#include "FrameView.h"
#include "GBAApp.h"
#include "GDBController.h"
@ -1615,6 +1616,8 @@ void Window::setupMenu(QMenuBar* menubar) {
m_actions.addAction(tr("Scripting..."), "scripting", this, &Window::scriptingOpen, "tools");
#endif
m_actions.addAction(tr("Create forwarder..."), "createForwarder", openTView<ForwarderView>(), "tools");
m_actions.addSeparator("tools");
m_actions.addAction(tr("Settings..."), "settings", this, &Window::openSettingsWindow, "tools")->setRole(Action::Role::SETTINGS);
m_actions.addAction(tr("Make portable"), "makePortable", this, &Window::tryMakePortable, "tools");