Qt: Add bug report tool

This commit is contained in:
Vicki Pfau 2020-12-09 18:30:36 -08:00
parent a5f3718f81
commit 0065b62633
9 changed files with 734 additions and 4 deletions

View File

@ -5,6 +5,7 @@ Features:
- Separate overrides for GBC games that can also run on SGB or regular GB - Separate overrides for GBC games that can also run on SGB or regular GB
- Game Boy Player features can be enabled by default for all compatible games - Game Boy Player features can be enabled by default for all compatible games
- Frame viewer support for Game Boy - Frame viewer support for Game Boy
- Bug report tool for gathering information helpful for reporting bugs
- Mute option in homebrew ports - Mute option in homebrew ports
- Status indicators for fast-forward and mute in homebrew ports - Status indicators for fast-forward and mute in homebrew ports
- VBA bug compatibility mode for ROM hacks that don't work on real hardware - VBA bug compatibility mode for ROM hacks that don't work on real hardware

View File

@ -100,6 +100,7 @@ set(SOURCE_FILES
PaletteView.cpp PaletteView.cpp
PlacementControl.cpp PlacementControl.cpp
RegisterView.cpp RegisterView.cpp
ReportView.cpp
ROMInfo.cpp ROMInfo.cpp
RotatedHeaderView.cpp RotatedHeaderView.cpp
SavestateButton.cpp SavestateButton.cpp
@ -139,6 +140,7 @@ set(UI_FILES
PaletteView.ui PaletteView.ui
PlacementControl.ui PlacementControl.ui
PrinterView.ui PrinterView.ui
ReportView.ui
ROMInfo.ui ROMInfo.ui
SensorView.ui SensorView.ui
SettingsView.ui SettingsView.ui

View File

@ -27,12 +27,8 @@ Q_OBJECT
public: public:
enum class Driver { enum class Driver {
QT = 0, QT = 0,
#if defined(BUILD_GL) || defined(BUILD_GLES2) || defined(USE_EPOXY)
OPENGL = 1, OPENGL = 1,
#endif
#ifdef BUILD_GL
OPENGL1 = 2, OPENGL1 = 2,
#endif
}; };
Display(QWidget* parent = nullptr); Display(QWidget* parent = nullptr);

View File

@ -56,6 +56,7 @@ public:
static QString dataDir(); static QString dataDir();
QList<Window*> windows() { return m_windows; }
Window* newWindow(); Window* newWindow();
QString getOpenFileName(QWidget* owner, const QString& title, const QString& filter = {}); QString getOpenFileName(QWidget* owner, const QString& title, const QString& filter = {});

View File

@ -0,0 +1,436 @@
/* Copyright (c) 2013-2020 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 "ReportView.h"
#include <QBuffer>
#include <QDesktopServices>
#include <QOffscreenSurface>
#include <QScreen>
#include <QSysInfo>
#include <mgba/core/version.h>
#include <mgba-util/vfs.h>
#include "CoreController.h"
#include "GBAApp.h"
#include "Window.h"
#include "ui_ReportView.h"
#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
#define USE_CPUID
#include <cpuid.h>
#endif
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
#define USE_CPUID
#endif
#if defined(BUILD_GL) || defined(BUILD_GLES2) || defined(BUILD_GLES3) || defined(USE_EPOXY)
#define DISPLAY_GL_INFO
#include "DisplayGL.h"
#include <QOpenGLFunctions>
#endif
#ifdef USE_SQLITE3
#include "feature/sqlite3/no-intro.h"
#endif
using namespace QGBA;
static const QLatin1String yesNo[2] = {
QLatin1String("No"),
QLatin1String("Yes")
};
#ifdef USE_CPUID
unsigned ReportView::s_cpuidMax = 0xFFFFFFFF;
unsigned ReportView::s_cpuidExtMax = 0xFFFFFFFF;
#endif
ReportView::ReportView(QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
{
m_ui.setupUi(this);
QString description = m_ui.description->text();
description.replace("{projectName}", QLatin1String(projectName));
m_ui.description->setText(description);
connect(m_ui.fileList, &QListWidget::currentTextChanged, this, &ReportView::setShownReport);
}
void ReportView::generateReport() {
m_displayOrder.clear();
m_reports.clear();
QDir configDir(ConfigController::configDir());
QStringList swReport;
swReport << QString("Name: %1").arg(QLatin1String(projectName));
swReport << QString("Executable location: %1").arg(redact(QCoreApplication::applicationFilePath()));
swReport << QString("Portable: %1").arg(yesNo[ConfigController::isPortable()]);
swReport << QString("Configuration directory: %1").arg(redact(configDir.path()));
swReport << QString("Version: %1").arg(QLatin1String(projectVersion));
swReport << QString("Git branch: %1").arg(QLatin1String(gitBranch));
swReport << QString("Git commit: %1").arg(QLatin1String(gitCommit));
swReport << QString("Git revision: %1").arg(gitRevision);
swReport << QString("OS: %1").arg(QSysInfo::prettyProductName());
swReport << QString("Build architecture: %1").arg(QSysInfo::buildCpuArchitecture());
swReport << QString("Run architecture: %1").arg(QSysInfo::currentCpuArchitecture());
swReport << QString("Qt version: %1").arg(QLatin1String(qVersion()));
addReport(QString("System info"), swReport.join('\n'));
QStringList hwReport;
addCpuInfo(hwReport);
addGLInfo(hwReport);
addReport(QString("Hardware info"), hwReport.join('\n'));
QList<QScreen*> screens = QGuiApplication::screens();
std::sort(screens.begin(), screens.end(), [](const QScreen* a, const QScreen* b) {
if (a->geometry().y() < b->geometry().y()) {
return true;
}
if (a->geometry().x() < b->geometry().x()) {
return true;
}
return false;
});
int screenId = 0;
for (const QScreen* screen : screens) {
++screenId;
QStringList screenReport;
addScreenInfo(screenReport, screen);
addReport(QString("Screen %1").arg(screenId), screenReport.join('\n'));
}
QList<QPair<QString, QByteArray>> deferredBinaries;
QList<ConfigController*> configs;
int winId = 0;
for (auto window : GBAApp::app()->windows()) {
++winId;
QStringList windowReport;
auto controller = window->controller();
ConfigController* config = window->config();
if (configs.indexOf(config) < 0) {
configs.append(config);
}
windowReport << QString("Window size: %1x%2").arg(window->width()).arg(window->height());
windowReport << QString("Window location: %1, %2").arg(window->x()).arg(window->y());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QScreen* screen = window->screen();
#else
QScreen* screen = NULL;
if (window->windowHandle()) {
screen = window->windowHandle()->screen();
}
#endif
if (screen && screens.contains(screen)) {
windowReport << QString("Screen: %1").arg(screens.contains(screen) + 1);
} else {
windowReport << QString("Screen: Unknown");
}
if (controller) {
windowReport << QString("ROM open: Yes");
{
CoreController::Interrupter interrupter(controller);
addROMInfo(windowReport, controller.get());
if (m_ui.includeSave->isChecked() && !m_ui.includeState->isChecked()) {
// Only do the save separately if savestates aren't enabled, to guarantee consistency
mCore* core = controller->thread()->core;
void* sram = NULL;
size_t size = core->savedataClone(core, &sram);
if (sram) {
QByteArray save(static_cast<const char*>(sram), size);
free(sram);
deferredBinaries.append(qMakePair(QString("Save %1").arg(winId), save));
}
}
}
if (m_ui.includeState->isChecked()) {
QBuffer state;
int flags = SAVESTATE_SCREENSHOT | SAVESTATE_CHEATS | SAVESTATE_RTC | SAVESTATE_METADATA;
if (m_ui.includeSave->isChecked()) {
flags |= SAVESTATE_SAVEDATA;
}
controller->saveState(&state, flags);
deferredBinaries.append(qMakePair(QString("State %1").arg(winId), state.buffer()));
if (m_ui.includeSave->isChecked()) {
VFile* vf = VFileDevice::wrap(&state, QIODevice::ReadOnly);
mStateExtdata extdata;
mStateExtdataItem savedata;
mStateExtdataInit(&extdata);
if (mCoreExtractExtdata(controller->thread()->core, vf, &extdata) && mStateExtdataGet(&extdata, EXTDATA_SAVEDATA, &savedata)) {
QByteArray save(static_cast<const char*>(savedata.data), savedata.size);
deferredBinaries.append(qMakePair(QString("Save %1").arg(winId), save));
}
mStateExtdataDeinit(&extdata);
}
}
} else {
windowReport << QString("ROM open: No");
}
windowReport << QString("Configuration: %1").arg(configs.indexOf(config) + 1);
addReport(QString("Window %1").arg(winId), windowReport.join('\n'));
}
for (ConfigController* config : configs) {
VFile* vf = VFileDevice::openMemory();
mCoreConfigSaveVFile(config->config(), vf);
void* contents = vf->map(vf, vf->size(vf), MAP_READ);
if (contents) {
QString report(QString::fromUtf8(static_cast<const char*>(contents), vf->size(vf)));
addReport(QString("Configuration %1").arg(configs.indexOf(config) + 1), redact(report));
vf->unmap(vf, contents, vf->size(vf));
}
vf->close(vf);
}
QFile qtIni(configDir.filePath("qt.ini"));
if (qtIni.open(QIODevice::ReadOnly | QIODevice::Text)) {
addReport(QString("Qt Configuration"), redact(QString::fromUtf8(qtIni.readAll())));
qtIni.close();
}
std::sort(deferredBinaries.begin(), deferredBinaries.end());
for (auto& pair : deferredBinaries) {
addBinary(pair.first, pair.second);
}
rebuildModel();
}
void ReportView::save() {
QString filename = GBAApp::app()->getSaveFileName(this, tr("Bug report archive"), tr("ZIP archive (*.zip)"));
if (filename.isNull()) {
return;
}
VDir* zip = VDirOpenZip(filename.toLocal8Bit().constData(), O_WRONLY | O_CREAT | O_TRUNC);
if (!zip) {
return;
}
for (const auto& filename : m_displayOrder) {
VFileDevice vf(zip->openFile(zip, filename.toLocal8Bit().constData(), O_WRONLY));
if (m_reports.contains(filename)) {
vf.setTextModeEnabled(true);
vf.write(m_reports[filename].toUtf8());
} else if (m_binaries.contains(filename)) {
vf.write(m_binaries[filename]);
}
vf.close();
}
zip->close(zip);
}
void ReportView::setShownReport(const QString& filename) {
m_ui.fileView->setPlainText(m_reports[filename]);
}
void ReportView::rebuildModel() {
m_ui.fileList->clear();
for (const auto& filename : m_displayOrder) {
QListWidgetItem* item = new QListWidgetItem(filename);
if (m_binaries.contains(filename)) {
item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
}
m_ui.fileList->addItem(item);
}
m_ui.save->setEnabled(true);
m_ui.fileList->setEnabled(true);
m_ui.fileView->setEnabled(true);
m_ui.openList->setEnabled(true);
m_ui.fileList->setCurrentRow(0);
m_ui.fileView->installEventFilter(this);
}
void ReportView::openBugReportPage() {
QDesktopServices::openUrl(QUrl("https://mgba.io/i/"));
}
void ReportView::addCpuInfo(QStringList& report) {
#ifdef USE_CPUID
std::array<unsigned, 4> regs;
if (!cpuid(0, regs)) {
return;
}
unsigned vendor[4] = { regs[1], regs[3], regs[2], 0 };
auto testBit = [](unsigned bit, unsigned reg) {
return yesNo[bool(reg & (1 << bit))];
};
QStringList features;
report << QString("CPU manufacturer: %1").arg(QLatin1String(reinterpret_cast<char*>(vendor)));
cpuid(1, regs);
unsigned family = ((regs[0] >> 8) & 0xF) | ((regs[0] >> 16) & 0xFF0);
unsigned model = ((regs[0] >> 4) & 0xF) | ((regs[0] >> 12) & 0xF0);
report << QString("CPU family: %1h").arg(family, 2, 16, QChar('0'));
report << QString("CPU model: %1h").arg(model, 2, 16, QChar('0'));
features << QString("Supports SSE: %1").arg(testBit(25, regs[3]));
features << QString("Supports SSE2: %1").arg(testBit(26, regs[3]));
features << QString("Supports SSE3: %1").arg(testBit(0, regs[2]));
features << QString("Supports SSSE3: %1").arg(testBit(9, regs[2]));
features << QString("Supports SSE4.1: %1").arg(testBit(19, regs[2]));
features << QString("Supports SSE4.2: %1").arg(testBit(20, regs[2]));
features << QString("Supports MOVBE: %1").arg(testBit(22, regs[2]));
features << QString("Supports POPCNT: %1").arg(testBit(23, regs[2]));
features << QString("Supports RDRAND: %1").arg(testBit(30, regs[2]));
features << QString("Supports AVX: %1").arg(testBit(28, regs[2]));
cpuid(7, 0, regs);
features << QString("Supports AVX2: %1").arg(testBit(5, regs[1]));
features << QString("Supports BMI1: %1").arg(testBit(3, regs[1]));
features << QString("Supports BMI2: %1").arg(testBit(8, regs[1]));
cpuid(0x80000001, regs);
features << QString("Supports ABM: %1").arg(testBit(5, regs[2]));
features << QString("Supports SSE4a: %1").arg(testBit(6, regs[2]));
features.sort();
report << features;
#endif
}
void ReportView::addGLInfo(QStringList& report) {
#ifdef DISPLAY_GL_INFO
QSurfaceFormat format;
report << QString("OpenGL type: %1").arg(QLatin1String(QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL ? "OpenGL" : "OpenGL|ES"));
format.setVersion(1, 4);
report << QString("OpenGL supports legacy (1.x) contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) {
format.setVersion(2, 0);
} else {
format.setVersion(3, 2);
}
format.setProfile(QSurfaceFormat::CoreProfile);
report << QString("OpenGL supports core contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
QOpenGLContext context;
if (context.create()) {
QOffscreenSurface surface;
surface.create();
context.makeCurrent(&surface);
report << QString("OpenGL renderer: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_RENDERER))));
report << QString("OpenGL vendor: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VENDOR))));
report << QString("OpenGL version string: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VERSION))));
}
#else
report << QString("OpenGL support disabled at compilation time");
#endif
}
void ReportView::addROMInfo(QStringList& report, CoreController* controller) {
report << QString("Currently paused: %1").arg(yesNo[controller->isPaused()]);
mCore* core = controller->thread()->core;
char title[17] = {};
core->getGameTitle(core, title);
report << QString("Internal title: %1").arg(QLatin1String(title));
title[8] = '\0';
core->getGameCode(core, title);
if (title[0]) {
report << QString("Game code: %1").arg(QLatin1String(title));
} else {
report << QString("Invalid game code");
}
uint32_t crc32 = 0;
core->checksum(core, &crc32, CHECKSUM_CRC32);
report << QString("CRC32: %1").arg(crc32, 8, 16, QChar('0'));
#ifdef USE_SQLITE3
const NoIntroDB* db = GBAApp::app()->gameDB();
if (db && crc32) {
NoIntroGame game{};
if (NoIntroDBLookupGameByCRC(db, crc32, &game)) {
report << QString("No-Intro name: %1").arg(game.name);
} else {
report << QString("Not present in No-Intro database").arg(game.name);
}
}
#endif
}
void ReportView::addScreenInfo(QStringList& report, const QScreen* screen) {
QRect geometry = screen->geometry();
report << QString("Size: %1x%2").arg(geometry.width()).arg(geometry.height());
report << QString("Location: %1, %2").arg(geometry.x()).arg(geometry.y());
report << QString("Refresh rate: %1 Hz").arg(screen->refreshRate());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
report << QString("Pixel ratio: %1").arg(screen->devicePixelRatio());
#endif
report << QString("Logical DPI: %1x%2").arg(screen->logicalDotsPerInchX()).arg(screen->logicalDotsPerInchY());
report << QString("Physical DPI: %1x%2").arg(screen->physicalDotsPerInchX()).arg(screen->physicalDotsPerInchY());
}
void ReportView::addReport(const QString& filename, const QString& report) {
m_reports[filename] = report;
m_displayOrder.append(filename);
}
void ReportView::addBinary(const QString& filename, const QByteArray& binary) {
m_binaries[filename] = binary;
m_displayOrder.append(filename);
}
QString ReportView::redact(const QString& text) {
static QRegularExpression home(R"((?:\b|^)[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/]+|(?:/usr)?/home/[^/]+)",
QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption);
QString redacted = text;
redacted.replace(home, QString("[Home directory]"));
return redacted;
}
bool ReportView::eventFilter(QObject*, QEvent* event) {
if (event->type() != QEvent::FocusOut) {
QListWidgetItem* currentReport = m_ui.fileList->currentItem();
if (currentReport && !currentReport->text().isNull()) {
m_reports[currentReport->text()] = m_ui.fileView->toPlainText();
}
}
return false;
}
#ifdef USE_CPUID
bool ReportView::cpuid(unsigned id, std::array<unsigned, 4>& regs) {
return cpuid(id, 0, regs);
}
bool ReportView::cpuid(unsigned id, unsigned sub, std::array<unsigned, 4>& regs) {
if (s_cpuidMax == 0xFFFFFFFF) {
#ifdef _MSC_VER
__cpuid(reinterpret_cast<int*>(regs.data()), 0);
s_cpuidMax = regs[0];
__cpuid(reinterpret_cast<int*>(regs.data()), 0x80000000);
s_cpuidExtMax = regs[0];
#else
s_cpuidMax = __get_cpuid_max(0, nullptr);
s_cpuidExtMax = __get_cpuid_max(0x80000000, nullptr);
#endif
}
regs[0] = 0;
regs[1] = 0;
regs[2] = 0;
regs[3] = 0;
if (!(id & 0x80000000) && id > s_cpuidMax) {
return false;
}
if ((id & 0x80000000) && id > s_cpuidExtMax) {
return false;
}
#ifdef _MSC_VER
__cpuidex(reinterpret_cast<int*>(regs.data()), id, sub);
#else
__cpuid_count(id, sub, regs[0], regs[1], regs[2], regs[3]);
#endif
return true;
}
#endif

View File

@ -0,0 +1,68 @@
/* Copyright (c) 2013-2020 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 <QDialog>
#include <QHash>
#include <array>
#include <memory>
#include "ConfigController.h"
#include "ui_ReportView.h"
namespace QGBA {
class ConfigController;
class CoreController;
class ReportView : public QDialog {
Q_OBJECT
public:
ReportView(QWidget* parent = nullptr);
public slots:
void generateReport();
void save();
private slots:
void setShownReport(const QString&);
void rebuildModel();
void openBugReportPage();
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
void addCpuInfo(QStringList&);
void addGLInfo(QStringList&);
void addROMInfo(QStringList&, CoreController*);
void addScreenInfo(QStringList&, const QScreen*);
void addReport(const QString& filename, const QString& report);
void addBinary(const QString& filename, const QByteArray& report);
QString redact(const QString& text);
#if (defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))) || (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64)))
static bool cpuid(unsigned id, std::array<unsigned, 4>& regs);
static bool cpuid(unsigned id, unsigned sub, std::array<unsigned, 4>& regs);
static unsigned s_cpuidMax;
static unsigned s_cpuidExtMax;
#endif
ConfigController* m_config;
QStringList m_displayOrder;
QHash<QString, QString> m_reports;
QHash<QString, QByteArray> m_binaries;
Ui::ReportView m_ui;
};
}

View File

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReportView</class>
<widget class="QDialog" name="ReportView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>855</width>
<height>474</height>
</rect>
</property>
<property name="windowTitle">
<string>Generate Bug Report</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="3,2,7,0">
<item row="1" column="1" rowspan="3">
<widget class="QListWidget" name="fileList">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget>
</item>
<item row="1" column="2" rowspan="3">
<widget class="QPlainTextEdit" name="fileView">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="textInteractionFlags">
<set>Qt::TextEditorInteraction</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="description">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;To file a bug report, please first generate a report file to attach to the bug report you're about to file. It is recommended that you include the save files, as these often help with debugging issues. This will collect some information about the version of {projectName} you're running, your configuration, your computer, and the game you currently have open (if any). Once this collection is completed you can review all of the information gathered below and save it to a zip file. The collection will automatically attempt to redact any personal information, such as your username if it's in any of the paths gathered, but just in case you can edit it afterwards. After you have generated and saved it, please click the button below or go to &lt;a href=&quot;https://mgba.io/i/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#2980b9;&quot;&gt;mgba.io/i&lt;/span&gt;&lt;/a&gt; to file the bug report on GitHub. Make sure to attach the report you generated!&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1" rowspan="2" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="generate">
<property name="text">
<string>Generate report</string>
</property>
<property name="icon">
<iconset theme="view-refresh">
<normaloff>../../../../../../</normaloff>../../../../../../</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="save">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save</string>
</property>
<property name="icon">
<iconset theme="document-save">
<normaloff>../../../../../../</normaloff>../../../../../../</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openList">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Open issue list in browser</string>
</property>
<property name="icon">
<iconset theme="document-send">
<normaloff>../../../../../../</normaloff>../../../../../../</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" rowspan="4">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="includeSave">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Include save file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeState">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Create and include savestate</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>openList</sender>
<signal>clicked()</signal>
<receiver>ReportView</receiver>
<slot>openBugReportPage()</slot>
<hints>
<hint type="sourcelabel">
<x>593</x>
<y>442</y>
</hint>
<hint type="destinationlabel">
<x>357</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>generate</sender>
<signal>clicked()</signal>
<receiver>ReportView</receiver>
<slot>generateReport()</slot>
<hints>
<hint type="sourcelabel">
<x>121</x>
<y>432</y>
</hint>
<hint type="destinationlabel">
<x>357</x>
<y>229</y>
</hint>
</hints>
</connection>
<connection>
<sender>save</sender>
<signal>clicked()</signal>
<receiver>ReportView</receiver>
<slot>save()</slot>
<hints>
<hint type="sourcelabel">
<x>357</x>
<y>432</y>
</hint>
<hint type="destinationlabel">
<x>357</x>
<y>229</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>generateReport()</slot>
<slot>save()</slot>
<slot>openBugReportPage()</slot>
</slots>
</ui>

View File

@ -47,6 +47,7 @@
#include "PaletteView.h" #include "PaletteView.h"
#include "PlacementControl.h" #include "PlacementControl.h"
#include "PrinterView.h" #include "PrinterView.h"
#include "ReportView.h"
#include "ROMInfo.h" #include "ROMInfo.h"
#include "SensorView.h" #include "SensorView.h"
#include "SettingsView.h" #include "SettingsView.h"
@ -1219,6 +1220,7 @@ void Window::setupMenu(QMenuBar* menubar) {
m_actions.addSeparator("file"); m_actions.addSeparator("file");
#endif #endif
m_actions.addAction(tr("Report bug..."), "bugReport", openTView<ReportView>(), "file");
m_actions.addAction(tr("About..."), "about", openTView<AboutScreen>(), "file"); m_actions.addAction(tr("About..."), "about", openTView<AboutScreen>(), "file");
#ifndef Q_OS_MAC #ifndef Q_OS_MAC

View File

@ -54,6 +54,8 @@ public:
std::shared_ptr<CoreController> controller() { return m_controller; } std::shared_ptr<CoreController> controller() { return m_controller; }
void setConfig(ConfigController*); void setConfig(ConfigController*);
ConfigController* config() { return m_config; }
void argumentsPassed(mArguments*); void argumentsPassed(mArguments*);
void resizeFrame(const QSize& size); void resizeFrame(const QSize& size);