all repos — mgba @ d3a0ce00db07af2f1322c4014b79b221ff1b98c9

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 <QFileDialog>
  9#include <QFileInfo>
 10#include <QKeyEvent>
 11#include <QKeySequence>
 12#include <QMenuBar>
 13#include <QMessageBox>
 14#include <QMimeData>
 15#include <QPainter>
 16#include <QStackedLayout>
 17
 18#include "CheatsView.h"
 19#include "ConfigController.h"
 20#include "DisplayGL.h"
 21#include "GameController.h"
 22#include "GBAKeyEditor.h"
 23#include "GDBController.h"
 24#include "GDBWindow.h"
 25#include "GIFView.h"
 26#include "LoadSaveState.h"
 27#include "LogView.h"
 28#include "MultiplayerController.h"
 29#include "OverrideView.h"
 30#include "SensorView.h"
 31#include "SettingsView.h"
 32#include "ShortcutController.h"
 33#include "ShortcutView.h"
 34#include "VideoView.h"
 35
 36extern "C" {
 37#include "platform/commandline.h"
 38}
 39
 40using namespace QGBA;
 41
 42Window::Window(ConfigController* config, int playerId, QWidget* parent)
 43	: QMainWindow(parent)
 44	, m_logView(new LogView())
 45	, m_stateWindow(nullptr)
 46	, m_screenWidget(new WindowBackground())
 47	, m_logo(":/res/mgba-1024.png")
 48	, m_config(config)
 49	, m_inputController(playerId)
 50#ifdef USE_FFMPEG
 51	, m_videoView(nullptr)
 52#endif
 53#ifdef USE_MAGICK
 54	, m_gifView(nullptr)
 55#endif
 56#ifdef USE_GDB_STUB
 57	, m_gdbController(nullptr)
 58#endif
 59	, m_mruMenu(nullptr)
 60	, m_shortcutController(new ShortcutController(this))
 61	, m_playerId(playerId)
 62{
 63	setWindowTitle(PROJECT_NAME);
 64	setFocusPolicy(Qt::StrongFocus);
 65	setAcceptDrops(true);
 66	m_controller = new GameController(this);
 67	m_controller->setInputController(&m_inputController);
 68	m_controller->setOverrides(m_config->overrides());
 69
 70	QGLFormat format(QGLFormat(QGL::Rgba | QGL::DoubleBuffer));
 71	format.setSwapInterval(1);
 72	m_display = new DisplayGL(format, this);
 73
 74	m_logo.setDevicePixelRatio(m_screenWidget->devicePixelRatio());
 75	m_logo = m_logo; // Free memory left over in old pixmap
 76
 77	m_screenWidget->setMinimumSize(m_display->minimumSize());
 78	m_screenWidget->setSizePolicy(m_display->sizePolicy());
 79	m_screenWidget->setSizeHint(m_display->minimumSize() * 2);
 80	m_screenWidget->setPixmap(m_logo);
 81	m_screenWidget->setLockAspectRatio(m_logo.width(), m_logo.height());
 82	setCentralWidget(m_screenWidget);
 83
 84	QVariant windowPos = m_config->getQtOption("windowPos");
 85	if (!windowPos.isNull()) {
 86		move(windowPos.toPoint());
 87	}
 88
 89	connect(m_controller, SIGNAL(gameStarted(GBAThread*)), this, SLOT(gameStarted(GBAThread*)));
 90	connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_display, SLOT(stopDrawing()));
 91	connect(m_controller, SIGNAL(gameStopped(GBAThread*)), this, SLOT(gameStopped()));
 92	connect(m_controller, SIGNAL(stateLoaded(GBAThread*)), m_display, SLOT(forceDraw()));
 93	connect(m_controller, SIGNAL(gamePaused(GBAThread*)), m_display, SLOT(pauseDrawing()));
 94#ifndef Q_OS_MAC
 95	connect(m_controller, SIGNAL(gamePaused(GBAThread*)), menuBar(), SLOT(show()));
 96	connect(m_controller, &GameController::gameUnpaused, [this]() {
 97		if(isFullScreen()) {
 98			menuBar()->hide();
 99		}
100	});
101#endif
102	connect(m_controller, SIGNAL(gameUnpaused(GBAThread*)), m_display, SLOT(unpauseDrawing()));
103	connect(m_controller, SIGNAL(postLog(int, const QString&)), m_logView, SLOT(postLog(int, const QString&)));
104	connect(m_controller, SIGNAL(frameAvailable(const uint32_t*)), this, SLOT(recordFrame()));
105	connect(m_controller, SIGNAL(frameAvailable(const uint32_t*)), m_display, SLOT(framePosted(const uint32_t*)));
106	connect(m_controller, SIGNAL(gameCrashed(const QString&)), this, SLOT(gameCrashed(const QString&)));
107	connect(m_controller, SIGNAL(gameFailed()), this, SLOT(gameFailed()));
108	connect(m_controller, SIGNAL(unimplementedBiosCall(int)), this, SLOT(unimplementedBiosCall(int)));
109	connect(m_logView, SIGNAL(levelsSet(int)), m_controller, SLOT(setLogLevel(int)));
110	connect(m_logView, SIGNAL(levelsEnabled(int)), m_controller, SLOT(enableLogLevel(int)));
111	connect(m_logView, SIGNAL(levelsDisabled(int)), m_controller, SLOT(disableLogLevel(int)));
112	connect(this, SIGNAL(startDrawing(const uint32_t*, GBAThread*)), m_display, SLOT(startDrawing(const uint32_t*, GBAThread*)), Qt::QueuedConnection);
113	connect(this, SIGNAL(shutdown()), m_display, SLOT(stopDrawing()));
114	connect(this, SIGNAL(shutdown()), m_controller, SLOT(closeGame()));
115	connect(this, SIGNAL(shutdown()), m_logView, SLOT(hide()));
116	connect(this, SIGNAL(audioBufferSamplesChanged(int)), m_controller, SLOT(setAudioBufferSamples(int)));
117	connect(this, SIGNAL(fpsTargetChanged(float)), m_controller, SLOT(setFPSTarget(float)));
118	connect(&m_fpsTimer, SIGNAL(timeout()), this, SLOT(showFPS()));
119
120	m_logView->setLevels(GBA_LOG_WARN | GBA_LOG_ERROR | GBA_LOG_FATAL);
121	m_fpsTimer.setInterval(FPS_TIMER_INTERVAL);
122
123	m_shortcutController->setConfigController(m_config);
124	setupMenu(menuBar());
125}
126
127Window::~Window() {
128	delete m_logView;
129
130#ifdef USE_FFMPEG
131	delete m_videoView;
132#endif
133
134#ifdef USE_MAGICK
135	delete m_gifView;
136#endif
137}
138
139void Window::argumentsPassed(GBAArguments* args) {
140	loadConfig();
141
142	if (args->patch) {
143		m_controller->loadPatch(args->patch);
144	}
145
146	if (args->fname) {
147		m_controller->loadGame(args->fname, args->dirmode);
148	}
149}
150
151void Window::resizeFrame(int width, int height) {
152	QSize newSize(width, height);
153	newSize -= m_screenWidget->size();
154	newSize += size();
155	resize(newSize);
156}
157
158void Window::setConfig(ConfigController* config) {
159	m_config = config;
160}
161
162void Window::loadConfig() {
163	const GBAOptions* opts = m_config->options();
164
165	m_logView->setLevels(opts->logLevel);
166
167	m_controller->setOptions(opts);
168	m_display->lockAspectRatio(opts->lockAspectRatio);
169	m_display->filter(opts->resampleVideo);
170
171	if (opts->bios) {
172		m_controller->loadBIOS(opts->bios);
173	}
174
175	if (opts->fpsTarget) {
176		emit fpsTargetChanged(opts->fpsTarget);
177	}
178
179	if (opts->audioBuffers) {
180		emit audioBufferSamplesChanged(opts->audioBuffers);
181	}
182
183	if (opts->width && opts->height) {
184		resizeFrame(opts->width, opts->height);
185	}
186
187	if (opts->fullscreen) {
188		enterFullScreen();
189	}
190
191	m_mruFiles = m_config->getMRU();
192	updateMRU();
193
194	m_inputController.setConfiguration(m_config);
195}
196
197void Window::saveConfig() {
198	m_config->write();
199}
200
201void Window::selectROM() {
202	bool doPause = m_controller->isLoaded() && !m_controller->isPaused();
203	if (doPause) {
204		m_controller->setPaused(true);
205	}
206	QString filename = QFileDialog::getOpenFileName(this, tr("Select ROM"), m_config->getQtOption("lastDirectory").toString(), tr("Game Boy Advance ROMs (*.gba *.zip *.rom *.bin)"));
207	if (doPause) {
208		m_controller->setPaused(false);
209	}
210	if (!filename.isEmpty()) {
211		m_config->setQtOption("lastDirectory", QFileInfo(filename).dir().path());
212		m_controller->loadGame(filename);
213	}
214}
215
216void Window::selectBIOS() {
217	bool doPause = m_controller->isLoaded() && !m_controller->isPaused();
218	if (doPause) {
219		m_controller->setPaused(true);
220	}
221	QString filename = QFileDialog::getOpenFileName(this, tr("Select BIOS"), m_config->getQtOption("lastDirectory").toString());
222	if (doPause) {
223		m_controller->setPaused(false);
224	}
225	if (!filename.isEmpty()) {
226		m_config->setQtOption("lastDirectory", QFileInfo(filename).dir().path());
227		m_config->setOption("bios", filename);
228		m_config->updateOption("bios");
229		m_config->setOption("useBios", true);
230		m_config->updateOption("useBios");
231		m_controller->loadBIOS(filename);
232	}
233}
234
235void Window::selectPatch() {
236	bool doPause = m_controller->isLoaded() && !m_controller->isPaused();
237	if (doPause) {
238		m_controller->setPaused(true);
239	}
240	QString filename = QFileDialog::getOpenFileName(this, tr("Select patch"), m_config->getQtOption("lastDirectory").toString(), tr("Patches (*.ips *.ups *.bps)"));
241	if (doPause) {
242		m_controller->setPaused(false);
243	}
244	if (!filename.isEmpty()) {
245		m_config->setQtOption("lastDirectory", QFileInfo(filename).dir().path());
246		m_controller->loadPatch(filename);
247	}
248}
249
250void Window::openKeymapWindow() {
251	GBAKeyEditor* keyEditor = new GBAKeyEditor(&m_inputController, InputController::KEYBOARD);
252	connect(this, SIGNAL(shutdown()), keyEditor, SLOT(close()));
253	keyEditor->setAttribute(Qt::WA_DeleteOnClose);
254	keyEditor->show();
255}
256
257void Window::openSettingsWindow() {
258	SettingsView* settingsWindow = new SettingsView(m_config);
259	connect(this, SIGNAL(shutdown()), settingsWindow, SLOT(close()));
260	connect(settingsWindow, SIGNAL(biosLoaded(const QString&)), m_controller, SLOT(loadBIOS(const QString&)));
261	connect(settingsWindow, SIGNAL(audioDriverChanged()), m_controller, SLOT(reloadAudioDriver()));
262	settingsWindow->setAttribute(Qt::WA_DeleteOnClose);
263	settingsWindow->show();
264}
265
266void Window::openShortcutWindow() {
267	ShortcutView* shortcutView = new ShortcutView();
268	shortcutView->setController(m_shortcutController);
269	connect(this, SIGNAL(shutdown()), shortcutView, SLOT(close()));
270	shortcutView->setAttribute(Qt::WA_DeleteOnClose);
271	shortcutView->show();
272}
273
274void Window::openOverrideWindow() {
275	OverrideView* overrideWindow = new OverrideView(m_controller, m_config);
276	connect(this, SIGNAL(shutdown()), overrideWindow, SLOT(close()));
277	overrideWindow->setAttribute(Qt::WA_DeleteOnClose);
278	overrideWindow->show();
279}
280
281void Window::openSensorWindow() {
282	SensorView* sensorWindow = new SensorView(m_controller);
283	connect(this, SIGNAL(shutdown()), sensorWindow, SLOT(close()));
284	sensorWindow->setAttribute(Qt::WA_DeleteOnClose);
285	sensorWindow->show();
286}
287
288void Window::openCheatsWindow() {
289	CheatsView* cheatsWindow = new CheatsView(m_controller);
290	connect(this, SIGNAL(shutdown()), cheatsWindow, SLOT(close()));
291	cheatsWindow->setAttribute(Qt::WA_DeleteOnClose);
292	cheatsWindow->show();
293}
294
295#ifdef BUILD_SDL
296void Window::openGamepadWindow() {
297	const char* profile = m_inputController.profileForType(SDL_BINDING_BUTTON);
298	GBAKeyEditor* keyEditor = new GBAKeyEditor(&m_inputController, SDL_BINDING_BUTTON, profile);
299	connect(this, SIGNAL(shutdown()), keyEditor, SLOT(close()));
300	keyEditor->setAttribute(Qt::WA_DeleteOnClose);
301	keyEditor->show();
302}
303#endif
304
305#ifdef USE_FFMPEG
306void Window::openVideoWindow() {
307	if (!m_videoView) {
308		m_videoView = new VideoView();
309		connect(m_videoView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*)));
310		connect(m_videoView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
311		connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_videoView, SLOT(stopRecording()));
312		connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_videoView, SLOT(close()));
313		connect(this, SIGNAL(shutdown()), m_videoView, SLOT(close()));
314	}
315	m_videoView->show();
316}
317#endif
318
319#ifdef USE_MAGICK
320void Window::openGIFWindow() {
321	if (!m_gifView) {
322		m_gifView = new GIFView();
323		connect(m_gifView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*)));
324		connect(m_gifView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection);
325		connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(stopRecording()));
326		connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(close()));
327		connect(this, SIGNAL(shutdown()), m_gifView, SLOT(close()));
328	}
329	m_gifView->show();
330}
331#endif
332
333#ifdef USE_GDB_STUB
334void Window::gdbOpen() {
335	if (!m_gdbController) {
336		m_gdbController = new GDBController(m_controller, this);
337	}
338	GDBWindow* window = new GDBWindow(m_gdbController);
339	connect(this, SIGNAL(shutdown()), window, SLOT(close()));
340	window->setAttribute(Qt::WA_DeleteOnClose);
341	window->show();
342}
343#endif
344
345void Window::keyPressEvent(QKeyEvent* event) {
346	if (event->isAutoRepeat()) {
347		QWidget::keyPressEvent(event);
348		return;
349	}
350	GBAKey key = m_inputController.mapKeyboard(event->key());
351	if (key == GBA_KEY_NONE) {
352		QWidget::keyPressEvent(event);
353		return;
354	}
355	m_controller->keyPressed(key);
356	event->accept();
357}
358
359void Window::keyReleaseEvent(QKeyEvent* event) {
360	if (event->isAutoRepeat()) {
361		QWidget::keyReleaseEvent(event);
362		return;
363	}
364	GBAKey key = m_inputController.mapKeyboard(event->key());
365	if (key == GBA_KEY_NONE) {
366		QWidget::keyPressEvent(event);
367		return;
368	}
369	m_controller->keyReleased(key);
370	event->accept();
371}
372
373void Window::resizeEvent(QResizeEvent*) {
374	m_config->setOption("height", m_screenWidget->height());
375	m_config->setOption("width", m_screenWidget->width());
376	m_config->setOption("fullscreen", isFullScreen());
377}
378
379void Window::closeEvent(QCloseEvent* event) {
380	emit shutdown();
381	m_config->setQtOption("windowPos", pos());
382	QMainWindow::closeEvent(event);
383}
384
385void Window::focusOutEvent(QFocusEvent*) {
386	m_controller->setTurbo(false, false);
387	m_controller->clearKeys();
388}
389
390void Window::dragEnterEvent(QDragEnterEvent* event) {
391	if (event->mimeData()->hasFormat("text/uri-list")) {
392		event->acceptProposedAction();
393	}
394}
395
396void Window::dropEvent(QDropEvent* event) {
397	QString uris = event->mimeData()->data("text/uri-list");
398	uris = uris.trimmed();
399	if (uris.contains("\n")) {
400		// Only one file please
401		return;
402	}
403	QUrl url(uris);
404	if (!url.isLocalFile()) {
405		// No remote loading
406		return;
407	}
408	event->accept();
409	m_controller->loadGame(url.path());
410}
411
412void Window::mouseDoubleClickEvent(QMouseEvent* event) {
413	if (event->button() != Qt::LeftButton) {
414		return;
415	}
416	toggleFullScreen();
417}
418
419void Window::enterFullScreen() {
420	if (isFullScreen()) {
421		return;
422	}
423	showFullScreen();
424#ifndef Q_OS_MAC
425	if (m_controller->isLoaded() && !m_controller->isPaused()) {
426		menuBar()->hide();
427	}
428#endif
429}
430
431void Window::exitFullScreen() {
432	if (!isFullScreen()) {
433		return;
434	}
435	showNormal();
436	menuBar()->show();
437}
438
439void Window::toggleFullScreen() {
440	if (isFullScreen()) {
441		exitFullScreen();
442	} else {
443		enterFullScreen();
444	}
445}
446
447void Window::gameStarted(GBAThread* context) {
448	char title[13] = { '\0' };
449	MutexLock(&context->stateMutex);
450	if (context->state < THREAD_EXITING) {
451		emit startDrawing(m_controller->drawContext(), context);
452		GBAGetGameTitle(context->gba, title);
453	} else {
454		MutexUnlock(&context->stateMutex);
455		return;
456	}
457	MutexUnlock(&context->stateMutex);
458	foreach (QAction* action, m_gameActions) {
459		action->setDisabled(false);
460	}
461	appendMRU(context->fname);
462	setWindowTitle(tr(PROJECT_NAME " - %1").arg(title));
463	attachWidget(m_display);
464
465#ifndef Q_OS_MAC
466	if(isFullScreen()) {
467		menuBar()->hide();
468	}
469#endif
470
471	m_hitUnimplementedBiosCall = false;
472	m_fpsTimer.start();
473}
474
475void Window::gameStopped() {
476	foreach (QAction* action, m_gameActions) {
477		action->setDisabled(true);
478	}
479	setWindowTitle(tr(PROJECT_NAME));
480	detachWidget(m_display);
481	m_screenWidget->setLockAspectRatio(m_logo.width(), m_logo.height());
482	m_screenWidget->setPixmap(m_logo);
483
484	m_fpsTimer.stop();
485}
486
487void Window::gameCrashed(const QString& errorMessage) {
488	QMessageBox* crash = new QMessageBox(QMessageBox::Critical, tr("Crash"),
489		tr("The game has crashed with the following error:\n\n%1").arg(errorMessage),
490		QMessageBox::Ok, this,  Qt::Sheet);
491	crash->setAttribute(Qt::WA_DeleteOnClose);
492	crash->show();
493}
494
495void Window::gameFailed() {
496	QMessageBox* fail = new QMessageBox(QMessageBox::Warning, tr("Couldn't Load"),
497		tr("Could not load game. Are you sure it's in the correct format?"),
498		QMessageBox::Ok, this,  Qt::Sheet);
499	fail->setAttribute(Qt::WA_DeleteOnClose);
500	fail->show();
501}
502
503void Window::unimplementedBiosCall(int call) {
504	if (m_hitUnimplementedBiosCall) {
505		return;
506	}
507	m_hitUnimplementedBiosCall = true;
508
509	QMessageBox* fail = new QMessageBox(QMessageBox::Warning, tr("Unimplemented BIOS call"),
510		tr("This game uses a BIOS call that is not implemented. Please use the official BIOS for best experience."),
511		QMessageBox::Ok, this,  Qt::Sheet);
512	fail->setAttribute(Qt::WA_DeleteOnClose);
513	fail->show();
514}
515
516void Window::recordFrame() {
517	m_frameList.append(QDateTime::currentDateTime());
518	while (m_frameList.count() > FRAME_LIST_SIZE) {
519		m_frameList.removeFirst();
520	}
521}
522
523void Window::showFPS() {
524	char title[13] = { '\0' };
525	GBAGetGameTitle(m_controller->thread()->gba, title);
526	if (m_frameList.isEmpty()) {
527		setWindowTitle(tr(PROJECT_NAME " - %1").arg(title));
528		return;
529	}
530	qint64 interval = m_frameList.first().msecsTo(m_frameList.last());
531	float fps = (m_frameList.count() - 1) * 10000.f / interval;
532	fps = round(fps) / 10.f;
533	setWindowTitle(tr(PROJECT_NAME " - %1 (%2 fps)").arg(title).arg(fps));
534}
535
536void Window::openStateWindow(LoadSave ls) {
537	if (m_stateWindow) {
538		return;
539	}
540	bool wasPaused = m_controller->isPaused();
541	m_stateWindow = new LoadSaveState(m_controller);
542	connect(this, SIGNAL(shutdown()), m_stateWindow, SLOT(close()));
543	connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_stateWindow, SLOT(close()));
544	connect(m_stateWindow, &LoadSaveState::closed, [this]() {
545		m_screenWidget->layout()->removeWidget(m_stateWindow);
546		m_stateWindow = nullptr;
547		setFocus();
548	});
549	if (!wasPaused) {
550		m_controller->setPaused(true);
551		connect(m_stateWindow, &LoadSaveState::closed, [this]() { m_controller->setPaused(false); });
552	}
553	m_stateWindow->setAttribute(Qt::WA_DeleteOnClose);
554	m_stateWindow->setMode(ls);
555	attachWidget(m_stateWindow);
556}
557
558void Window::setupMenu(QMenuBar* menubar) {
559	menubar->clear();
560	QMenu* fileMenu = menubar->addMenu(tr("&File"));
561	m_shortcutController->addMenu(fileMenu);
562	installEventFilter(m_shortcutController);
563	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &ROM..."), this, SLOT(selectROM()), QKeySequence::Open), "loadROM");
564	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &BIOS..."), this, SLOT(selectBIOS())), "loadBIOS");
565	addControlledAction(fileMenu, fileMenu->addAction(tr("Load &patch..."), this, SLOT(selectPatch())), "loadPatch");
566
567	m_mruMenu = fileMenu->addMenu(tr("Recent"));
568
569	fileMenu->addSeparator();
570
571	QAction* loadState = new QAction(tr("&Load state"), fileMenu);
572	loadState->setShortcut(tr("F10"));
573	connect(loadState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::LOAD); });
574	m_gameActions.append(loadState);
575	addControlledAction(fileMenu, loadState, "loadState");
576
577	QAction* saveState = new QAction(tr("&Save state"), fileMenu);
578	saveState->setShortcut(tr("Shift+F10"));
579	connect(saveState, &QAction::triggered, [this]() { this->openStateWindow(LoadSave::SAVE); });
580	m_gameActions.append(saveState);
581	addControlledAction(fileMenu, saveState, "saveState");
582
583	QMenu* quickLoadMenu = fileMenu->addMenu(tr("Quick load"));
584	QMenu* quickSaveMenu = fileMenu->addMenu(tr("Quick save"));
585	int i;
586	for (i = 1; i < 10; ++i) {
587		QAction* quickLoad = new QAction(tr("State &%1").arg(i), quickLoadMenu);
588		quickLoad->setShortcut(tr("F%1").arg(i));
589		connect(quickLoad, &QAction::triggered, [this, i]() { m_controller->loadState(i); });
590		m_gameActions.append(quickLoad);
591		addAction(quickLoad);
592		quickLoadMenu->addAction(quickLoad);
593
594		QAction* quickSave = new QAction(tr("State &%1").arg(i), quickSaveMenu);
595		quickSave->setShortcut(tr("Shift+F%1").arg(i));
596		connect(quickSave, &QAction::triggered, [this, i]() { m_controller->saveState(i); });
597		m_gameActions.append(quickSave);
598		addAction(quickSave);
599		quickSaveMenu->addAction(quickSave);
600	}
601
602	fileMenu->addSeparator();
603	QAction* multiWindow = new QAction(tr("New multiplayer window"), fileMenu);
604	connect(multiWindow, &QAction::triggered, [this]() {
605		std::shared_ptr<MultiplayerController> multiplayer = m_controller->multiplayerController();
606		if (!multiplayer) {
607			multiplayer = std::make_shared<MultiplayerController>();
608			m_controller->setMultiplayerController(multiplayer);
609		}
610		Window* w2 = new Window(m_config, multiplayer->attached());
611		w2->setAttribute(Qt::WA_DeleteOnClose);
612		w2->loadConfig();
613		w2->controller()->setMultiplayerController(multiplayer);
614		w2->show();
615	});
616	addControlledAction(fileMenu, multiWindow, "multiWindow");
617
618#ifndef Q_OS_MAC
619	addControlledAction(fileMenu, fileMenu->addAction(tr("E&xit"), this, SLOT(close()), QKeySequence::Quit), "quit");
620#endif
621
622	QMenu* emulationMenu = menubar->addMenu(tr("&Emulation"));
623	m_shortcutController->addMenu(emulationMenu);
624	QAction* reset = new QAction(tr("&Reset"), emulationMenu);
625	reset->setShortcut(tr("Ctrl+R"));
626	connect(reset, SIGNAL(triggered()), m_controller, SLOT(reset()));
627	m_gameActions.append(reset);
628	addControlledAction(emulationMenu, reset, "reset");
629
630	QAction* shutdown = new QAction(tr("Sh&utdown"), emulationMenu);
631	connect(shutdown, SIGNAL(triggered()), m_controller, SLOT(closeGame()));
632	m_gameActions.append(shutdown);
633	addControlledAction(emulationMenu, shutdown, "shutdown");
634	emulationMenu->addSeparator();
635
636	QAction* pause = new QAction(tr("&Pause"), emulationMenu);
637	pause->setChecked(false);
638	pause->setCheckable(true);
639	pause->setShortcut(tr("Ctrl+P"));
640	connect(pause, SIGNAL(triggered(bool)), m_controller, SLOT(setPaused(bool)));
641	connect(m_controller, &GameController::gamePaused, [this, pause]() {
642		pause->setChecked(true);
643
644		QImage currentImage(reinterpret_cast<const uchar*>(m_controller->drawContext()), VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, 1024, QImage::Format_RGB32);
645		QPixmap pixmap;
646		pixmap.convertFromImage(currentImage.rgbSwapped());
647		m_screenWidget->setPixmap(pixmap);
648		m_screenWidget->setLockAspectRatio(3, 2);
649	});
650	connect(m_controller, &GameController::gameUnpaused, [pause]() { pause->setChecked(false); });
651	m_gameActions.append(pause);
652	addControlledAction(emulationMenu, pause, "pause");
653
654	QAction* frameAdvance = new QAction(tr("&Next frame"), emulationMenu);
655	frameAdvance->setShortcut(tr("Ctrl+N"));
656	connect(frameAdvance, SIGNAL(triggered()), m_controller, SLOT(frameAdvance()));
657	m_gameActions.append(frameAdvance);
658	addControlledAction(emulationMenu, frameAdvance, "frameAdvance");
659
660	emulationMenu->addSeparator();
661
662	QAction* turbo = new QAction(tr("&Fast forward"), emulationMenu);
663	turbo->setCheckable(true);
664	turbo->setChecked(false);
665	turbo->setShortcut(tr("Shift+Tab"));
666	connect(turbo, SIGNAL(triggered(bool)), m_controller, SLOT(setTurbo(bool)));
667	addControlledAction(emulationMenu, turbo, "fastForward");
668
669	QAction* rewind = new QAction(tr("Re&wind"), emulationMenu);
670	rewind->setShortcut(tr("`"));
671	connect(rewind, SIGNAL(triggered()), m_controller, SLOT(rewind()));
672	m_gameActions.append(rewind);
673	addControlledAction(emulationMenu, rewind, "rewind");
674
675	ConfigOption* videoSync = m_config->addOption("videoSync");
676	videoSync->addBoolean(tr("Sync to &video"), emulationMenu);
677	videoSync->connect([this](const QVariant& value) {
678		m_controller->setVideoSync(value.toBool());
679	}, this);
680	m_config->updateOption("videoSync");
681
682	ConfigOption* audioSync = m_config->addOption("audioSync");
683	audioSync->addBoolean(tr("Sync to &audio"), emulationMenu);
684	audioSync->connect([this](const QVariant& value) {
685		m_controller->setAudioSync(value.toBool());
686	}, this);
687	m_config->updateOption("audioSync");
688
689	emulationMenu->addSeparator();
690
691	QMenu* solarMenu = emulationMenu->addMenu(tr("Solar sensor"));
692	QAction* solarIncrease = new QAction(tr("Increase solar level"), solarMenu);
693	connect(solarIncrease, SIGNAL(triggered()), m_controller, SLOT(increaseLuminanceLevel()));
694	addControlledAction(solarMenu, solarIncrease, "increaseLuminanceLevel");
695
696	QAction* solarDecrease = new QAction(tr("Decrease solar level"), solarMenu);
697	connect(solarDecrease, SIGNAL(triggered()), m_controller, SLOT(decreaseLuminanceLevel()));
698	addControlledAction(solarMenu, solarDecrease, "decreaseLuminanceLevel");
699
700	QAction* maxSolar = new QAction(tr("Brightest solar level"), solarMenu);
701	connect(maxSolar, &QAction::triggered, [this]() { m_controller->setLuminanceLevel(10); });
702	addControlledAction(solarMenu, maxSolar, "maxLuminanceLevel");
703
704	QAction* minSolar = new QAction(tr("Darkest solar level"), solarMenu);
705	connect(minSolar, &QAction::triggered, [this]() { m_controller->setLuminanceLevel(0); });
706	addControlledAction(solarMenu, minSolar, "minLuminanceLevel");
707
708	QMenu* avMenu = menubar->addMenu(tr("Audio/&Video"));
709	m_shortcutController->addMenu(avMenu);
710	QMenu* frameMenu = avMenu->addMenu(tr("Frame size"));
711	m_shortcutController->addMenu(frameMenu, avMenu);
712	for (int i = 1; i <= 6; ++i) {
713		QAction* setSize = new QAction(tr("%1x").arg(QString::number(i)), avMenu);
714		connect(setSize, &QAction::triggered, [this, i]() {
715			showNormal();
716			resizeFrame(VIDEO_HORIZONTAL_PIXELS * i, VIDEO_VERTICAL_PIXELS * i);
717		});
718		addControlledAction(frameMenu, setSize, tr("frame%1x").arg(QString::number(i)));
719	}
720	addControlledAction(frameMenu, frameMenu->addAction(tr("Fullscreen"), this, SLOT(toggleFullScreen()), QKeySequence("Ctrl+F")), "fullscreen");
721
722	ConfigOption* lockAspectRatio = m_config->addOption("lockAspectRatio");
723	lockAspectRatio->addBoolean(tr("Lock aspect ratio"), avMenu);
724	lockAspectRatio->connect([this](const QVariant& value) {
725		m_display->lockAspectRatio(value.toBool());
726	}, this);
727	m_config->updateOption("lockAspectRatio");
728
729	ConfigOption* resampleVideo = m_config->addOption("resampleVideo");
730	resampleVideo->addBoolean(tr("Resample video"), avMenu);
731	resampleVideo->connect([this](const QVariant& value) {
732		m_display->filter(value.toBool());
733	}, this);
734	m_config->updateOption("resampleVideo");
735
736	QMenu* skipMenu = avMenu->addMenu(tr("Frame&skip"));
737	ConfigOption* skip = m_config->addOption("frameskip");
738	skip->connect([this](const QVariant& value) {
739		m_controller->setFrameskip(value.toInt());
740	}, this);
741	for (int i = 0; i <= 10; ++i) {
742		skip->addValue(QString::number(i), i, skipMenu);
743	}
744	m_config->updateOption("frameskip");
745
746	avMenu->addSeparator();
747
748	QMenu* buffersMenu = avMenu->addMenu(tr("Audio buffer &size"));
749	ConfigOption* buffers = m_config->addOption("audioBuffers");
750	buffers->connect([this](const QVariant& value) {
751		emit audioBufferSamplesChanged(value.toInt());
752	}, this);
753	buffers->addValue(tr("512"), 512, buffersMenu);
754	buffers->addValue(tr("768"), 768, buffersMenu);
755	buffers->addValue(tr("1024"), 1024, buffersMenu);
756	buffers->addValue(tr("2048"), 2048, buffersMenu);
757	buffers->addValue(tr("4096"), 4096, buffersMenu);
758	m_config->updateOption("audioBuffers");
759
760	avMenu->addSeparator();
761
762	QMenu* target = avMenu->addMenu("FPS target");
763	ConfigOption* fpsTargetOption = m_config->addOption("fpsTarget");
764	fpsTargetOption->connect([this](const QVariant& value) {
765		emit fpsTargetChanged(value.toInt());
766	}, this);
767	fpsTargetOption->addValue(tr("15"), 15, target);
768	fpsTargetOption->addValue(tr("30"), 30, target);
769	fpsTargetOption->addValue(tr("45"), 45, target);
770	fpsTargetOption->addValue(tr("60"), 60, target);
771	fpsTargetOption->addValue(tr("90"), 90, target);
772	fpsTargetOption->addValue(tr("120"), 120, target);
773	fpsTargetOption->addValue(tr("240"), 240, target);
774	m_config->updateOption("fpsTarget");
775
776#if defined(USE_PNG) || defined(USE_FFMPEG) || defined(USE_MAGICK)
777	avMenu->addSeparator();
778#endif
779
780#ifdef USE_PNG
781	QAction* screenshot = new QAction(tr("Take &screenshot"), avMenu);
782	screenshot->setShortcut(tr("F12"));
783	connect(screenshot, SIGNAL(triggered()), m_controller, SLOT(screenshot()));
784	m_gameActions.append(screenshot);
785	addControlledAction(avMenu, screenshot, "screenshot");
786#endif
787
788#ifdef USE_FFMPEG
789	QAction* recordOutput = new QAction(tr("Record output..."), avMenu);
790	recordOutput->setShortcut(tr("F11"));
791	connect(recordOutput, SIGNAL(triggered()), this, SLOT(openVideoWindow()));
792	addControlledAction(avMenu, recordOutput, "recordOutput");
793#endif
794
795#ifdef USE_MAGICK
796	QAction* recordGIF = new QAction(tr("Record GIF..."), avMenu);
797	recordGIF->setShortcut(tr("Shift+F11"));
798	connect(recordGIF, SIGNAL(triggered()), this, SLOT(openGIFWindow()));
799	addControlledAction(avMenu, recordGIF, "recordGIF");
800#endif
801
802	QMenu* toolsMenu = menubar->addMenu(tr("&Tools"));
803	m_shortcutController->addMenu(toolsMenu);
804	QAction* viewLogs = new QAction(tr("View &logs..."), toolsMenu);
805	connect(viewLogs, SIGNAL(triggered()), m_logView, SLOT(show()));
806	addControlledAction(toolsMenu, viewLogs, "viewLogs");
807
808	QAction* overrides = new QAction(tr("Game &overrides..."), toolsMenu);
809	connect(overrides, SIGNAL(triggered()), this, SLOT(openOverrideWindow()));
810	addControlledAction(toolsMenu, overrides, "overrideWindow");
811
812	QAction* sensors = new QAction(tr("Game &Pak sensors..."), toolsMenu);
813	connect(sensors, SIGNAL(triggered()), this, SLOT(openSensorWindow()));
814	addControlledAction(toolsMenu, sensors, "sensorWindow");
815
816	QAction* cheats = new QAction(tr("&Cheats..."), toolsMenu);
817	connect(cheats, SIGNAL(triggered()), this, SLOT(openCheatsWindow()));
818	addControlledAction(toolsMenu, cheats, "cheatsWindow");
819
820#ifdef USE_GDB_STUB
821	QAction* gdbWindow = new QAction(tr("Start &GDB server..."), toolsMenu);
822	connect(gdbWindow, SIGNAL(triggered()), this, SLOT(gdbOpen()));
823	addControlledAction(toolsMenu, gdbWindow, "gdbWindow");
824#endif
825
826	toolsMenu->addSeparator();
827	addControlledAction(toolsMenu, toolsMenu->addAction(tr("Settings..."), this, SLOT(openSettingsWindow())), "settings");
828	addControlledAction(toolsMenu, toolsMenu->addAction(tr("Edit shortcuts..."), this, SLOT(openShortcutWindow())), "shortcuts");
829
830	QAction* keymap = new QAction(tr("Remap keyboard..."), toolsMenu);
831	connect(keymap, SIGNAL(triggered()), this, SLOT(openKeymapWindow()));
832	addControlledAction(toolsMenu, keymap, "remapKeyboard");
833
834#ifdef BUILD_SDL
835	QAction* gamepad = new QAction(tr("Remap gamepad..."), toolsMenu);
836	connect(gamepad, SIGNAL(triggered()), this, SLOT(openGamepadWindow()));
837	addControlledAction(toolsMenu, gamepad, "remapGamepad");
838#endif
839
840	ConfigOption* skipBios = m_config->addOption("skipBios");
841	skipBios->connect([this](const QVariant& value) {
842		m_controller->setSkipBIOS(value.toBool());
843	}, this);
844
845	ConfigOption* useBios = m_config->addOption("useBios");
846	useBios->connect([this](const QVariant& value) {
847		m_controller->setUseBIOS(value.toBool());
848	}, this);
849
850	ConfigOption* rewindEnable = m_config->addOption("rewindEnable");
851	rewindEnable->connect([this](const QVariant& value) {
852		m_controller->setRewind(value.toBool(), m_config->getOption("rewindBufferCapacity").toInt(), m_config->getOption("rewindBufferInterval").toInt());
853	}, this);
854
855	ConfigOption* rewindBufferCapacity = m_config->addOption("rewindBufferCapacity");
856	rewindBufferCapacity->connect([this](const QVariant& value) {
857		m_controller->setRewind(m_config->getOption("rewindEnable").toInt(), value.toInt(), m_config->getOption("rewindBufferInterval").toInt());
858	}, this);
859
860	ConfigOption* rewindBufferInterval = m_config->addOption("rewindBufferInterval");
861	rewindBufferInterval->connect([this](const QVariant& value) {
862		m_controller->setRewind(m_config->getOption("rewindEnable").toInt(), m_config->getOption("rewindBufferCapacity").toInt(), value.toInt());
863	}, this);
864
865	ConfigOption* allowOpposingDirections = m_config->addOption("allowOpposingDirections");
866	allowOpposingDirections->connect([this](const QVariant& value) {
867		m_inputController.setAllowOpposing(value.toBool());
868	}, this);
869
870	QMenu* other = new QMenu(tr("Other"), this);
871	m_shortcutController->addMenu(other);
872	m_shortcutController->addFunctions(other, [this]() {
873		m_controller->setTurbo(true, false);
874	}, [this]() {
875		m_controller->setTurbo(false, false);
876	}, QKeySequence(Qt::Key_Tab), tr("Fast Forward (held)"), "holdFastForward");
877
878	addControlledAction(other, other->addAction(tr("Exit fullscreen"), this, SLOT(exitFullScreen()), QKeySequence("Esc")), "exitFullScreen");
879
880	foreach (QAction* action, m_gameActions) {
881		action->setDisabled(true);
882	}
883}
884
885void Window::attachWidget(QWidget* widget) {
886	m_screenWidget->layout()->addWidget(widget);
887	static_cast<QStackedLayout*>(m_screenWidget->layout())->setCurrentWidget(widget);
888}
889
890void Window::detachWidget(QWidget* widget) {
891	m_screenWidget->layout()->removeWidget(widget);
892}
893
894void Window::appendMRU(const QString& fname) {
895	int index = m_mruFiles.indexOf(fname);
896	if (index >= 0) {
897		m_mruFiles.removeAt(index);
898	}
899	m_mruFiles.prepend(fname);
900	while (m_mruFiles.size() > ConfigController::MRU_LIST_SIZE) {
901		m_mruFiles.removeLast();
902	}
903	updateMRU();
904}
905
906void Window::updateMRU() {
907	if (!m_mruMenu) {
908		return;
909	}
910	m_mruMenu->clear();
911	int i = 0;
912	for (const QString& file : m_mruFiles) {
913		QAction* item = new QAction(file, m_mruMenu);
914		item->setShortcut(QString("Ctrl+%1").arg(i));
915		connect(item, &QAction::triggered, [this, file]() { m_controller->loadGame(file); });
916		m_mruMenu->addAction(item);
917		++i;
918	}
919	m_config->setMRU(m_mruFiles);
920	m_config->write();
921	m_mruMenu->setEnabled(i > 0);
922}
923
924QAction* Window::addControlledAction(QMenu* menu, QAction* action, const QString& name) {
925	m_shortcutController->addAction(menu, action, name);
926	menu->addAction(action);
927	action->setShortcutContext(Qt::WidgetShortcut);
928	addAction(action);
929	return action;
930}
931
932WindowBackground::WindowBackground(QWidget* parent)
933	: QLabel(parent)
934{
935	setLayout(new QStackedLayout());
936	layout()->setContentsMargins(0, 0, 0, 0);
937	setAlignment(Qt::AlignCenter);
938}
939
940void WindowBackground::setSizeHint(const QSize& hint) {
941	m_sizeHint = hint;
942}
943
944QSize WindowBackground::sizeHint() const {
945	return m_sizeHint;
946}
947
948void WindowBackground::setLockAspectRatio(int width, int height) {
949	m_aspectWidth = width;
950	m_aspectHeight = height;
951}
952
953void WindowBackground::paintEvent(QPaintEvent*) {
954	const QPixmap* logo = pixmap();
955	if (!logo) {
956		return;
957	}
958	QPainter painter(this);
959	painter.setRenderHint(QPainter::SmoothPixmapTransform);
960	painter.fillRect(QRect(QPoint(), size()), Qt::black);
961	QSize s = size();
962	QSize ds = s;
963	if (s.width() * m_aspectHeight > s.height() * m_aspectWidth) {
964		ds.setWidth(s.height() * m_aspectWidth / m_aspectHeight);
965	} else if (s.width() * m_aspectHeight < s.height() * m_aspectWidth) {
966		ds.setHeight(s.width() * m_aspectHeight / m_aspectWidth);
967	}
968	QPoint origin = QPoint((s.width() - ds.width()) / 2, (s.height() - ds.height()) / 2);
969	QRect full(origin, ds);
970	painter.drawPixmap(full, *logo);
971}