// Copyright 2021 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/GBAWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "AudioCommon/AudioCommon.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/HW/GBACore.h" #include "Core/HW/GBAPad.h" #include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI_Device.h" #include "Core/Movie.h" #include "Core/NetPlayProto.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/Resources.h" #include "DolphinQt/Settings.h" #include "DolphinQt/Settings/GameCubePane.h" static void RestartCore(const std::weak_ptr& core, std::string_view rom_path = {}) { Core::RunOnCPUThread( [core, rom_path = std::string(rom_path)] { if (auto core_ptr = core.lock()) { auto& info = Config::MAIN_GBA_ROM_PATHS[core_ptr->GetDeviceNumber()]; core_ptr->Stop(); Config::SetCurrent(info, rom_path); if (core_ptr->Start(CoreTiming::GetTicks())) return; Config::SetCurrent(info, Config::GetBase(info)); core_ptr->Start(CoreTiming::GetTicks()); } }, false); } GBAWidget::GBAWidget(std::weak_ptr core, int device_number, std::string_view game_title, int width, int height, QWidget* parent, Qt::WindowFlags flags) : QWidget(parent, flags), m_core(std::move(core)), m_device_number(device_number), m_local_pad(device_number), m_game_title(game_title), m_width(width), m_height(height), m_is_local_pad(true), m_volume(0), m_muted(false), m_force_disconnect(false) { bool visible = true; setWindowIcon(Resources::GetAppIcon()); setAcceptDrops(true); resize(m_width, m_height); setVisible(visible); SetVolume(100); if (!visible) ToggleMute(); LoadGeometry(); UpdateTitle(); } GBAWidget::~GBAWidget() { SaveGeometry(); } void GBAWidget::GameChanged(std::string_view game_title, int width, int height) { m_game_title = game_title; m_width = width; m_height = height; UpdateTitle(); update(); } void GBAWidget::SetVideoBuffer(std::vector video_buffer) { m_video_buffer = std::move(video_buffer); update(); } void GBAWidget::SetVolume(int volume) { m_muted = false; m_volume = std::clamp(volume, 0, 100); UpdateVolume(); } void GBAWidget::VolumeDown() { SetVolume(m_volume - 10); } void GBAWidget::VolumeUp() { SetVolume(m_volume + 10); } bool GBAWidget::IsMuted() { return m_muted; } void GBAWidget::ToggleMute() { m_muted = !m_muted; UpdateVolume(); } void GBAWidget::ToggleDisconnect() { if (!CanControlCore()) return; m_force_disconnect = !m_force_disconnect; Core::RunOnCPUThread( [core = m_core, force_disconnect = m_force_disconnect] { if (auto core_ptr = core.lock()) core_ptr->SetForceDisconnect(force_disconnect); }, false); } void GBAWidget::LoadROM() { if (!CanControlCore()) return; std::string rom_path = GameCubePane::GetOpenGBARom(""); if (rom_path.empty()) return; RestartCore(m_core, rom_path); } void GBAWidget::UnloadROM() { if (!CanControlCore() || m_game_title.empty()) return; RestartCore(m_core); } void GBAWidget::ResetCore() { if (!CanResetCore()) return; Pad::SetGBAReset(m_local_pad, true); } void GBAWidget::DoState(bool export_state) { if (!CanControlCore() && !export_state) return; QString state_path = QDir::toNativeSeparators( (export_state ? QFileDialog::getSaveFileName : QFileDialog::getOpenFileName)( this, tr("Select a File"), QString(), tr("mGBA Save States (*.ss0 *.ss1 *.ss2 *.ss3 *.ss4 *.ss5 *.ss6 *.ss7 *.ss8 *.ss9);;" "All Files (*)"), nullptr, QFileDialog::Options())); if (state_path.isEmpty()) return; Core::RunOnCPUThread( [export_state, core = m_core, state_path = state_path.toStdString()] { if (auto core_ptr = core.lock()) { if (export_state) core_ptr->ExportState(state_path); else core_ptr->ImportState(state_path); } }, false); } void GBAWidget::Resize(int scale) { resize(m_width * scale, m_height * scale); } void GBAWidget::UpdateTitle() { std::string title = fmt::format("GBA{}", m_device_number + 1); if (!m_netplayer_name.empty()) title += " " + m_netplayer_name; if (!m_game_title.empty()) title += " | " + m_game_title; if (m_muted) title += " | Muted"; else title += fmt::format(" | Volume {}%", m_volume); setWindowTitle(QString::fromStdString(title)); } void GBAWidget::UpdateVolume() { int volume = m_muted ? 0 : m_volume * 256 / 100; g_sound_stream->GetMixer()->SetGBAVolume(m_device_number, volume, volume); UpdateTitle(); } void GBAWidget::LoadGeometry() { const QSettings& settings = Settings::GetQSettings(); const QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); if (settings.contains(key)) restoreGeometry(settings.value(key).toByteArray()); } void GBAWidget::SaveGeometry() { QSettings& settings = Settings::GetQSettings(); const QString key = QStringLiteral("gbawidget/geometry%1").arg(m_local_pad + 1); settings.setValue(key, saveGeometry()); } bool GBAWidget::CanControlCore() { return !Movie::IsMovieActive() && !NetPlay::IsNetPlayRunning(); } bool GBAWidget::CanResetCore() { return m_is_local_pad; } void GBAWidget::closeEvent(QCloseEvent* event) { event->ignore(); } void GBAWidget::contextMenuEvent(QContextMenuEvent* event) { auto* menu = new QMenu(this); connect(menu, &QMenu::triggered, menu, &QMenu::deleteLater); auto* disconnect_action = new QAction(m_force_disconnect ? tr("Dis&connected") : tr("&Connected"), menu); disconnect_action->setEnabled(CanControlCore()); disconnect_action->setCheckable(true); disconnect_action->setChecked(!m_force_disconnect); connect(disconnect_action, &QAction::triggered, this, &GBAWidget::ToggleDisconnect); auto* load_action = new QAction(tr("L&oad ROM"), menu); load_action->setEnabled(CanControlCore()); connect(load_action, &QAction::triggered, this, &GBAWidget::LoadROM); auto* unload_action = new QAction(tr("&Unload ROM"), menu); unload_action->setEnabled(CanControlCore() && !m_game_title.empty()); connect(unload_action, &QAction::triggered, this, &GBAWidget::UnloadROM); auto* reset_action = new QAction(tr("&Reset"), menu); reset_action->setEnabled(CanResetCore()); connect(reset_action, &QAction::triggered, this, &GBAWidget::ResetCore); auto* mute_action = new QAction(tr("&Mute"), menu); mute_action->setCheckable(true); mute_action->setChecked(m_muted); connect(mute_action, &QAction::triggered, this, &GBAWidget::ToggleMute); auto* size_menu = new QMenu(tr("Window Size"), menu); auto* x1_action = new QAction(tr("&1x"), size_menu); connect(x1_action, &QAction::triggered, this, [this] { Resize(1); }); auto* x2_action = new QAction(tr("&2x"), size_menu); connect(x2_action, &QAction::triggered, this, [this] { Resize(2); }); auto* x3_action = new QAction(tr("&3x"), size_menu); connect(x3_action, &QAction::triggered, this, [this] { Resize(3); }); auto* x4_action = new QAction(tr("&4x"), size_menu); connect(x4_action, &QAction::triggered, this, [this] { Resize(4); }); size_menu->addAction(x1_action); size_menu->addAction(x2_action); size_menu->addAction(x3_action); size_menu->addAction(x4_action); auto* state_menu = new QMenu(tr("Save State"), menu); auto* import_action = new QAction(tr("&Import State"), state_menu); import_action->setEnabled(CanControlCore()); connect(import_action, &QAction::triggered, this, [this] { DoState(false); }); auto* export_state = new QAction(tr("&Export State"), state_menu); connect(export_state, &QAction::triggered, this, [this] { DoState(true); }); state_menu->addAction(import_action); state_menu->addAction(export_state); menu->addAction(disconnect_action); menu->addSeparator(); menu->addAction(load_action); menu->addAction(unload_action); menu->addAction(reset_action); menu->addSeparator(); menu->addMenu(state_menu); menu->addSeparator(); menu->addAction(mute_action); menu->addSeparator(); menu->addMenu(size_menu); menu->move(event->globalPos()); menu->show(); } void GBAWidget::paintEvent(QPaintEvent* event) { QPainter painter(this); painter.fillRect(QRect(QPoint(), size()), Qt::black); if (m_video_buffer.size() == static_cast(m_width * m_height)) { QImage image(reinterpret_cast(m_video_buffer.data()), m_width, m_height, QImage::Format_ARGB32); image = image.convertToFormat(QImage::Format_RGB32); image = image.rgbSwapped(); QSize widget_size = size(); if (widget_size == QSize(m_width, m_height)) { painter.drawImage(QPoint(), image, QRect(0, 0, m_width, m_height)); } else if (static_cast(m_width) / m_height > static_cast(widget_size.width()) / widget_size.height()) { int new_height = widget_size.width() * m_height / m_width; painter.drawImage( QRect(0, (widget_size.height() - new_height) / 2, widget_size.width(), new_height), image, QRect(0, 0, m_width, m_height)); } else { int new_width = widget_size.height() * m_width / m_height; painter.drawImage( QRect((widget_size.width() - new_width) / 2, 0, new_width, widget_size.height()), image, QRect(0, 0, m_width, m_height)); } } } void GBAWidget::dragEnterEvent(QDragEnterEvent* event) { if (CanControlCore() && event->mimeData()->hasUrls()) event->acceptProposedAction(); } void GBAWidget::dropEvent(QDropEvent* event) { if (!CanControlCore()) return; for (const QUrl& url : event->mimeData()->urls()) { QFileInfo file_info(url.toLocalFile()); QString path = file_info.filePath(); if (!file_info.isFile()) continue; if (!file_info.exists() || !file_info.isReadable()) { ModalMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path)); continue; } if (file_info.suffix() == QStringLiteral("raw")) { Core::RunOnCPUThread( [core = m_core, card_path = path.toStdString()] { if (auto core_ptr = core.lock()) core_ptr->EReaderQueueCard(card_path); }, false); } else { RestartCore(m_core, path.toStdString()); } } } GBAWidgetController::~GBAWidgetController() { m_widget->deleteLater(); } void GBAWidgetController::Create(std::weak_ptr core, int device_number, std::string_view game_title, int width, int height) { m_widget = new GBAWidget(std::move(core), device_number, game_title, width, height); } void GBAWidgetController::GameChanged(std::string_view game_title, int width, int height) { m_widget->GameChanged(game_title, width, height); } void GBAWidgetController::FrameEnded(std::vector video_buffer) { m_widget->SetVideoBuffer(std::move(video_buffer)); }