all repos — mgba @ 7ee3bf2482e94ac56dc9555ee37ce071b032ea28

mGBA Game Boy Advance Emulator

src/platform/qt/Window.cpp (view raw)

   1/* Copyright (c) 2013-2014 Jeffrey Pfau
   2 *
   3 * This Source Code Form is subject to the terms of the Mozilla Public
   4 * License, v. 2.0. If a copy of the MPL was not distributed with this
   5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
   6#include "Window.h"
   7
   8#include <QDesktopWidget>
   9#include <QKeyEvent>
  10#include <QKeySequence>
  11#include <QMenuBar>
  12#include <QMessageBox>
  13#include <QMimeData>
  14#include <QPainter>
  15#include <QStackedLayout>
  16
  17#include "AboutScreen.h"
  18#include "ArchiveInspector.h"
  19#include "CheatsView.h"
  20#include "ConfigController.h"
  21#include "Display.h"
  22#include "GameController.h"
  23#include "GBAApp.h"
  24#include "GDBController.h"
  25#include "GDBWindow.h"
  26#include "GIFView.h"
  27#include "IOViewer.h"
  28#include "LoadSaveState.h"
  29#include "LogView.h"
  30#include "MultiplayerController.h"
  31#include "MemoryView.h"
  32#include "OverrideView.h"
  33#include "PaletteView.h"
  34#include "TileView.h"
  35#include "ROMInfo.h"
  36#include "SensorView.h"
  37#include "SettingsView.h"
  38#include "ShaderSelector.h"
  39#include "ShortcutController.h"
  40#include "VideoView.h"
  41
  42extern "C" {
  43#include "core/version.h"
  44#ifdef M_CORE_GB
  45#include "gb/gb.h"
  46#endif
  47#include "feature/commandline.h"
  48#include "util/nointro.h"
  49#include "util/vfs.h"
  50}
  51
  52using namespace QGBA;
  53
  54Window::Window(ConfigController* config, int playerId, QWidget* parent)
  55	: QMainWindow(parent)
  56	, m_log(0)
  57	, m_logView(new LogView(&m_log))
  58	, m_stateWindow(nullptr)
  59	, m_screenWidget(new WindowBackground())
  60	, m_logo(":/res/mgba-1024.png")
  61	, m_config(config)
  62	, m_inputController(playerId, this)
  63#ifdef USE_FFMPEG
  64	, m_videoView(nullptr)
  65#endif
  66#ifdef USE_MAGICK
  67	, m_gifView(nullptr)
  68#endif
  69#ifdef USE_GDB_STUB
  70	, m_gdbController(nullptr)
  71#endif
  72	, m_mruMenu(nullptr)
  73	, m_shortcutController(new ShortcutController(this))
  74	, m_fullscreenOnStart(false)
  75	, m_autoresume(false)
  76{
  77	setFocusPolicy(Qt::StrongFocus);
  78	setAcceptDrops(true);
  79	setAttribute(Qt::WA_DeleteOnClose);
  80	m_controller = new GameController(this);
  81	m_controller->setInputController(&m_inputController);
  82	updateTitle();
  83
  84	m_display = Display::create(this);
  85	m_shaderView = new ShaderSelector(m_display, m_config);
  86
  87	m_logo.setDevicePixelRatio(m_screenWidget->devicePixelRatio());
  88	m_logo = m_logo; // Free memory left over in old pixmap
  89
  90	m_screenWidget->setMinimumSize(m_display->minimumSize());
  91	m_screenWidget->setSizePolicy(m_display->sizePolicy());
  92#ifdef M_CORE_GBA
  93	m_screenWidget->setSizeHint(QSize(VIDEO_HORIZONTAL_PIXELS * 2, VIDEO_VERTICAL_PIXELS * 2));
  94#endif
  95	m_screenWidget->setPixmap(m_logo);
  96	m_screenWidget->setLockAspectRatio(m_logo.width(), m_logo.height());
  97	setCentralWidget(m_screenWidget);
  98
  99	connect(m_controller, SIGNAL(gameStarted(mCoreThread*, const QString&)), this, SLOT(gameStarted(mCoreThread*, const QString&)));
 100	connect(m_controller, SIGNAL(gameStarted(mCoreThread*, const QString&)), &m_inputController, SLOT(suspendScreensaver()));
 101	connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_display, SLOT(stopDrawing()));
 102	connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), this, SLOT(gameStopped()));
 103	connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), &m_inputController, SLOT(resumeScreensaver()));
 104	connect(m_controller, SIGNAL(stateLoaded(mCoreThread*)), m_display, SLOT(forceDraw()));
 105	connect(m_controller, SIGNAL(rewound(mCoreThread*)), m_display, SLOT(forceDraw()));
 106	connect(m_controller, &GameController::gamePaused, [this](mCoreThread* context) {
 107		unsigned width, height;
 108		context->core->desiredVideoDimensions(context->core, &width, &height);
 109		QImage currentImage(reinterpret_cast<const uchar*>(m_controller->drawContext()), width, height,
 110		                    width * BYTES_PER_PIXEL, QImage::Format_RGBX8888);
 111		QPixmap pixmap;
 112		pixmap.convertFromImage(currentImage);
 113		m_screenWidget->setPixmap(pixmap);
 114		m_screenWidget->setLockAspectRatio(width, height);
 115	});
 116	connect(m_controller, SIGNAL(gamePaused(mCoreThread*)), m_display, SLOT(pauseDrawing()));
 117#ifndef Q_OS_MAC
 118	connect(m_controller, SIGNAL(gamePaused(mCoreThread*)), menuBar(), SLOT(show()));
 119	connect(m_controller, &GameController::gameUnpaused, [this]() {
 120		if(isFullScreen()) {
 121			menuBar()->hide();
 122		}
 123	});
 124#endif
 125	connect(m_controller, SIGNAL(gamePaused(mCoreThread*)), &m_inputController, SLOT(resumeScreensaver()));
 126	connect(m_controller, SIGNAL(gameUnpaused(mCoreThread*)), m_display, SLOT(unpauseDrawing()));
 127	connect(m_controller, SIGNAL(gameUnpaused(mCoreThread*)), &m_inputController, SLOT(suspendScreensaver()));
 128	connect(m_controller, SIGNAL(postLog(int, int, const QString&)), &m_log, SLOT(postLog(int, int, const QString&)));
 129	connect(m_controller, SIGNAL(frameAvailable(const uint32_t*)), this, SLOT(recordFrame()));
 130	connect(m_controller, SIGNAL(frameAvailable(const uint32_t*)), m_display, SLOT(framePosted(const uint32_t*)));
 131	connect(m_controller, SIGNAL(gameCrashed(const QString&)), this, SLOT(gameCrashed(const QString&)));
 132	connect(m_controller, SIGNAL(gameFailed()), this, SLOT(gameFailed()));
 133	connect(m_controller, SIGNAL(unimplementedBiosCall(int)), this, SLOT(unimplementedBiosCall(int)));
 134	connect(m_controller, SIGNAL(statusPosted(const QString&)), m_display, SLOT(showMessage(const QString&)));
 135	connect(&m_log, SIGNAL(levelsSet(int)), m_controller, SLOT(setLogLevel(int)));
 136	connect(&m_log, SIGNAL(levelsEnabled(int)), m_controller, SLOT(enableLogLevel(int)));
 137	connect(&m_log, SIGNAL(levelsDisabled(int)), m_controller, SLOT(disableLogLevel(int)));
 138	connect(this, SIGNAL(startDrawing(mCoreThread*)), m_display, SLOT(startDrawing(mCoreThread*)), Qt::QueuedConnection);
 139	connect(this, SIGNAL(shutdown()), m_display, SLOT(stopDrawing()));
 140	connect(this, SIGNAL(shutdown()), m_controller, SLOT(closeGame()));
 141	connect(this, SIGNAL(shutdown()), m_logView, SLOT(hide()));
 142	connect(this, SIGNAL(shutdown()), m_shaderView, SLOT(hide()));
 143	connect(this, SIGNAL(audioBufferSamplesChanged(int)), m_controller, SLOT(setAudioBufferSamples(int)));
 144	connect(this, SIGNAL(sampleRateChanged(unsigned)), m_controller, SLOT(setAudioSampleRate(unsigned)));
 145	connect(this, SIGNAL(fpsTargetChanged(float)), m_controller, SLOT(setFPSTarget(float)));
 146	connect(&m_fpsTimer, SIGNAL(timeout()), this, SLOT(showFPS()));
 147	connect(&m_focusCheck, SIGNAL(timeout()), this, SLOT(focusCheck()));
 148	connect(m_display, &Display::hideCursor, [this]() {
 149		if (static_cast<QStackedLayout*>(m_screenWidget->layout())->currentWidget() == m_display) {
 150			m_screenWidget->setCursor(Qt::BlankCursor);
 151		}
 152	});
 153	connect(m_display, &Display::showCursor, [this]() {
 154		m_screenWidget->unsetCursor();
 155	});
 156	connect(&m_inputController, SIGNAL(profileLoaded(const QString&)), m_shortcutController, SLOT(loadProfile(const QString&)));
 157
 158	m_log.setLevels(mLOG_WARN | mLOG_ERROR | mLOG_FATAL);
 159	m_fpsTimer.setInterval(FPS_TIMER_INTERVAL);
 160	m_focusCheck.setInterval(200);
 161
 162	m_shortcutController->setConfigController(m_config);
 163	setupMenu(menuBar());
 164}
 165
 166Window::~Window() {
 167	delete m_logView;
 168
 169#ifdef USE_FFMPEG
 170	delete m_videoView;
 171#endif
 172
 173#ifdef USE_MAGICK
 174	delete m_gifView;
 175#endif
 176}
 177
 178void Window::argumentsPassed(mArguments* args) {
 179	loadConfig();
 180
 181	if (args->patch) {
 182		m_controller->loadPatch(args->patch);
 183	}
 184
 185	if (args->fname) {
 186		m_controller->loadGame(args->fname);
 187	}
 188
 189#ifdef USE_GDB_STUB
 190	if (args->debuggerType == DEBUGGER_GDB) {
 191		if (!m_gdbController) {
 192			m_gdbController = new GDBController(m_controller, this);
 193			m_gdbController->listen();
 194		}
 195	}
 196#endif
 197}
 198
 199void Window::resizeFrame(const QSize& size) {
 200	QSize newSize(size);
 201	m_screenWidget->setSizeHint(newSize);
 202	newSize -= m_screenWidget->size();
 203	newSize += this->size();
 204	resize(newSize);
 205}
 206
 207void Window::setConfig(ConfigController* config) {
 208	m_config = config;
 209}
 210
 211void Window::loadConfig() {
 212	const mCoreOptions* opts = m_config->options();
 213	reloadConfig();
 214
 215	// TODO: Move these to ConfigController
 216	if (opts->fpsTarget) {
 217		emit fpsTargetChanged(opts->fpsTarget);
 218	}
 219
 220	if (opts->audioBuffers) {
 221		emit audioBufferSamplesChanged(opts->audioBuffers);
 222	}
 223
 224	if (opts->sampleRate) {
 225		emit sampleRateChanged(opts->sampleRate);
 226	}
 227
 228	if (opts->width && opts->height) {
 229		resizeFrame(QSize(opts->width, opts->height));
 230	}
 231
 232	if (opts->fullscreen) {
 233		enterFullScreen();
 234	}
 235
 236	if (opts->shader) {
 237		struct VDir* shader = VDirOpen(opts->shader);
 238		if (shader) {
 239			m_display->setShaders(shader);
 240			m_shaderView->refreshShaders();
 241			shader->close(shader);
 242		}
 243	}
 244
 245	m_mruFiles = m_config->getMRU();
 246	updateMRU();
 247
 248	m_inputController.setConfiguration(m_config);
 249	m_controller->setUseBIOS(opts->useBios);
 250}
 251
 252void Window::reloadConfig() {
 253	const mCoreOptions* opts = m_config->options();
 254
 255	m_log.setLevels(opts->logLevel);
 256
 257	QString saveStateExtdata = m_config->getOption("saveStateExtdata");
 258	bool ok;
 259	int flags = saveStateExtdata.toInt(&ok);
 260	if (ok) {
 261		m_controller->setSaveStateExtdata(flags);
 262	}
 263
 264	QString loadStateExtdata = m_config->getOption("loadStateExtdata");
 265	flags = loadStateExtdata.toInt(&ok);
 266	if (ok) {
 267		m_controller->setLoadStateExtdata(flags);
 268	}
 269
 270	m_controller->setConfig(m_config->config());
 271	m_display->lockAspectRatio(opts->lockAspectRatio);
 272	m_display->filter(opts->resampleVideo);
 273
 274	if (opts->bios) {
 275		m_controller->loadBIOS(opts->bios);
 276	}
 277
 278	m_inputController.setScreensaverSuspendable(opts->suspendScreensaver);
 279}
 280
 281void Window::saveConfig() {
 282	m_inputController.saveConfiguration();
 283	m_config->write();
 284}
 285
 286QString Window::getFilters() const {
 287	QStringList filters;
 288	QStringList formats;
 289
 290#ifdef M_CORE_GBA
 291	QStringList gbaFormats{
 292		"*.gba",
 293#if defined(USE_LIBZIP) || defined(USE_ZLIB)
 294		"*.zip",
 295#endif
 296#ifdef USE_LZMA
 297		"*.7z",
 298#endif
 299		"*.agb",
 300		"*.mb",
 301		"*.rom",
 302		"*.bin"};
 303	formats.append(gbaFormats);
 304	filters.append(tr("Game Boy Advance ROMs (%1)").arg(gbaFormats.join(QChar(' '))));
 305#endif
 306
 307#ifdef M_CORE_GB
 308	QStringList gbFormats{
 309		"*.gb",
 310		"*.gbc",
 311#if defined(USE_LIBZIP) || defined(USE_ZLIB)
 312		"*.zip",
 313#endif
 314#ifdef USE_LZMA
 315		"*.7z",
 316#endif
 317		"*.rom",
 318		"*.bin"};
 319	formats.append(gbFormats);
 320	filters.append(tr("Game Boy ROMs (%1)").arg(gbFormats.join(QChar(' '))));
 321#endif
 322
 323	formats.removeDuplicates();
 324	filters.prepend(tr("All ROMs (%1)").arg(formats.join(QChar(' '))));
 325	return filters.join(";;");
 326}
 327
 328QString Window::getFiltersArchive() const {
 329	QStringList filters;
 330
 331	QStringList formats{
 332#if defined(USE_LIBZIP) || defined(USE_ZLIB)
 333		"*.zip",
 334#endif
 335#ifdef USE_LZMA
 336		"*.7z",
 337#endif
 338	};
 339	filters.append(tr("Archives (%1)").arg(formats.join(QChar(' '))));
 340	return filters.join(";;");
 341}
 342
 343void Window::selectROM() {
 344	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), getFilters());
 345	if (!filename.isEmpty()) {
 346		m_controller->loadGame(filename);
 347	}
 348}
 349
 350void Window::selectROMInArchive() {
 351	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), getFiltersArchive());
 352	if (filename.isEmpty()) {
 353		return;
 354	}
 355	ArchiveInspector* archiveInspector = new ArchiveInspector(filename);
 356	connect(archiveInspector, &QDialog::accepted, [this,  archiveInspector, filename]() {
 357		VFile* output = archiveInspector->selectedVFile();
 358		if (output) {
 359			m_controller->loadGame(output, filename);
 360		}
 361		archiveInspector->close();
 362	});
 363	archiveInspector->setAttribute(Qt::WA_DeleteOnClose);
 364	archiveInspector->show();
 365}
 366
 367void Window::replaceROM() {
 368	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select ROM"), getFilters());
 369	if (!filename.isEmpty()) {
 370		m_controller->replaceGame(filename);
 371	}
 372}
 373
 374void Window::selectSave(bool temporary) {
 375	QStringList formats{"*.sav"};
 376	QString filter = tr("Game Boy Advance save files (%1)").arg(formats.join(QChar(' ')));
 377	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save"), filter);
 378	if (!filename.isEmpty()) {
 379		m_controller->loadSave(filename, temporary);
 380	}
 381}
 382
 383void Window::multiplayerChanged() {
 384	int attached = 1;
 385	MultiplayerController* multiplayer = m_controller->multiplayerController();
 386	if (multiplayer) {
 387		attached = multiplayer->attached();
 388	}
 389	if (m_controller->isLoaded()) {
 390		for (QAction* action : m_nonMpActions) {
 391			action->setDisabled(attached > 1);
 392		}
 393	}
 394}
 395
 396void Window::selectBIOS() {
 397	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select BIOS"));
 398	if (!filename.isEmpty()) {
 399		QFileInfo info(filename);
 400		m_config->setOption("bios", info.canonicalFilePath());
 401		m_config->updateOption("bios");
 402		m_config->setOption("useBios", true);
 403		m_config->updateOption("useBios");
 404		m_controller->loadBIOS(info.canonicalFilePath());
 405	}
 406}
 407
 408void Window::selectPatch() {
 409	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select patch"), tr("Patches (*.ips *.ups *.bps)"));
 410	if (!filename.isEmpty()) {
 411		m_controller->loadPatch(filename);
 412	}
 413}
 414
 415void Window::openView(QWidget* widget) {
 416	connect(this, SIGNAL(shutdown()), widget, SLOT(close()));
 417	widget->setAttribute(Qt::WA_DeleteOnClose);
 418	widget->show();
 419}
 420
 421void Window::importSharkport() {
 422	QString filename = GBAApp::app()->getOpenFileName(this, tr("Select save"), tr("GameShark saves (*.sps *.xps)"));
 423	if (!filename.isEmpty()) {
 424		m_controller->importSharkport(filename);
 425	}
 426}
 427
 428void Window::exportSharkport() {
 429	QString filename = GBAApp::app()->getSaveFileName(this, tr("Select save"), tr("GameShark saves (*.sps *.xps)"));
 430	if (!filename.isEmpty()) {
 431		m_controller->exportSharkport(filename);
 432	}
 433}
 434
 435void Window::openSettingsWindow() {
 436	SettingsView* settingsWindow = new SettingsView(m_config, &m_inputController, m_shortcutController);
 437	connect(settingsWindow, SIGNAL(biosLoaded(const QString&)), m_controller, SLOT(loadBIOS(const QString&)));
 438	connect(settingsWindow, SIGNAL(audioDriverChanged()), m_controller, SLOT(reloadAudioDriver()));
 439	connect(settingsWindow, SIGNAL(displayDriverChanged()), this, SLOT(mustRestart()));
 440	connect(settingsWindow, SIGNAL(pathsChanged()), this, SLOT(reloadConfig()));
 441	openView(settingsWindow);
 442}
 443
 444void Window::openOverrideWindow() {
 445	OverrideView* overrideWindow = new OverrideView(m_controller, m_config);
 446	openView(overrideWindow);
 447}
 448
 449void Window::openSensorWindow() {
 450	SensorView* sensorWindow = new SensorView(m_controller, &m_inputController);
 451	openView(sensorWindow);
 452}
 453
 454void Window::openCheatsWindow() {
 455	CheatsView* cheatsWindow = new CheatsView(m_controller);
 456	openView(cheatsWindow);
 457}
 458
 459void Window::openPaletteWindow() {
 460	PaletteView* paletteWindow = new PaletteView(m_controller);
 461	openView(paletteWindow);
 462}
 463
 464void Window::openTileWindow() {
 465	TileView* tileWindow = new TileView(m_controller);
 466	openView(tileWindow);
 467}
 468
 469void Window::openMemoryWindow() {
 470	MemoryView* memoryWindow = new MemoryView(m_controller);
 471	openView(memoryWindow);
 472}
 473
 474void Window::openIOViewer() {
 475	IOViewer* ioViewer = new IOViewer(m_controller);
 476	openView(ioViewer);
 477}
 478
 479void Window::openAboutScreen() {
 480	AboutScreen* about = new AboutScreen();
 481	openView(about);
 482}
 483
 484void Window::openROMInfo() {
 485	ROMInfo* romInfo = new ROMInfo(m_controller);
 486	openView(romInfo);
 487}
 488
 489#ifdef USE_FFMPEG
 490void Window::openVideoWindow() {
 491	if (!m_videoView) {
 492		m_videoView = new VideoView();
 493		connect(m_videoView, SIGNAL(recordingStarted(mAVStream*)), m_controller, SLOT(setAVStream(mAVStream*)));
 494		connect(m_videoView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
 495		connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_videoView, SLOT(stopRecording()));
 496		connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_videoView, SLOT(close()));
 497		connect(m_controller, &GameController::gameStarted, [this]() {
 498			m_videoView->setNativeResolution(m_controller->screenDimensions());
 499		});
 500		if (m_controller->isLoaded()) {
 501			m_videoView->setNativeResolution(m_controller->screenDimensions());
 502		}
 503		connect(this, SIGNAL(shutdown()), m_videoView, SLOT(close()));
 504	}
 505	m_videoView->show();
 506}
 507#endif
 508
 509#ifdef USE_MAGICK
 510void Window::openGIFWindow() {
 511	if (!m_gifView) {
 512		m_gifView = new GIFView();
 513		connect(m_gifView, SIGNAL(recordingStarted(mAVStream*)), m_controller, SLOT(setAVStream(mAVStream*)));
 514		connect(m_gifView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
 515		connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_gifView, SLOT(stopRecording()));
 516		connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_gifView, SLOT(close()));
 517		connect(this, SIGNAL(shutdown()), m_gifView, SLOT(close()));
 518	}
 519	m_gifView->show();
 520}
 521#endif
 522
 523#ifdef USE_GDB_STUB
 524void Window::gdbOpen() {
 525	if (!m_gdbController) {
 526		m_gdbController = new GDBController(m_controller, this);
 527	}
 528	GDBWindow* window = new GDBWindow(m_gdbController);
 529	openView(window);
 530}
 531#endif
 532
 533void Window::keyPressEvent(QKeyEvent* event) {
 534	if (event->isAutoRepeat()) {
 535		QWidget::keyPressEvent(event);
 536		return;
 537	}
 538	GBAKey key = m_inputController.mapKeyboard(event->key());
 539	if (key == GBA_KEY_NONE) {
 540		QWidget::keyPressEvent(event);
 541		return;
 542	}
 543	m_controller->keyPressed(key);
 544	event->accept();
 545}
 546
 547void Window::keyReleaseEvent(QKeyEvent* event) {
 548	if (event->isAutoRepeat()) {
 549		QWidget::keyReleaseEvent(event);
 550		return;
 551	}
 552	GBAKey key = m_inputController.mapKeyboard(event->key());
 553	if (key == GBA_KEY_NONE) {
 554		QWidget::keyPressEvent(event);
 555		return;
 556	}
 557	m_controller->keyReleased(key);
 558	event->accept();
 559}
 560
 561void Window::resizeEvent(QResizeEvent* event) {
 562	if (!isFullScreen()) {
 563		m_config->setOption("height", m_screenWidget->height());
 564		m_config->setOption("width", m_screenWidget->width());
 565	}
 566
 567	int factor = 0;
 568	QSize size(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
 569	if (m_controller->isLoaded()) {
 570		size = m_controller->screenDimensions();
 571	}
 572	if (event->size().width() % size.width() == 0 && event->size().height() % size.height() == 0 &&
 573	    event->size().width() / size.width() == event->size().height() / size.height()) {
 574		factor = event->size().width() / size.width();
 575	}
 576	for (QMap<int, QAction*>::iterator iter = m_frameSizes.begin(); iter != m_frameSizes.end(); ++iter) {
 577		bool enableSignals = iter.value()->blockSignals(true);
 578		if (iter.key() == factor) {
 579			iter.value()->setChecked(true);
 580		} else {
 581			iter.value()->setChecked(false);
 582		}
 583		iter.value()->blockSignals(enableSignals);
 584	}
 585
 586	m_config->setOption("fullscreen", isFullScreen());
 587}
 588
 589void Window::showEvent(QShowEvent* event) {
 590	resizeFrame(m_screenWidget->sizeHint());
 591	QVariant windowPos = m_config->getQtOption("windowPos");
 592	if (!windowPos.isNull()) {
 593		move(windowPos.toPoint());
 594	} else {
 595		QRect rect = frameGeometry();
 596		rect.moveCenter(QApplication::desktop()->availableGeometry().center());
 597		move(rect.topLeft());
 598	}
 599	if (m_fullscreenOnStart) {
 600		enterFullScreen();
 601		m_fullscreenOnStart = false;
 602	}
 603}
 604
 605void Window::closeEvent(QCloseEvent* event) {
 606	emit shutdown();
 607	m_config->setQtOption("windowPos", pos());
 608	saveConfig();
 609	QMainWindow::closeEvent(event);
 610}
 611
 612void Window::focusInEvent(QFocusEvent*) {
 613	m_display->forceDraw();
 614}
 615
 616void Window::focusOutEvent(QFocusEvent*) {
 617	m_controller->setTurbo(false, false);
 618	m_controller->stopRewinding();
 619	m_controller->clearKeys();
 620}
 621
 622void Window::dragEnterEvent(QDragEnterEvent* event) {
 623	if (event->mimeData()->hasFormat("text/uri-list")) {
 624		event->acceptProposedAction();
 625	}
 626}
 627
 628void Window::dropEvent(QDropEvent* event) {
 629	QString uris = event->mimeData()->data("text/uri-list");
 630	uris = uris.trimmed();
 631	if (uris.contains("\n")) {
 632		// Only one file please
 633		return;
 634	}
 635	QUrl url(uris);
 636	if (!url.isLocalFile()) {
 637		// No remote loading
 638		return;
 639	}
 640	event->accept();
 641	m_controller->loadGame(url.toLocalFile());
 642}
 643
 644void Window::mouseDoubleClickEvent(QMouseEvent* event) {
 645	if (event->button() != Qt::LeftButton) {
 646		return;
 647	}
 648	toggleFullScreen();
 649}
 650
 651void Window::enterFullScreen() {
 652	if (!isVisible()) {
 653		m_fullscreenOnStart = true;
 654		return;
 655	}
 656	if (isFullScreen()) {
 657		return;
 658	}
 659	showFullScreen();
 660#ifndef Q_OS_MAC
 661	if (m_controller->isLoaded() && !m_controller->isPaused()) {
 662		menuBar()->hide();
 663	}
 664#endif
 665}
 666
 667void Window::exitFullScreen() {
 668	if (!isFullScreen()) {
 669		return;
 670	}
 671	m_screenWidget->unsetCursor();
 672	menuBar()->show();
 673	showNormal();
 674}
 675
 676void Window::toggleFullScreen() {
 677	if (isFullScreen()) {
 678		exitFullScreen();
 679	} else {
 680		enterFullScreen();
 681	}
 682}
 683
 684void Window::gameStarted(mCoreThread* context, const QString& fname) {
 685	MutexLock(&context->stateMutex);
 686	if (context->state < THREAD_EXITING) {
 687		emit startDrawing(context);
 688	} else {
 689		MutexUnlock(&context->stateMutex);
 690		return;
 691	}
 692	MutexUnlock(&context->stateMutex);
 693	foreach (QAction* action, m_gameActions) {
 694		action->setDisabled(false);
 695	}
 696#ifdef M_CORE_GBA
 697	foreach (QAction* action, m_gbaActions) {
 698		action->setDisabled(context->core->platform(context->core) != PLATFORM_GBA);
 699	}
 700#endif
 701	multiplayerChanged();
 702	if (!fname.isEmpty()) {
 703		setWindowFilePath(fname);
 704		appendMRU(fname);
 705	}
 706	updateTitle();
 707	unsigned width, height;
 708	context->core->desiredVideoDimensions(context->core, &width, &height);
 709	m_display->setMinimumSize(width, height);
 710	m_screenWidget->setMinimumSize(m_display->minimumSize());
 711	attachWidget(m_display);
 712
 713#ifndef Q_OS_MAC
 714	if (isFullScreen()) {
 715		menuBar()->hide();
 716	}
 717#endif
 718
 719	m_hitUnimplementedBiosCall = false;
 720	m_fpsTimer.start();
 721	m_focusCheck.start();
 722}
 723
 724void Window::gameStopped() {
 725#ifdef M_CORE_GBA
 726	foreach (QAction* action, m_gbaActions) {
 727		action->setDisabled(false);
 728	}
 729#endif
 730	foreach (QAction* action, m_gameActions) {
 731		action->setDisabled(true);
 732	}
 733	setWindowFilePath(QString());
 734	updateTitle();
 735	detachWidget(m_display);
 736	m_screenWidget->setLockAspectRatio(m_logo.width(), m_logo.height());
 737	m_screenWidget->setPixmap(m_logo);
 738	m_screenWidget->unsetCursor();
 739#ifdef M_CORE_GB
 740	m_display->setMinimumSize(GB_VIDEO_HORIZONTAL_PIXELS, GB_VIDEO_VERTICAL_PIXELS);
 741#elif defined(M_CORE_GBA)
 742	m_display->setMinimumSize(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
 743#endif
 744	m_screenWidget->setMinimumSize(m_display->minimumSize());
 745
 746	m_fpsTimer.stop();
 747	m_focusCheck.stop();
 748}
 749
 750void Window::gameCrashed(const QString& errorMessage) {
 751	QMessageBox* crash = new QMessageBox(QMessageBox::Critical, tr("Crash"),
 752	                                     tr("The game has crashed with the following error:\n\n%1").arg(errorMessage),
 753	                                     QMessageBox::Ok, this, Qt::Sheet);
 754	crash->setAttribute(Qt::WA_DeleteOnClose);
 755	crash->show();
 756}
 757
 758void Window::gameFailed() {
 759	QMessageBox* fail = new QMessageBox(QMessageBox::Warning, tr("Couldn't Load"),
 760	                                    tr("Could not load game. Are you sure it's in the correct format?"),
 761	                                    QMessageBox::Ok, this, Qt::Sheet);
 762	fail->setAttribute(Qt::WA_DeleteOnClose);
 763	fail->show();
 764}
 765
 766void Window::unimplementedBiosCall(int call) {
 767	if (m_hitUnimplementedBiosCall) {
 768		return;
 769	}
 770	m_hitUnimplementedBiosCall = true;
 771
 772	QMessageBox* fail = new QMessageBox(
 773	    QMessageBox::Warning, tr("Unimplemented BIOS call"),
 774	    tr("This game uses a BIOS call that is not implemented. Please use the official BIOS for best experience."),
 775	    QMessageBox::Ok, this, Qt::Sheet);
 776	fail->setAttribute(Qt::WA_DeleteOnClose);
 777	fail->show();
 778}
 779
 780void Window::tryMakePortable() {
 781	QMessageBox* confirm = new QMessageBox(QMessageBox::Question, tr("Really make portable?"),
 782	                                       tr("This will make the emulator load its configuration from the same directory as the executable. Do you want to continue?"),
 783	                                       QMessageBox::Yes | QMessageBox::Cancel, this, Qt::Sheet);
 784	confirm->setAttribute(Qt::WA_DeleteOnClose);
 785	connect(confirm->button(QMessageBox::Yes), SIGNAL(clicked()), m_config, SLOT(makePortable()));
 786	confirm->show();
 787}
 788
 789void Window::mustRestart() {
 790	QMessageBox* dialog = new QMessageBox(QMessageBox::Warning, tr("Restart needed"),
 791	                                      tr("Some changes will not take effect until the emulator is restarted."),
 792	                                      QMessageBox::Ok, this, Qt::Sheet);
 793	dialog->setAttribute(Qt::WA_DeleteOnClose);
 794	dialog->show();
 795}
 796
 797void Window::recordFrame() {
 798	m_frameList.append(QDateTime::currentDateTime());
 799	while (m_frameList.count() > FRAME_LIST_SIZE) {
 800		m_frameList.removeFirst();
 801	}
 802}
 803
 804void Window::showFPS() {
 805	if (m_frameList.isEmpty()) {
 806		updateTitle();
 807		return;
 808	}
 809	qint64 interval = m_frameList.first().msecsTo(m_frameList.last());
 810	float fps = (m_frameList.count() - 1) * 10000.f / interval;
 811	fps = round(fps) / 10.f;
 812	updateTitle(fps);
 813}
 814
 815void Window::updateTitle(float fps) {
 816	QString title;
 817
 818	m_controller->threadInterrupt();
 819	if (m_controller->isLoaded()) {
 820		const NoIntroDB* db = GBAApp::app()->gameDB();
 821		NoIntroGame game{};
 822		uint32_t crc32 = 0;
 823
 824		switch (m_controller->thread()->core->platform(m_controller->thread()->core)) {
 825	#ifdef M_CORE_GBA
 826		case PLATFORM_GBA: {
 827			GBA* gba = static_cast<GBA*>(m_controller->thread()->core->board);
 828			crc32 = gba->romCrc32;
 829			break;
 830		}
 831	#endif
 832	#ifdef M_CORE_GB
 833		case PLATFORM_GB: {
 834			GB* gb = static_cast<GB*>(m_controller->thread()->core->board);
 835			crc32 = gb->romCrc32;
 836			break;
 837		}
 838	#endif
 839		default:
 840			break;
 841		}
 842
 843		if (db && crc32) {
 844			NoIntroDBLookupGameByCRC(db, crc32, &game);
 845			title = QLatin1String(game.name);
 846		} else {
 847			char gameTitle[17] = { '\0' };
 848			mCore* core = m_controller->thread()->core;
 849			core->getGameTitle(core, gameTitle);
 850			title = gameTitle;
 851		}
 852	}
 853	MultiplayerController* multiplayer = m_controller->multiplayerController();
 854	if (multiplayer && multiplayer->attached() > 1) {
 855		title += tr(" -  Player %1 of %2").arg(multiplayer->playerId(m_controller) + 1).arg(multiplayer->attached());
 856		for (QAction* action : m_nonMpActions) {
 857			action->setDisabled(true);
 858		}
 859	} else if (m_controller->isLoaded()) {
 860		for (QAction* action : m_nonMpActions) {
 861			action->setDisabled(false);
 862		}
 863	}
 864	m_controller->threadContinue();
 865	if (title.isNull()) {
 866		setWindowTitle(tr("%1 - %2").arg(projectName).arg(projectVersion));
 867	} else if (fps < 0) {
 868		setWindowTitle(tr("%1 - %2 - %3").arg(projectName).arg(title).arg(projectVersion));
 869	} else {
 870		setWindowTitle(tr("%1 - %2 (%3 fps) - %4").arg(projectName).arg(title).arg(fps).arg(projectVersion));
 871	}
 872}
 873
 874void Window::openStateWindow(LoadSave ls) {
 875	if (m_stateWindow) {
 876		return;
 877	}
 878	MultiplayerController* multiplayer = m_controller->multiplayerController();
 879	if (multiplayer && multiplayer->attached() > 1) {
 880		return;
 881	}
 882	bool wasPaused = m_controller->isPaused();
 883	m_stateWindow = new LoadSaveState(m_controller);
 884	connect(this, SIGNAL(shutdown()), m_stateWindow, SLOT(close()));
 885	connect(m_controller, SIGNAL(gameStopped(mCoreThread*)), m_stateWindow, SLOT(close()));
 886	connect(m_stateWindow, &LoadSaveState::closed, [this]() {
 887		detachWidget(m_stateWindow);
 888		m_stateWindow = nullptr;
 889		QMetaObject::invokeMethod(this, "setFocus", Qt::QueuedConnection);
 890	});
 891	if (!wasPaused) {
 892		m_controller->setPaused(true);
 893		connect(m_stateWindow, &LoadSaveState::closed, [this]() { m_controller->setPaused(false); });
 894	}
 895	m_stateWindow->setAttribute(Qt::WA_DeleteOnClose);
 896	m_stateWindow->setMode(ls);
 897	attachWidget(m_stateWindow);
 898}
 899
 900void Window::setupMenu(QMenuBar* menubar) {
 901	menubar->clear();
 902	QMenu* fileMenu = menubar->addMenu(tr("&File"));
 903	m_shortcutController->addMenu(fileMenu);
 904	installEventFilter(m_shortcutController);
 905	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &ROM..."), this, SLOT(selectROM()), QKeySequence::Open),
 906	                    "loadROM");
 907	addControlledAction(fileMenu, fileMenu->addAction(tr("Load ROM in archive..."), this, SLOT(selectROMInArchive())),
 908	                    "loadROMInArchive");
 909
 910	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &BIOS..."), this, SLOT(selectBIOS())), "loadBIOS");
 911
 912	QAction* loadTemporarySave = new QAction(tr("Load temporary save..."), fileMenu);
 913	connect(loadTemporarySave, &QAction::triggered, [this]() { this->selectSave(true); });
 914	m_gameActions.append(loadTemporarySave);
 915	addControlledAction(fileMenu, loadTemporarySave, "loadTemporarySave");
 916
 917	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &patch..."), this, SLOT(selectPatch())), "loadPatch");
 918	addControlledAction(fileMenu, fileMenu->addAction(tr("Boot BIOS"), m_controller, SLOT(bootBIOS())), "bootBIOS");
 919
 920	addControlledAction(fileMenu, fileMenu->addAction(tr("Replace ROM..."), this, SLOT(replaceROM())), "replaceROM");
 921
 922	QAction* romInfo = new QAction(tr("ROM &info..."), fileMenu);
 923	connect(romInfo, SIGNAL(triggered()), this, SLOT(openROMInfo()));
 924	m_gameActions.append(romInfo);
 925	addControlledAction(fileMenu, romInfo, "romInfo");
 926
 927	m_mruMenu = fileMenu->addMenu(tr("Recent"));
 928
 929	fileMenu->addSeparator();
 930
 931	addControlledAction(fileMenu, fileMenu->addAction(tr("Make portable"), this, SLOT(tryMakePortable())), "makePortable");
 932
 933	fileMenu->addSeparator();
 934
 935	QAction* loadState = new QAction(tr("&Load state"), fileMenu);
 936	loadState->setShortcut(tr("F10"));
 937	connect(loadState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::LOAD); });
 938	m_gameActions.append(loadState);
 939	m_nonMpActions.append(loadState);
 940	addControlledAction(fileMenu, loadState, "loadState");
 941
 942	QAction* saveState = new QAction(tr("&Save state"), fileMenu);
 943	saveState->setShortcut(tr("Shift+F10"));
 944	connect(saveState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::SAVE); });
 945	m_gameActions.append(saveState);
 946	m_nonMpActions.append(saveState);
 947	addControlledAction(fileMenu, saveState, "saveState");
 948
 949	QMenu* quickLoadMenu = fileMenu->addMenu(tr("Quick load"));
 950	QMenu* quickSaveMenu = fileMenu->addMenu(tr("Quick save"));
 951	m_shortcutController->addMenu(quickLoadMenu);
 952	m_shortcutController->addMenu(quickSaveMenu);
 953
 954	QAction* quickLoad = new QAction(tr("Load recent"), quickLoadMenu);
 955	connect(quickLoad, SIGNAL(triggered()), m_controller, SLOT(loadState()));
 956	m_gameActions.append(quickLoad);
 957	m_nonMpActions.append(quickLoad);
 958	addControlledAction(quickLoadMenu, quickLoad, "quickLoad");
 959
 960	QAction* quickSave = new QAction(tr("Save recent"), quickSaveMenu);
 961	connect(quickSave, SIGNAL(triggered()), m_controller, SLOT(saveState()));
 962	m_gameActions.append(quickSave);
 963	m_nonMpActions.append(quickSave);
 964	addControlledAction(quickSaveMenu, quickSave, "quickSave");
 965
 966	quickLoadMenu->addSeparator();
 967	quickSaveMenu->addSeparator();
 968
 969	QAction* undoLoadState = new QAction(tr("Undo load state"), quickLoadMenu);
 970	undoLoadState->setShortcut(tr("F11"));
 971	connect(undoLoadState, SIGNAL(triggered()), m_controller, SLOT(loadBackupState()));
 972	m_gameActions.append(undoLoadState);
 973	m_nonMpActions.append(undoLoadState);
 974	addControlledAction(quickLoadMenu, undoLoadState, "undoLoadState");
 975
 976	QAction* undoSaveState = new QAction(tr("Undo save state"), quickSaveMenu);
 977	undoSaveState->setShortcut(tr("Shift+F11"));
 978	connect(undoSaveState, SIGNAL(triggered()), m_controller, SLOT(saveBackupState()));
 979	m_gameActions.append(undoSaveState);
 980	m_nonMpActions.append(undoSaveState);
 981	addControlledAction(quickSaveMenu, undoSaveState, "undoSaveState");
 982
 983	quickLoadMenu->addSeparator();
 984	quickSaveMenu->addSeparator();
 985
 986	int i;
 987	for (i = 1; i < 10; ++i) {
 988		quickLoad = new QAction(tr("State &%1").arg(i), quickLoadMenu);
 989		quickLoad->setShortcut(tr("F%1").arg(i));
 990		connect(quickLoad, &QAction::triggered, [this, i]() { m_controller->loadState(i); });
 991		m_gameActions.append(quickLoad);
 992		m_nonMpActions.append(quickLoad);
 993		addControlledAction(quickLoadMenu, quickLoad, QString("quickLoad.%1").arg(i));
 994
 995		quickSave = new QAction(tr("State &%1").arg(i), quickSaveMenu);
 996		quickSave->setShortcut(tr("Shift+F%1").arg(i));
 997		connect(quickSave, &QAction::triggered, [this, i]() { m_controller->saveState(i); });
 998		m_gameActions.append(quickSave);
 999		m_nonMpActions.append(quickSave);
1000		addControlledAction(quickSaveMenu, quickSave, QString("quickSave.%1").arg(i));
1001	}
1002
1003#ifdef M_CORE_GBA
1004	fileMenu->addSeparator();
1005	QAction* importShark = new QAction(tr("Import GameShark Save"), fileMenu);
1006	connect(importShark, SIGNAL(triggered()), this, SLOT(importSharkport()));
1007	m_gameActions.append(importShark);
1008	m_gbaActions.append(importShark);
1009	addControlledAction(fileMenu, importShark, "importShark");
1010
1011	QAction* exportShark = new QAction(tr("Export GameShark Save"), fileMenu);
1012	connect(exportShark, SIGNAL(triggered()), this, SLOT(exportSharkport()));
1013	m_gameActions.append(exportShark);
1014	m_gbaActions.append(exportShark);
1015	addControlledAction(fileMenu, exportShark, "exportShark");
1016#endif
1017
1018	fileMenu->addSeparator();
1019	QAction* multiWindow = new QAction(tr("New multiplayer window"), fileMenu);
1020	connect(multiWindow, &QAction::triggered, [this]() {
1021		GBAApp::app()->newWindow();
1022	});
1023	addControlledAction(fileMenu, multiWindow, "multiWindow");
1024
1025#ifndef Q_OS_MAC
1026	fileMenu->addSeparator();
1027#endif
1028
1029	QAction* about = new QAction(tr("About"), fileMenu);
1030	connect(about, SIGNAL(triggered()), this, SLOT(openAboutScreen()));
1031	fileMenu->addAction(about);
1032
1033#ifndef Q_OS_MAC
1034	addControlledAction(fileMenu, fileMenu->addAction(tr("E&xit"), this, SLOT(close()), QKeySequence::Quit), "quit");
1035#endif
1036
1037	QMenu* emulationMenu = menubar->addMenu(tr("&Emulation"));
1038	m_shortcutController->addMenu(emulationMenu);
1039	QAction* reset = new QAction(tr("&Reset"), emulationMenu);
1040	reset->setShortcut(tr("Ctrl+R"));
1041	connect(reset, SIGNAL(triggered()), m_controller, SLOT(reset()));
1042	m_gameActions.append(reset);
1043	addControlledAction(emulationMenu, reset, "reset");
1044
1045	QAction* shutdown = new QAction(tr("Sh&utdown"), emulationMenu);
1046	connect(shutdown, SIGNAL(triggered()), m_controller, SLOT(closeGame()));
1047	m_gameActions.append(shutdown);
1048	addControlledAction(emulationMenu, shutdown, "shutdown");
1049
1050#ifdef M_CORE_GBA
1051	QAction* yank = new QAction(tr("Yank game pak"), emulationMenu);
1052	connect(yank, SIGNAL(triggered()), m_controller, SLOT(yankPak()));
1053	m_gameActions.append(yank);
1054	m_gbaActions.append(yank);
1055	addControlledAction(emulationMenu, yank, "yank");
1056#endif
1057	emulationMenu->addSeparator();
1058
1059	QAction* pause = new QAction(tr("&Pause"), emulationMenu);
1060	pause->setChecked(false);
1061	pause->setCheckable(true);
1062	pause->setShortcut(tr("Ctrl+P"));
1063	connect(pause, SIGNAL(triggered(bool)), m_controller, SLOT(setPaused(bool)));
1064	connect(m_controller, &GameController::gamePaused, [this, pause]() {
1065		pause->setChecked(true);
1066	});
1067	connect(m_controller, &GameController::gameUnpaused, [pause]() { pause->setChecked(false); });
1068	m_gameActions.append(pause);
1069	addControlledAction(emulationMenu, pause, "pause");
1070
1071	QAction* frameAdvance = new QAction(tr("&Next frame"), emulationMenu);
1072	frameAdvance->setShortcut(tr("Ctrl+N"));
1073	connect(frameAdvance, SIGNAL(triggered()), m_controller, SLOT(frameAdvance()));
1074	m_gameActions.append(frameAdvance);
1075	addControlledAction(emulationMenu, frameAdvance, "frameAdvance");
1076
1077	emulationMenu->addSeparator();
1078
1079	m_shortcutController->addFunctions(emulationMenu, [this]() {
1080		m_controller->setTurbo(true, false);
1081	}, [this]() {
1082		m_controller->setTurbo(false, false);
1083	}, QKeySequence(Qt::Key_Tab), tr("Fast forward (held)"), "holdFastForward");
1084
1085	QAction* turbo = new QAction(tr("&Fast forward"), emulationMenu);
1086	turbo->setCheckable(true);
1087	turbo->setChecked(false);
1088	turbo->setShortcut(tr("Shift+Tab"));
1089	connect(turbo, SIGNAL(triggered(bool)), m_controller, SLOT(setTurbo(bool)));
1090	addControlledAction(emulationMenu, turbo, "fastForward");
1091
1092	QMenu* ffspeedMenu = emulationMenu->addMenu(tr("Fast forward speed"));
1093	ConfigOption* ffspeed = m_config->addOption("fastForwardRatio");
1094	ffspeed->connect([this](const QVariant& value) {
1095		m_controller->setTurboSpeed(value.toFloat());
1096	}, this);
1097	ffspeed->addValue(tr("Unbounded"), -1.0f, ffspeedMenu);
1098	ffspeed->setValue(QVariant(-1.0f));
1099	ffspeedMenu->addSeparator();
1100	for (i = 2; i < 11; ++i) {
1101		ffspeed->addValue(tr("%0x").arg(i), i, ffspeedMenu);
1102	}
1103	m_config->updateOption("fastForwardRatio");
1104
1105	m_shortcutController->addFunctions(emulationMenu, [this]() {
1106		m_controller->startRewinding();
1107	}, [this]() {
1108		m_controller->stopRewinding();
1109	}, QKeySequence("`"), tr("Rewind (held)"), "holdRewind");
1110
1111	QAction* rewind = new QAction(tr("Re&wind"), emulationMenu);
1112	rewind->setShortcut(tr("~"));
1113	connect(rewind, SIGNAL(triggered()), m_controller, SLOT(rewind()));
1114	m_gameActions.append(rewind);
1115	m_nonMpActions.append(rewind);
1116	addControlledAction(emulationMenu, rewind, "rewind");
1117
1118	QAction* frameRewind = new QAction(tr("Step backwards"), emulationMenu);
1119	frameRewind->setShortcut(tr("Ctrl+B"));
1120	connect(frameRewind, &QAction::triggered, [this] () {
1121		m_controller->rewind(1);
1122	});
1123	m_gameActions.append(frameRewind);
1124	m_nonMpActions.append(frameRewind);
1125	addControlledAction(emulationMenu, frameRewind, "frameRewind");
1126
1127	ConfigOption* videoSync = m_config->addOption("videoSync");
1128	videoSync->addBoolean(tr("Sync to &video"), emulationMenu);
1129	videoSync->connect([this](const QVariant& value) {
1130		reloadConfig();
1131	}, this);
1132	m_config->updateOption("videoSync");
1133
1134	ConfigOption* audioSync = m_config->addOption("audioSync");
1135	audioSync->addBoolean(tr("Sync to &audio"), emulationMenu);
1136	audioSync->connect([this](const QVariant& value) {
1137		reloadConfig();
1138	}, this);
1139	m_config->updateOption("audioSync");
1140
1141	emulationMenu->addSeparator();
1142
1143	QMenu* solarMenu = emulationMenu->addMenu(tr("Solar sensor"));
1144	m_shortcutController->addMenu(solarMenu);
1145	QAction* solarIncrease = new QAction(tr("Increase solar level"), solarMenu);
1146	connect(solarIncrease, SIGNAL(triggered()), m_controller, SLOT(increaseLuminanceLevel()));
1147	addControlledAction(solarMenu, solarIncrease, "increaseLuminanceLevel");
1148
1149	QAction* solarDecrease = new QAction(tr("Decrease solar level"), solarMenu);
1150	connect(solarDecrease, SIGNAL(triggered()), m_controller, SLOT(decreaseLuminanceLevel()));
1151	addControlledAction(solarMenu, solarDecrease, "decreaseLuminanceLevel");
1152
1153	QAction* maxSolar = new QAction(tr("Brightest solar level"), solarMenu);
1154	connect(maxSolar, &QAction::triggered, [this]() { m_controller->setLuminanceLevel(10); });
1155	addControlledAction(solarMenu, maxSolar, "maxLuminanceLevel");
1156
1157	QAction* minSolar = new QAction(tr("Darkest solar level"), solarMenu);
1158	connect(minSolar, &QAction::triggered, [this]() { m_controller->setLuminanceLevel(0); });
1159	addControlledAction(solarMenu, minSolar, "minLuminanceLevel");
1160
1161	solarMenu->addSeparator();
1162	for (int i = 0; i <= 10; ++i) {
1163		QAction* setSolar = new QAction(tr("Brightness %1").arg(QString::number(i)), solarMenu);
1164		connect(setSolar, &QAction::triggered, [this, i]() {
1165			m_controller->setLuminanceLevel(i);
1166		});
1167		addControlledAction(solarMenu, setSolar, QString("luminanceLevel.%1").arg(QString::number(i)));
1168	}
1169
1170	QMenu* avMenu = menubar->addMenu(tr("Audio/&Video"));
1171	m_shortcutController->addMenu(avMenu);
1172	QMenu* frameMenu = avMenu->addMenu(tr("Frame size"));
1173	m_shortcutController->addMenu(frameMenu, avMenu);
1174	for (int i = 1; i <= 6; ++i) {
1175		QAction* setSize = new QAction(tr("%1x").arg(QString::number(i)), avMenu);
1176		setSize->setCheckable(true);
1177		connect(setSize, &QAction::triggered, [this, i, setSize]() {
1178			showNormal();
1179			QSize size(VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS);
1180			if (m_controller->isLoaded()) {
1181				size = m_controller->screenDimensions();
1182			}
1183			size *= i;
1184			resizeFrame(size);
1185			bool enableSignals = setSize->blockSignals(true);
1186			setSize->setChecked(true);
1187			setSize->blockSignals(enableSignals);
1188		});
1189		m_frameSizes[i] = setSize;
1190		addControlledAction(frameMenu, setSize, QString("frame%1x").arg(QString::number(i)));
1191	}
1192	QKeySequence fullscreenKeys;
1193#ifdef Q_OS_WIN
1194	fullscreenKeys = QKeySequence("Alt+Return");
1195#else
1196	fullscreenKeys = QKeySequence("Ctrl+F");
1197#endif
1198	addControlledAction(frameMenu, frameMenu->addAction(tr("Toggle fullscreen"), this, SLOT(toggleFullScreen()), fullscreenKeys), "fullscreen");
1199
1200	ConfigOption* lockAspectRatio = m_config->addOption("lockAspectRatio");
1201	lockAspectRatio->addBoolean(tr("Lock aspect ratio"), avMenu);
1202	lockAspectRatio->connect([this](const QVariant& value) {
1203		m_display->lockAspectRatio(value.toBool());
1204	}, this);
1205	m_config->updateOption("lockAspectRatio");
1206
1207	ConfigOption* resampleVideo = m_config->addOption("resampleVideo");
1208	resampleVideo->addBoolean(tr("Resample video"), avMenu);
1209	resampleVideo->connect([this](const QVariant& value) {
1210		m_display->filter(value.toBool());
1211	}, this);
1212	m_config->updateOption("resampleVideo");
1213
1214	QMenu* skipMenu = avMenu->addMenu(tr("Frame&skip"));
1215	ConfigOption* skip = m_config->addOption("frameskip");
1216	skip->connect([this](const QVariant& value) {
1217		reloadConfig();
1218	}, this);
1219	for (int i = 0; i <= 10; ++i) {
1220		skip->addValue(QString::number(i), i, skipMenu);
1221	}
1222	m_config->updateOption("frameskip");
1223
1224	QAction* shaderView = new QAction(tr("Shader options..."), avMenu);
1225	connect(shaderView, SIGNAL(triggered()), m_shaderView, SLOT(show()));
1226	if (!m_display->supportsShaders()) {
1227		shaderView->setEnabled(false);
1228	}
1229	addControlledAction(avMenu, shaderView, "shaderSelector");
1230
1231	avMenu->addSeparator();
1232
1233	ConfigOption* mute = m_config->addOption("mute");
1234	mute->addBoolean(tr("Mute"), avMenu);
1235	mute->connect([this](const QVariant& value) {
1236		reloadConfig();
1237	}, this);
1238	m_config->updateOption("mute");
1239
1240	QMenu* target = avMenu->addMenu(tr("FPS target"));
1241	ConfigOption* fpsTargetOption = m_config->addOption("fpsTarget");
1242	fpsTargetOption->connect([this](const QVariant& value) {
1243		emit fpsTargetChanged(value.toFloat());
1244	}, this);
1245	fpsTargetOption->addValue(tr("15"), 15, target);
1246	fpsTargetOption->addValue(tr("30"), 30, target);
1247	fpsTargetOption->addValue(tr("45"), 45, target);
1248	fpsTargetOption->addValue(tr("Native (59.7)"), float(GBA_ARM7TDMI_FREQUENCY) / float(VIDEO_TOTAL_LENGTH), target);
1249	fpsTargetOption->addValue(tr("60"), 60, target);
1250	fpsTargetOption->addValue(tr("90"), 90, target);
1251	fpsTargetOption->addValue(tr("120"), 120, target);
1252	fpsTargetOption->addValue(tr("240"), 240, target);
1253	m_config->updateOption("fpsTarget");
1254
1255#if defined(USE_PNG) || defined(USE_FFMPEG) || defined(USE_MAGICK)
1256	avMenu->addSeparator();
1257#endif
1258
1259#ifdef USE_PNG
1260	QAction* screenshot = new QAction(tr("Take &screenshot"), avMenu);
1261	screenshot->setShortcut(tr("F12"));
1262	connect(screenshot, SIGNAL(triggered()), m_controller, SLOT(screenshot()));
1263	m_gameActions.append(screenshot);
1264	addControlledAction(avMenu, screenshot, "screenshot");
1265#endif
1266
1267#ifdef USE_FFMPEG
1268	QAction* recordOutput = new QAction(tr("Record output..."), avMenu);
1269	connect(recordOutput, SIGNAL(triggered()), this, SLOT(openVideoWindow()));
1270	addControlledAction(avMenu, recordOutput, "recordOutput");
1271	m_gameActions.append(recordOutput);
1272#endif
1273
1274#ifdef USE_MAGICK
1275	QAction* recordGIF = new QAction(tr("Record GIF..."), avMenu);
1276	connect(recordGIF, SIGNAL(triggered()), this, SLOT(openGIFWindow()));
1277	addControlledAction(avMenu, recordGIF, "recordGIF");
1278#endif
1279
1280	avMenu->addSeparator();
1281	QMenu* videoLayers = avMenu->addMenu(tr("Video layers"));
1282	m_shortcutController->addMenu(videoLayers, avMenu);
1283
1284	for (int i = 0; i < 4; ++i) {
1285		QAction* enableBg = new QAction(tr("Background %0").arg(i), videoLayers);
1286		enableBg->setCheckable(true);
1287		enableBg->setChecked(true);
1288		connect(enableBg, &QAction::triggered, [this, i](bool enable) { m_controller->setVideoLayerEnabled(i, enable); });
1289		addControlledAction(videoLayers, enableBg, QString("enableBG%0").arg(i));
1290	}
1291
1292	QAction* enableObj = new QAction(tr("OBJ (sprites)"), videoLayers);
1293	enableObj->setCheckable(true);
1294	enableObj->setChecked(true);
1295	connect(enableObj, &QAction::triggered, [this](bool enable) { m_controller->setVideoLayerEnabled(4, enable); });
1296	addControlledAction(videoLayers, enableObj, "enableOBJ");
1297
1298	QMenu* audioChannels = avMenu->addMenu(tr("Audio channels"));
1299	m_shortcutController->addMenu(audioChannels, avMenu);
1300
1301	for (int i = 0; i < 4; ++i) {
1302		QAction* enableCh = new QAction(tr("Channel %0").arg(i + 1), audioChannels);
1303		enableCh->setCheckable(true);
1304		enableCh->setChecked(true);
1305		connect(enableCh, &QAction::triggered, [this, i](bool enable) { m_controller->setAudioChannelEnabled(i, enable); });
1306		addControlledAction(audioChannels, enableCh, QString("enableCh%0").arg(i + 1));
1307	}
1308
1309	QAction* enableChA = new QAction(tr("Channel A"), audioChannels);
1310	enableChA->setCheckable(true);
1311	enableChA->setChecked(true);
1312	connect(enableChA, &QAction::triggered, [this, i](bool enable) { m_controller->setAudioChannelEnabled(4, enable); });
1313	addControlledAction(audioChannels, enableChA, QString("enableChA"));
1314
1315	QAction* enableChB = new QAction(tr("Channel B"), audioChannels);
1316	enableChB->setCheckable(true);
1317	enableChB->setChecked(true);
1318	connect(enableChB, &QAction::triggered, [this, i](bool enable) { m_controller->setAudioChannelEnabled(5, enable); });
1319	addControlledAction(audioChannels, enableChB, QString("enableChB"));
1320
1321	QMenu* toolsMenu = menubar->addMenu(tr("&Tools"));
1322	m_shortcutController->addMenu(toolsMenu);
1323	QAction* viewLogs = new QAction(tr("View &logs..."), toolsMenu);
1324	connect(viewLogs, SIGNAL(triggered()), m_logView, SLOT(show()));
1325	addControlledAction(toolsMenu, viewLogs, "viewLogs");
1326
1327#ifdef M_CORE_GBA
1328	QAction* overrides = new QAction(tr("Game &overrides..."), toolsMenu);
1329	connect(overrides, SIGNAL(triggered()), this, SLOT(openOverrideWindow()));
1330	addControlledAction(toolsMenu, overrides, "overrideWindow");
1331#endif
1332
1333	QAction* sensors = new QAction(tr("Game &Pak sensors..."), toolsMenu);
1334	connect(sensors, SIGNAL(triggered()), this, SLOT(openSensorWindow()));
1335	addControlledAction(toolsMenu, sensors, "sensorWindow");
1336
1337	QAction* cheats = new QAction(tr("&Cheats..."), toolsMenu);
1338	connect(cheats, SIGNAL(triggered()), this, SLOT(openCheatsWindow()));
1339	m_gameActions.append(cheats);
1340	addControlledAction(toolsMenu, cheats, "cheatsWindow");
1341
1342#ifdef USE_GDB_STUB
1343	QAction* gdbWindow = new QAction(tr("Start &GDB server..."), toolsMenu);
1344	connect(gdbWindow, SIGNAL(triggered()), this, SLOT(gdbOpen()));
1345	m_gbaActions.append(gdbWindow);
1346	addControlledAction(toolsMenu, gdbWindow, "gdbWindow");
1347#endif
1348
1349	toolsMenu->addSeparator();
1350	addControlledAction(toolsMenu, toolsMenu->addAction(tr("Settings..."), this, SLOT(openSettingsWindow())),
1351	                    "settings");
1352
1353	toolsMenu->addSeparator();
1354
1355	QAction* paletteView = new QAction(tr("View &palette..."), toolsMenu);
1356	connect(paletteView, SIGNAL(triggered()), this, SLOT(openPaletteWindow()));
1357	m_gameActions.append(paletteView);
1358	addControlledAction(toolsMenu, paletteView, "paletteWindow");
1359
1360#ifdef M_CORE_GBA
1361	QAction* tileView = new QAction(tr("View &tiles..."), toolsMenu);
1362	connect(tileView, SIGNAL(triggered()), this, SLOT(openTileWindow()));
1363	m_gameActions.append(tileView);
1364	m_gbaActions.append(tileView);
1365	addControlledAction(toolsMenu, tileView, "tileWindow");
1366#endif
1367
1368	QAction* memoryView = new QAction(tr("View memory..."), toolsMenu);
1369	connect(memoryView, SIGNAL(triggered()), this, SLOT(openMemoryWindow()));
1370	m_gameActions.append(memoryView);
1371	addControlledAction(toolsMenu, memoryView, "memoryView");
1372
1373#ifdef M_CORE_GBA
1374	QAction* ioViewer = new QAction(tr("View &I/O registers..."), toolsMenu);
1375	connect(ioViewer, SIGNAL(triggered()), this, SLOT(openIOViewer()));
1376	m_gameActions.append(ioViewer);
1377	m_gbaActions.append(ioViewer);
1378	addControlledAction(toolsMenu, ioViewer, "ioViewer");
1379#endif
1380
1381	ConfigOption* skipBios = m_config->addOption("skipBios");
1382	skipBios->connect([this](const QVariant& value) {
1383		reloadConfig();
1384	}, this);
1385
1386	ConfigOption* useBios = m_config->addOption("useBios");
1387	useBios->connect([this](const QVariant& value) {
1388		m_controller->setUseBIOS(value.toBool());
1389	}, this);
1390
1391	ConfigOption* buffers = m_config->addOption("audioBuffers");
1392	buffers->connect([this](const QVariant& value) {
1393		emit audioBufferSamplesChanged(value.toInt());
1394	}, this);
1395
1396	ConfigOption* sampleRate = m_config->addOption("sampleRate");
1397	sampleRate->connect([this](const QVariant& value) {
1398		emit sampleRateChanged(value.toUInt());
1399	}, this);
1400
1401	ConfigOption* volume = m_config->addOption("volume");
1402	volume->connect([this](const QVariant& value) {
1403		reloadConfig();
1404	}, this);
1405
1406	ConfigOption* rewindEnable = m_config->addOption("rewindEnable");
1407	rewindEnable->connect([this](const QVariant& value) {
1408		m_controller->setRewind(value.toBool(), m_config->getOption("rewindBufferCapacity").toInt());
1409	}, this);
1410
1411	ConfigOption* rewindBufferCapacity = m_config->addOption("rewindBufferCapacity");
1412	rewindBufferCapacity->connect([this](const QVariant& value) {
1413		m_controller->setRewind(m_config->getOption("rewindEnable").toInt(), value.toInt());
1414	}, this);
1415
1416	ConfigOption* allowOpposingDirections = m_config->addOption("allowOpposingDirections");
1417	allowOpposingDirections->connect([this](const QVariant& value) {
1418		m_inputController.setAllowOpposing(value.toBool());
1419	}, this);
1420
1421	ConfigOption* saveStateExtdata = m_config->addOption("saveStateExtdata");
1422	saveStateExtdata->connect([this](const QVariant& value) {
1423		m_controller->setSaveStateExtdata(value.toInt());
1424	}, this);
1425
1426	ConfigOption* loadStateExtdata = m_config->addOption("loadStateExtdata");
1427	loadStateExtdata->connect([this](const QVariant& value) {
1428		m_controller->setLoadStateExtdata(value.toInt());
1429	}, this);
1430
1431	QAction* exitFullScreen = new QAction(tr("Exit fullscreen"), frameMenu);
1432	connect(exitFullScreen, SIGNAL(triggered()), this, SLOT(exitFullScreen()));
1433	exitFullScreen->setShortcut(QKeySequence("Esc"));
1434	addHiddenAction(frameMenu, exitFullScreen, "exitFullScreen");
1435
1436	QMenu* autofireMenu = new QMenu(tr("Autofire"), this);
1437	m_shortcutController->addMenu(autofireMenu);
1438
1439	m_shortcutController->addFunctions(autofireMenu, [this]() {
1440		m_controller->setAutofire(GBA_KEY_A, true);
1441	}, [this]() {
1442		m_controller->setAutofire(GBA_KEY_A, false);
1443	}, QKeySequence(), tr("Autofire A"), "autofireA");
1444
1445	m_shortcutController->addFunctions(autofireMenu, [this]() {
1446		m_controller->setAutofire(GBA_KEY_B, true);
1447	}, [this]() {
1448		m_controller->setAutofire(GBA_KEY_B, false);
1449	}, QKeySequence(), tr("Autofire B"), "autofireB");
1450
1451	m_shortcutController->addFunctions(autofireMenu, [this]() {
1452		m_controller->setAutofire(GBA_KEY_L, true);
1453	}, [this]() {
1454		m_controller->setAutofire(GBA_KEY_L, false);
1455	}, QKeySequence(), tr("Autofire L"), "autofireL");
1456
1457	m_shortcutController->addFunctions(autofireMenu, [this]() {
1458		m_controller->setAutofire(GBA_KEY_R, true);
1459	}, [this]() {
1460		m_controller->setAutofire(GBA_KEY_R, false);
1461	}, QKeySequence(), tr("Autofire R"), "autofireR");
1462
1463	m_shortcutController->addFunctions(autofireMenu, [this]() {
1464		m_controller->setAutofire(GBA_KEY_START, true);
1465	}, [this]() {
1466		m_controller->setAutofire(GBA_KEY_START, false);
1467	}, QKeySequence(), tr("Autofire Start"), "autofireStart");
1468
1469	m_shortcutController->addFunctions(autofireMenu, [this]() {
1470		m_controller->setAutofire(GBA_KEY_SELECT, true);
1471	}, [this]() {
1472		m_controller->setAutofire(GBA_KEY_SELECT, false);
1473	}, QKeySequence(), tr("Autofire Select"), "autofireSelect");
1474
1475	m_shortcutController->addFunctions(autofireMenu, [this]() {
1476		m_controller->setAutofire(GBA_KEY_UP, true);
1477	}, [this]() {
1478		m_controller->setAutofire(GBA_KEY_UP, false);
1479	}, QKeySequence(), tr("Autofire Up"), "autofireUp");
1480
1481	m_shortcutController->addFunctions(autofireMenu, [this]() {
1482		m_controller->setAutofire(GBA_KEY_RIGHT, true);
1483	}, [this]() {
1484		m_controller->setAutofire(GBA_KEY_RIGHT, false);
1485	}, QKeySequence(), tr("Autofire Right"), "autofireRight");
1486
1487	m_shortcutController->addFunctions(autofireMenu, [this]() {
1488		m_controller->setAutofire(GBA_KEY_DOWN, true);
1489	}, [this]() {
1490		m_controller->setAutofire(GBA_KEY_DOWN, false);
1491	}, QKeySequence(), tr("Autofire Down"), "autofireDown");
1492
1493	m_shortcutController->addFunctions(autofireMenu, [this]() {
1494		m_controller->setAutofire(GBA_KEY_LEFT, true);
1495	}, [this]() {
1496		m_controller->setAutofire(GBA_KEY_LEFT, false);
1497	}, QKeySequence(), tr("Autofire Left"), "autofireLeft");
1498
1499	foreach (QAction* action, m_gameActions) {
1500		action->setDisabled(true);
1501	}
1502}
1503
1504void Window::attachWidget(QWidget* widget) {
1505	m_screenWidget->layout()->addWidget(widget);
1506	m_screenWidget->unsetCursor();
1507	static_cast<QStackedLayout*>(m_screenWidget->layout())->setCurrentWidget(widget);
1508}
1509
1510void Window::detachWidget(QWidget* widget) {
1511	m_screenWidget->layout()->removeWidget(widget);
1512}
1513
1514void Window::appendMRU(const QString& fname) {
1515	int index = m_mruFiles.indexOf(fname);
1516	if (index >= 0) {
1517		m_mruFiles.removeAt(index);
1518	}
1519	m_mruFiles.prepend(fname);
1520	while (m_mruFiles.size() > ConfigController::MRU_LIST_SIZE) {
1521		m_mruFiles.removeLast();
1522	}
1523	updateMRU();
1524}
1525
1526void Window::updateMRU() {
1527	if (!m_mruMenu) {
1528		return;
1529	}
1530	for (QAction* action : m_mruMenu->actions()) {
1531		delete action;
1532	}
1533	m_mruMenu->clear();
1534	int i = 0;
1535	for (const QString& file : m_mruFiles) {
1536		QAction* item = new QAction(file, m_mruMenu);
1537		item->setShortcut(QString("Ctrl+%1").arg(i));
1538		connect(item, &QAction::triggered, [this, file]() { m_controller->loadGame(file); });
1539		m_mruMenu->addAction(item);
1540		++i;
1541	}
1542	m_config->setMRU(m_mruFiles);
1543	m_config->write();
1544	m_mruMenu->setEnabled(i > 0);
1545}
1546
1547QAction* Window::addControlledAction(QMenu* menu, QAction* action, const QString& name) {
1548	addHiddenAction(menu, action, name);
1549	menu->addAction(action);
1550	return action;
1551}
1552
1553QAction* Window::addHiddenAction(QMenu* menu, QAction* action, const QString& name) {
1554	m_shortcutController->addAction(menu, action, name);
1555	action->setShortcutContext(Qt::WidgetShortcut);
1556	addAction(action);
1557	return action;
1558}
1559
1560void Window::focusCheck() {
1561	if (!m_config->getOption("pauseOnFocusLost").toInt()) {
1562		return;
1563	}
1564	if (QGuiApplication::focusWindow() && m_autoresume) {
1565		m_controller->setPaused(false);
1566		m_autoresume = false;
1567	} else if (!QGuiApplication::focusWindow() && !m_controller->isPaused()) {
1568		m_autoresume = true;
1569		m_controller->setPaused(true);
1570	}
1571}
1572
1573WindowBackground::WindowBackground(QWidget* parent)
1574	: QLabel(parent)
1575{
1576	setLayout(new QStackedLayout());
1577	layout()->setContentsMargins(0, 0, 0, 0);
1578	setAlignment(Qt::AlignCenter);
1579}
1580
1581void WindowBackground::setSizeHint(const QSize& hint) {
1582	m_sizeHint = hint;
1583}
1584
1585QSize WindowBackground::sizeHint() const {
1586	return m_sizeHint;
1587}
1588
1589void WindowBackground::setLockAspectRatio(int width, int height) {
1590	m_aspectWidth = width;
1591	m_aspectHeight = height;
1592}
1593
1594void WindowBackground::paintEvent(QPaintEvent*) {
1595	const QPixmap* logo = pixmap();
1596	if (!logo) {
1597		return;
1598	}
1599	QPainter painter(this);
1600	painter.setRenderHint(QPainter::SmoothPixmapTransform);
1601	painter.fillRect(QRect(QPoint(), size()), Qt::black);
1602	QSize s = size();
1603	QSize ds = s;
1604	if (ds.width() * m_aspectHeight > ds.height() * m_aspectWidth) {
1605		ds.setWidth(ds.height() * m_aspectWidth / m_aspectHeight);
1606	} else if (ds.width() * m_aspectHeight < ds.height() * m_aspectWidth) {
1607		ds.setHeight(ds.width() * m_aspectHeight / m_aspectWidth);
1608	}
1609	QPoint origin = QPoint((s.width() - ds.width()) / 2, (s.height() - ds.height()) / 2);
1610	QRect full(origin, ds);
1611	painter.drawPixmap(full, *logo);
1612}