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