mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-29 17:15:40 +00:00
Qt: Add memory card editor
This commit is contained in:
parent
92da9917a8
commit
238152ae88
|
@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c
|
||||||
|
|
||||||
## Latest News
|
## Latest News
|
||||||
|
|
||||||
|
- 2020/09/19: Memory card importer/editor added to Qt frontend.
|
||||||
- 2020/09/13: Support for chaining post processing shaders added.
|
- 2020/09/13: Support for chaining post processing shaders added.
|
||||||
- 2020/09/12: Additional texture filtering options added.
|
- 2020/09/12: Additional texture filtering options added.
|
||||||
- 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet.
|
- 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet.
|
||||||
|
@ -63,6 +64,7 @@ Other features include:
|
||||||
- Automatic content scanning - game titles/regions are provided by redump.org
|
- Automatic content scanning - game titles/regions are provided by redump.org
|
||||||
- Optional automatic switching of memory cards for each game
|
- Optional automatic switching of memory cards for each game
|
||||||
- Supports loading cheats from libretro or PCSXR format lists
|
- Supports loading cheats from libretro or PCSXR format lists
|
||||||
|
- Memory card editor and save importer
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
- A CPU faster than a potato. But it needs to be 64-bit (either x86_64 or AArch64/ARMv8) otherwise you won't get a recompiler and it'll be slow. There are no plans to add any 32-bit recompilers.
|
- A CPU faster than a potato. But it needs to be 64-bit (either x86_64 or AArch64/ARMv8) otherwise you won't get a recompiler and it'll be slow. There are no plans to add any 32-bit recompilers.
|
||||||
|
|
|
@ -52,6 +52,9 @@ set(SRCS
|
||||||
mainwindow.cpp
|
mainwindow.cpp
|
||||||
mainwindow.h
|
mainwindow.h
|
||||||
mainwindow.ui
|
mainwindow.ui
|
||||||
|
memorycardeditordialog.cpp
|
||||||
|
memorycardeditordialog.h
|
||||||
|
memorycardeditordialog.ui
|
||||||
memorycardsettingswidget.cpp
|
memorycardsettingswidget.cpp
|
||||||
memorycardsettingswidget.h
|
memorycardsettingswidget.h
|
||||||
postprocessingchainconfigwidget.cpp
|
postprocessingchainconfigwidget.cpp
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<ClCompile Include="mainwindow.cpp" />
|
<ClCompile Include="mainwindow.cpp" />
|
||||||
<ClCompile Include="controllersettingswidget.cpp" />
|
<ClCompile Include="controllersettingswidget.cpp" />
|
||||||
<ClCompile Include="memorycardsettingswidget.cpp" />
|
<ClCompile Include="memorycardsettingswidget.cpp" />
|
||||||
|
<ClCompile Include="memorycardeditordialog.cpp" />
|
||||||
<ClCompile Include="postprocessingchainconfigwidget.cpp" />
|
<ClCompile Include="postprocessingchainconfigwidget.cpp" />
|
||||||
<ClCompile Include="postprocessingshaderconfigwidget.cpp" />
|
<ClCompile Include="postprocessingshaderconfigwidget.cpp" />
|
||||||
<ClCompile Include="postprocessingsettingswidget.cpp" />
|
<ClCompile Include="postprocessingsettingswidget.cpp" />
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
<QtMoc Include="controllersettingswidget.h" />
|
<QtMoc Include="controllersettingswidget.h" />
|
||||||
<QtMoc Include="enhancementsettingswidget.h" />
|
<QtMoc Include="enhancementsettingswidget.h" />
|
||||||
<QtMoc Include="memorycardsettingswidget.h" />
|
<QtMoc Include="memorycardsettingswidget.h" />
|
||||||
|
<QtMoc Include="memorycardeditordialog.h" />
|
||||||
<QtMoc Include="qtdisplaywidget.h" />
|
<QtMoc Include="qtdisplaywidget.h" />
|
||||||
<QtMoc Include="generalsettingswidget.h" />
|
<QtMoc Include="generalsettingswidget.h" />
|
||||||
<QtMoc Include="displaysettingswidget.h" />
|
<QtMoc Include="displaysettingswidget.h" />
|
||||||
|
@ -155,6 +157,9 @@
|
||||||
<QtUi Include="postprocessingsettingswidget.ui">
|
<QtUi Include="postprocessingsettingswidget.ui">
|
||||||
<FileType>Document</FileType>
|
<FileType>Document</FileType>
|
||||||
</QtUi>
|
</QtUi>
|
||||||
|
<QtUi Include="memorycardeditordialog.ui">
|
||||||
|
<FileType>Document</FileType>
|
||||||
|
</QtUi>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<QtResource Include="resources\resources.qrc">
|
<QtResource Include="resources\resources.qrc">
|
||||||
|
@ -181,6 +186,7 @@
|
||||||
<ClCompile Include="$(IntDir)moc_inputbindingwidgets.cpp" />
|
<ClCompile Include="$(IntDir)moc_inputbindingwidgets.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_mainwindow.cpp" />
|
<ClCompile Include="$(IntDir)moc_mainwindow.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_memorycardsettingswidget.cpp" />
|
<ClCompile Include="$(IntDir)moc_memorycardsettingswidget.cpp" />
|
||||||
|
<ClCompile Include="$(IntDir)moc_memorycardeditordialog.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_postprocessingchainconfigwidget.cpp" />
|
<ClCompile Include="$(IntDir)moc_postprocessingchainconfigwidget.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_postprocessingshaderconfigwidget.cpp" />
|
<ClCompile Include="$(IntDir)moc_postprocessingshaderconfigwidget.cpp" />
|
||||||
<ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
|
<ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "gamelistsettingswidget.h"
|
#include "gamelistsettingswidget.h"
|
||||||
#include "gamelistwidget.h"
|
#include "gamelistwidget.h"
|
||||||
#include "gamepropertiesdialog.h"
|
#include "gamepropertiesdialog.h"
|
||||||
|
#include "memorycardeditordialog.h"
|
||||||
#include "qtdisplaywidget.h"
|
#include "qtdisplaywidget.h"
|
||||||
#include "qthostinterface.h"
|
#include "qthostinterface.h"
|
||||||
#include "qtutils.h"
|
#include "qtutils.h"
|
||||||
|
@ -670,6 +671,7 @@ void MainWindow::connectSignals()
|
||||||
connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
|
connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
|
||||||
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
|
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
|
||||||
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
|
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
|
||||||
|
connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
|
||||||
|
|
||||||
connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
|
connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
|
||||||
Qt::BlockingQueuedConnection);
|
Qt::BlockingQueuedConnection);
|
||||||
|
@ -956,6 +958,15 @@ void MainWindow::onCheckForUpdatesActionTriggered()
|
||||||
checkForUpdates(true);
|
checkForUpdates(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::onToolsMemoryCardEditorTriggered()
|
||||||
|
{
|
||||||
|
if (!m_memory_card_editor_dialog)
|
||||||
|
m_memory_card_editor_dialog = new MemoryCardEditorDialog(this);
|
||||||
|
|
||||||
|
m_memory_card_editor_dialog->setModal(false);
|
||||||
|
m_memory_card_editor_dialog->show();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::checkForUpdates(bool display_message)
|
void MainWindow::checkForUpdates(bool display_message)
|
||||||
{
|
{
|
||||||
if (!AutoUpdaterDialog::isSupported())
|
if (!AutoUpdaterDialog::isSupported())
|
||||||
|
|
|
@ -14,6 +14,7 @@ class GameListWidget;
|
||||||
class QtHostInterface;
|
class QtHostInterface;
|
||||||
class QtDisplayWidget;
|
class QtDisplayWidget;
|
||||||
class AutoUpdaterDialog;
|
class AutoUpdaterDialog;
|
||||||
|
class MemoryCardEditorDialog;
|
||||||
|
|
||||||
class HostDisplay;
|
class HostDisplay;
|
||||||
struct GameListEntry;
|
struct GameListEntry;
|
||||||
|
@ -72,6 +73,7 @@ private Q_SLOTS:
|
||||||
void onDiscordServerActionTriggered();
|
void onDiscordServerActionTriggered();
|
||||||
void onAboutActionTriggered();
|
void onAboutActionTriggered();
|
||||||
void onCheckForUpdatesActionTriggered();
|
void onCheckForUpdatesActionTriggered();
|
||||||
|
void onToolsMemoryCardEditorTriggered();
|
||||||
|
|
||||||
void onGameListEntrySelected(const GameListEntry* entry);
|
void onGameListEntrySelected(const GameListEntry* entry);
|
||||||
void onGameListEntryDoubleClicked(const GameListEntry* entry);
|
void onGameListEntryDoubleClicked(const GameListEntry* entry);
|
||||||
|
@ -116,6 +118,7 @@ private:
|
||||||
|
|
||||||
SettingsDialog* m_settings_dialog = nullptr;
|
SettingsDialog* m_settings_dialog = nullptr;
|
||||||
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
|
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
|
||||||
|
MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr;
|
||||||
|
|
||||||
bool m_emulation_running = false;
|
bool m_emulation_running = false;
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<string>DuckStation</string>
|
<string>DuckStation</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
|
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QStackedWidget" name="mainContainer">
|
<widget class="QStackedWidget" name="mainContainer">
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>754</width>
|
<width>754</width>
|
||||||
<height>30</height>
|
<height>21</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuSystem">
|
<widget class="QMenu" name="menuSystem">
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<string>Change Disc</string>
|
<string>Change Disc</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/media-optical.png</normaloff>:/icons/media-optical.png</iconset>
|
<normaloff>:/icons/media-optical.png</normaloff>:/icons/media-optical.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuChangeDiscFromPlaylist">
|
<widget class="QMenu" name="menuChangeDiscFromPlaylist">
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<string>Cheats</string>
|
<string>Cheats</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/conical-flask-red.png</normaloff>:/icons/conical-flask-red.png</iconset>
|
<normaloff>:/icons/conical-flask-red.png</normaloff>:/icons/conical-flask-red.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
<string>Load State</string>
|
<string>Load State</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
|
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
<string>Save State</string>
|
<string>Save State</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset>
|
<normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -184,9 +184,16 @@
|
||||||
<addaction name="actionViewGameList"/>
|
<addaction name="actionViewGameList"/>
|
||||||
<addaction name="actionViewSystemDisplay"/>
|
<addaction name="actionViewSystemDisplay"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menu_Tools">
|
||||||
|
<property name="title">
|
||||||
|
<string>&Tools</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="actionMemory_Card_Editor"/>
|
||||||
|
</widget>
|
||||||
<addaction name="menuSystem"/>
|
<addaction name="menuSystem"/>
|
||||||
<addaction name="menuSettings"/>
|
<addaction name="menuSettings"/>
|
||||||
<addaction name="menu_View"/>
|
<addaction name="menu_View"/>
|
||||||
|
<addaction name="menu_Tools"/>
|
||||||
<addaction name="menuDebug"/>
|
<addaction name="menuDebug"/>
|
||||||
<addaction name="menuHelp"/>
|
<addaction name="menuHelp"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -228,7 +235,7 @@
|
||||||
<widget class="QStatusBar" name="statusBar"/>
|
<widget class="QStatusBar" name="statusBar"/>
|
||||||
<action name="actionStartDisc">
|
<action name="actionStartDisc">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/drive-optical.png</normaloff>:/icons/drive-optical.png</iconset>
|
<normaloff>:/icons/drive-optical.png</normaloff>:/icons/drive-optical.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -237,7 +244,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionStartBios">
|
<action name="actionStartBios">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/drive-removable-media.png</normaloff>:/icons/drive-removable-media.png</iconset>
|
<normaloff>:/icons/drive-removable-media.png</normaloff>:/icons/drive-removable-media.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -246,7 +253,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionScanForNewGames">
|
<action name="actionScanForNewGames">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/folder-open.png</normaloff>:/icons/folder-open.png</iconset>
|
<normaloff>:/icons/folder-open.png</normaloff>:/icons/folder-open.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -255,7 +262,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionRescanAllGames">
|
<action name="actionRescanAllGames">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/view-refresh.png</normaloff>:/icons/view-refresh.png</iconset>
|
<normaloff>:/icons/view-refresh.png</normaloff>:/icons/view-refresh.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -264,7 +271,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionPowerOff">
|
<action name="actionPowerOff">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/system-shutdown.png</normaloff>:/icons/system-shutdown.png</iconset>
|
<normaloff>:/icons/system-shutdown.png</normaloff>:/icons/system-shutdown.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -273,7 +280,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionReset">
|
<action name="actionReset">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/view-refresh.png</normaloff>:/icons/view-refresh.png</iconset>
|
<normaloff>:/icons/view-refresh.png</normaloff>:/icons/view-refresh.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -285,7 +292,7 @@
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/media-playback-pause.png</normaloff>:/icons/media-playback-pause.png</iconset>
|
<normaloff>:/icons/media-playback-pause.png</normaloff>:/icons/media-playback-pause.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -294,7 +301,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionLoadState">
|
<action name="actionLoadState">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
|
<normaloff>:/icons/document-open.png</normaloff>:/icons/document-open.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -303,7 +310,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionSaveState">
|
<action name="actionSaveState">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset>
|
<normaloff>:/icons/document-save.png</normaloff>:/icons/document-save.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -317,7 +324,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionConsoleSettings">
|
<action name="actionConsoleSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/utilities-system-monitor.png</normaloff>:/icons/utilities-system-monitor.png</iconset>
|
<normaloff>:/icons/utilities-system-monitor.png</normaloff>:/icons/utilities-system-monitor.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -326,7 +333,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionControllerSettings">
|
<action name="actionControllerSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/input-gaming.png</normaloff>:/icons/input-gaming.png</iconset>
|
<normaloff>:/icons/input-gaming.png</normaloff>:/icons/input-gaming.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -335,7 +342,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionHotkeySettings">
|
<action name="actionHotkeySettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/applications-other.png</normaloff>:/icons/applications-other.png</iconset>
|
<normaloff>:/icons/applications-other.png</normaloff>:/icons/applications-other.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -344,7 +351,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionDisplaySettings">
|
<action name="actionDisplaySettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/video-display.png</normaloff>:/icons/video-display.png</iconset>
|
<normaloff>:/icons/video-display.png</normaloff>:/icons/video-display.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -353,7 +360,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionEnhancementSettings">
|
<action name="actionEnhancementSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/antialias-icon.png</normaloff>:/icons/antialias-icon.png</iconset>
|
<normaloff>:/icons/antialias-icon.png</normaloff>:/icons/antialias-icon.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -362,7 +369,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionPostProcessingSettings">
|
<action name="actionPostProcessingSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/applications-graphics.png</normaloff>:/icons/applications-graphics.png</iconset>
|
<normaloff>:/icons/applications-graphics.png</normaloff>:/icons/applications-graphics.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -371,7 +378,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionFullscreen">
|
<action name="actionFullscreen">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/view-fullscreen.png</normaloff>:/icons/view-fullscreen.png</iconset>
|
<normaloff>:/icons/view-fullscreen.png</normaloff>:/icons/view-fullscreen.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -410,7 +417,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionChangeDisc">
|
<action name="actionChangeDisc">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/media-optical.png</normaloff>:/icons/media-optical.png</iconset>
|
<normaloff>:/icons/media-optical.png</normaloff>:/icons/media-optical.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -419,7 +426,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionCheats">
|
<action name="actionCheats">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/conical-flask-red.png</normaloff>:/icons/conical-flask-red.png</iconset>
|
<normaloff>:/icons/conical-flask-red.png</normaloff>:/icons/conical-flask-red.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -428,7 +435,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAudioSettings">
|
<action name="actionAudioSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/audio-card.png</normaloff>:/icons/audio-card.png</iconset>
|
<normaloff>:/icons/audio-card.png</normaloff>:/icons/audio-card.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -437,7 +444,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionGameListSettings">
|
<action name="actionGameListSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/folder-open.png</normaloff>:/icons/folder-open.png</iconset>
|
<normaloff>:/icons/folder-open.png</normaloff>:/icons/folder-open.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -446,7 +453,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionGeneralSettings">
|
<action name="actionGeneralSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/applications-system.png</normaloff>:/icons/applications-system.png</iconset>
|
<normaloff>:/icons/applications-system.png</normaloff>:/icons/applications-system.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -455,7 +462,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAdvancedSettings">
|
<action name="actionAdvancedSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/applications-development.png</normaloff>:/icons/applications-development.png</iconset>
|
<normaloff>:/icons/applications-development.png</normaloff>:/icons/applications-development.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -464,7 +471,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAddGameDirectory">
|
<action name="actionAddGameDirectory">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/edit-find.png</normaloff>:/icons/edit-find.png</iconset>
|
<normaloff>:/icons/edit-find.png</normaloff>:/icons/edit-find.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -473,7 +480,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionSettings">
|
<action name="actionSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/preferences-system.png</normaloff>:/icons/preferences-system.png</iconset>
|
<normaloff>:/icons/preferences-system.png</normaloff>:/icons/preferences-system.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -584,7 +591,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionScreenshot">
|
<action name="actionScreenshot">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/camera-photo.png</normaloff>:/icons/camera-photo.png</iconset>
|
<normaloff>:/icons/camera-photo.png</normaloff>:/icons/camera-photo.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -593,7 +600,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionMemoryCardSettings">
|
<action name="actionMemoryCardSettings">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/media-flash-24.png</normaloff>:/icons/media-flash-24.png</iconset>
|
<normaloff>:/icons/media-flash-24.png</normaloff>:/icons/media-flash-24.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -602,7 +609,7 @@
|
||||||
</action>
|
</action>
|
||||||
<action name="actionResumeLastState">
|
<action name="actionResumeLastState">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="resources/icons.qrc">
|
<iconset>
|
||||||
<normaloff>:/icons/media-playback-start.png</normaloff>:/icons/media-playback-start.png</iconset>
|
<normaloff>:/icons/media-playback-start.png</normaloff>:/icons/media-playback-start.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -647,6 +654,11 @@
|
||||||
<string>System &Display</string>
|
<string>System &Display</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionMemory_Card_Editor">
|
||||||
|
<property name="text">
|
||||||
|
<string>Memory &Card Editor</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="resources/icons.qrc"/>
|
<include location="resources/icons.qrc"/>
|
||||||
|
|
389
src/duckstation-qt/memorycardeditordialog.cpp
Normal file
389
src/duckstation-qt/memorycardeditordialog.cpp
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
#include "memorycardeditordialog.h"
|
||||||
|
#include "common/file_system.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "core/host_interface.h"
|
||||||
|
#include "qtutils.h"
|
||||||
|
#include <QtCore/QFileInfo>
|
||||||
|
#include <QtWidgets/QFileDialog>
|
||||||
|
#include <QtWidgets/QMessageBox>
|
||||||
|
|
||||||
|
static constexpr char MEMORY_CARD_IMAGE_FILTER[] =
|
||||||
|
QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Memory Card Types (*.mcd *.mcr *.mc)");
|
||||||
|
static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
|
||||||
|
QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)");
|
||||||
|
|
||||||
|
MemoryCardEditorDialog::MemoryCardEditorDialog(QWidget* parent) : QDialog(parent)
|
||||||
|
{
|
||||||
|
m_ui.setupUi(this);
|
||||||
|
m_card_a.path_cb = m_ui.cardAPath;
|
||||||
|
m_card_a.table = m_ui.cardA;
|
||||||
|
m_card_a.blocks_free_label = m_ui.cardAUsage;
|
||||||
|
m_card_a.save_button = m_ui.saveCardA;
|
||||||
|
m_card_b.path_cb = m_ui.cardBPath;
|
||||||
|
m_card_b.table = m_ui.cardB;
|
||||||
|
m_card_b.blocks_free_label = m_ui.cardBUsage;
|
||||||
|
m_card_b.save_button = m_ui.saveCardB;
|
||||||
|
|
||||||
|
connectUi();
|
||||||
|
populateComboBox(m_ui.cardAPath);
|
||||||
|
populateComboBox(m_ui.cardBPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryCardEditorDialog::~MemoryCardEditorDialog() = default;
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::resizeEvent(QResizeEvent* ev)
|
||||||
|
{
|
||||||
|
QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 100, 45});
|
||||||
|
QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 100, 45});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::closeEvent(QCloseEvent* ev)
|
||||||
|
{
|
||||||
|
promptForSave(&m_card_a);
|
||||||
|
promptForSave(&m_card_b);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::connectUi()
|
||||||
|
{
|
||||||
|
connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardASelectionChanged);
|
||||||
|
connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardBSelectionChanged);
|
||||||
|
connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
|
||||||
|
connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
|
||||||
|
connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile);
|
||||||
|
|
||||||
|
connect(m_ui.cardAPath, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
|
[this](int index) { loadCardFromComboBox(&m_card_a, index); });
|
||||||
|
connect(m_ui.cardBPath, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
|
[this](int index) { loadCardFromComboBox(&m_card_b, index); });
|
||||||
|
connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); });
|
||||||
|
connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); });
|
||||||
|
connect(m_ui.saveCardA, &QPushButton::clicked, [this]() { saveCard(&m_card_a); });
|
||||||
|
connect(m_ui.saveCardB, &QPushButton::clicked, [this]() { saveCard(&m_card_b); });
|
||||||
|
connect(m_ui.importCardA, &QPushButton::clicked, [this]() { importCard(&m_card_a); });
|
||||||
|
connect(m_ui.importCardB, &QPushButton::clicked, [this]() { importCard(&m_card_b); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::populateComboBox(QComboBox* cb)
|
||||||
|
{
|
||||||
|
QSignalBlocker sb(cb);
|
||||||
|
|
||||||
|
cb->clear();
|
||||||
|
|
||||||
|
cb->addItem(QString());
|
||||||
|
cb->addItem(tr("Browse..."));
|
||||||
|
|
||||||
|
const std::string base_path(g_host_interface->GetUserDirectoryRelativePath("memcards"));
|
||||||
|
FileSystem::FindResultsArray results;
|
||||||
|
FileSystem::FindFiles(base_path.c_str(), "*.mcd", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results);
|
||||||
|
for (FILESYSTEM_FIND_DATA& fd : results)
|
||||||
|
{
|
||||||
|
std::string real_filename(
|
||||||
|
StringUtil::StdStringFromFormat("%s%c%s", base_path.c_str(), FS_OSPATH_SEPERATOR_CHARACTER, fd.FileName.c_str()));
|
||||||
|
std::string::size_type pos = fd.FileName.rfind('.');
|
||||||
|
if (pos != std::string::npos)
|
||||||
|
fd.FileName.erase(pos);
|
||||||
|
|
||||||
|
cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index)
|
||||||
|
{
|
||||||
|
QString filename;
|
||||||
|
if (index == 1)
|
||||||
|
{
|
||||||
|
filename = QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
|
||||||
|
if (!filename.isEmpty())
|
||||||
|
{
|
||||||
|
// add to combo box
|
||||||
|
QFileInfo file(filename);
|
||||||
|
QSignalBlocker sb(card->path_cb);
|
||||||
|
card->path_cb->addItem(file.baseName(), QVariant(filename));
|
||||||
|
card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filename = card->path_cb->itemData(index).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
loadCard(filename, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::onCardASelectionChanged()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QSignalBlocker cb(m_card_b.table);
|
||||||
|
m_card_b.table->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::onCardBSelectionChanged()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QSignalBlocker cb(m_card_a.table);
|
||||||
|
m_card_a.table->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::clearSelection()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QSignalBlocker cb(m_card_a.table);
|
||||||
|
m_card_a.table->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QSignalBlocker cb(m_card_b.table);
|
||||||
|
m_card_b.table->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card)
|
||||||
|
{
|
||||||
|
promptForSave(card);
|
||||||
|
|
||||||
|
card->table->setRowCount(0);
|
||||||
|
card->dirty = false;
|
||||||
|
card->blocks_free_label->clear();
|
||||||
|
card->save_button->setEnabled(false);
|
||||||
|
|
||||||
|
card->filename.clear();
|
||||||
|
|
||||||
|
std::string filename_str = filename.toStdString();
|
||||||
|
if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str()))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Failed to load memory card image."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
card->filename = std::move(filename_str);
|
||||||
|
updateCardTable(card);
|
||||||
|
updateCardBlocksFree(card);
|
||||||
|
updateButtonState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::updateCardTable(Card* card)
|
||||||
|
{
|
||||||
|
card->table->setRowCount(0);
|
||||||
|
|
||||||
|
card->files = MemoryCardImage::EnumerateFiles(card->data);
|
||||||
|
for (const MemoryCardImage::FileInfo& fi : card->files)
|
||||||
|
{
|
||||||
|
const int row = card->table->rowCount();
|
||||||
|
card->table->insertRow(row);
|
||||||
|
|
||||||
|
if (!fi.icon_frames.empty())
|
||||||
|
{
|
||||||
|
const QImage image(reinterpret_cast<const u8*>(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH,
|
||||||
|
MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
|
||||||
|
|
||||||
|
QTableWidgetItem* icon = new QTableWidgetItem();
|
||||||
|
icon->setIcon(QIcon(QPixmap::fromImage(image)));
|
||||||
|
card->table->setItem(row, 0, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
card->table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(fi.title)));
|
||||||
|
card->table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(fi.filename)));
|
||||||
|
card->table->setItem(row, 3, new QTableWidgetItem(QStringLiteral("%1").arg(fi.num_blocks)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::updateCardBlocksFree(Card* card)
|
||||||
|
{
|
||||||
|
card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data);
|
||||||
|
card->blocks_free_label->setText(
|
||||||
|
tr("%1 blocks free%2").arg(card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::setCardDirty(Card* card)
|
||||||
|
{
|
||||||
|
card->dirty = true;
|
||||||
|
card->save_button->setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::newCard(Card* card)
|
||||||
|
{
|
||||||
|
promptForSave(card);
|
||||||
|
|
||||||
|
QString filename =
|
||||||
|
QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
|
||||||
|
if (filename.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
// add to combo box
|
||||||
|
QFileInfo file(filename);
|
||||||
|
QSignalBlocker sb(card->path_cb);
|
||||||
|
card->path_cb->addItem(file.baseName(), QVariant(filename));
|
||||||
|
card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
card->filename = filename.toStdString();
|
||||||
|
|
||||||
|
MemoryCardImage::Format(&card->data);
|
||||||
|
updateCardTable(card);
|
||||||
|
updateCardBlocksFree(card);
|
||||||
|
updateButtonState();
|
||||||
|
saveCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::saveCard(Card* card)
|
||||||
|
{
|
||||||
|
if (card->filename.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str()))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("Failed to write card to '%1'").arg(QString::fromStdString(card->filename)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
card->dirty = false;
|
||||||
|
card->save_button->setEnabled(false);
|
||||||
|
updateCardBlocksFree(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::promptForSave(Card* card)
|
||||||
|
{
|
||||||
|
if (card->filename.empty() || !card->dirty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (QMessageBox::question(this, tr("Save memory card?"),
|
||||||
|
tr("Memory card '%1' is not saved, do you want to save before closing?")
|
||||||
|
.arg(QString::fromStdString(card->filename)),
|
||||||
|
QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCard(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::doCopyFile()
|
||||||
|
{
|
||||||
|
const auto [src, fi] = getSelectedFile();
|
||||||
|
if (!fi)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a;
|
||||||
|
|
||||||
|
if (dst->blocks_free < fi->num_blocks)
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"),
|
||||||
|
tr("Insufficient blocks, this file needs %1 but only %2 are available.")
|
||||||
|
.arg(fi->num_blocks)
|
||||||
|
.arg(dst->blocks_free));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<u8> buffer;
|
||||||
|
if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Failed to read file %1").arg(QString::fromStdString(fi->filename)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Failed to write file %1").arg(QString::fromStdString(fi->filename)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
updateCardTable(dst);
|
||||||
|
updateCardBlocksFree(dst);
|
||||||
|
setCardDirty(dst);
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::doDeleteFile()
|
||||||
|
{
|
||||||
|
const auto [card, fi] = getSelectedFile();
|
||||||
|
if (!fi)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!MemoryCardImage::DeleteFile(&card->data, *fi))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
updateCardTable(card);
|
||||||
|
updateCardBlocksFree(card);
|
||||||
|
setCardDirty(card);
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::importCard(Card* card)
|
||||||
|
{
|
||||||
|
promptForSave(card);
|
||||||
|
|
||||||
|
QString filename =
|
||||||
|
QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER));
|
||||||
|
if (filename.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::unique_ptr<MemoryCardImage::DataArray> temp = std::make_unique<MemoryCardImage::DataArray>();
|
||||||
|
if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str()))
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Failed to import memory card. The log may contain more information."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
|
||||||
|
card->data = *temp;
|
||||||
|
updateCardTable(card);
|
||||||
|
updateCardBlocksFree(card);
|
||||||
|
setCardDirty(card);
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<MemoryCardEditorDialog::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorDialog::getSelectedFile()
|
||||||
|
{
|
||||||
|
QList<QTableWidgetSelectionRange> sel = m_card_a.table->selectedRanges();
|
||||||
|
Card* card = &m_card_a;
|
||||||
|
|
||||||
|
if (sel.isEmpty())
|
||||||
|
{
|
||||||
|
sel = m_card_b.table->selectedRanges();
|
||||||
|
card = &m_card_b;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel.isEmpty())
|
||||||
|
return std::tuple<Card*, const MemoryCardImage::FileInfo*>(nullptr, nullptr);
|
||||||
|
|
||||||
|
const int index = sel.front().topRow();
|
||||||
|
Assert(index >= 0 && static_cast<u32>(index) < card->files.size());
|
||||||
|
|
||||||
|
return std::tuple<Card*, const MemoryCardImage::FileInfo*>(card, &card->files[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemoryCardEditorDialog::updateButtonState()
|
||||||
|
{
|
||||||
|
const auto [selected_card, selected_file] = getSelectedFile();
|
||||||
|
const bool is_card_b = (selected_card == &m_card_b);
|
||||||
|
const bool has_selection = (selected_file != nullptr);
|
||||||
|
const bool card_a_present = !m_card_a.filename.empty();
|
||||||
|
const bool card_b_present = !m_card_b.filename.empty();
|
||||||
|
const bool both_cards_present = card_a_present && card_b_present;
|
||||||
|
m_ui.deleteFile->setEnabled(has_selection);
|
||||||
|
m_ui.exportFile->setEnabled(has_selection);
|
||||||
|
m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
|
||||||
|
m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);
|
||||||
|
m_ui.importCardA->setEnabled(card_a_present);
|
||||||
|
m_ui.importCardB->setEnabled(card_b_present);
|
||||||
|
}
|
63
src/duckstation-qt/memorycardeditordialog.h
Normal file
63
src/duckstation-qt/memorycardeditordialog.h
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#pragma once
|
||||||
|
#include "core/memory_card_image.h"
|
||||||
|
#include "ui_memorycardeditordialog.h"
|
||||||
|
#include <QtWidgets/QComboBox>
|
||||||
|
#include <QtWidgets/QDialog>
|
||||||
|
#include <QtWidgets/QLabel>
|
||||||
|
#include <QtWidgets/QPushButton>
|
||||||
|
#include <QtWidgets/QTableWidget>
|
||||||
|
|
||||||
|
class MemoryCardEditorDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
MemoryCardEditorDialog(QWidget* parent);
|
||||||
|
~MemoryCardEditorDialog();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* ev);
|
||||||
|
void closeEvent(QCloseEvent* ev);
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void onCardASelectionChanged();
|
||||||
|
void onCardBSelectionChanged();
|
||||||
|
void doCopyFile();
|
||||||
|
void doDeleteFile();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Card
|
||||||
|
{
|
||||||
|
std::string filename;
|
||||||
|
MemoryCardImage::DataArray data;
|
||||||
|
std::vector<MemoryCardImage::FileInfo> files;
|
||||||
|
u32 blocks_free = 0;
|
||||||
|
bool dirty = false;
|
||||||
|
|
||||||
|
QComboBox* path_cb = nullptr;
|
||||||
|
QTableWidget* table = nullptr;
|
||||||
|
QLabel* blocks_free_label = nullptr;
|
||||||
|
QPushButton* save_button = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
void connectUi();
|
||||||
|
void populateComboBox(QComboBox* cb);
|
||||||
|
void clearSelection();
|
||||||
|
void loadCardFromComboBox(Card* card, int index);
|
||||||
|
bool loadCard(const QString& filename, Card* card);
|
||||||
|
void updateCardTable(Card* card);
|
||||||
|
void updateCardBlocksFree(Card* card);
|
||||||
|
void setCardDirty(Card* card);
|
||||||
|
void newCard(Card* card);
|
||||||
|
void saveCard(Card* card);
|
||||||
|
void promptForSave(Card* card);
|
||||||
|
void importCard(Card* card);
|
||||||
|
|
||||||
|
std::tuple<Card*, const MemoryCardImage::FileInfo*> getSelectedFile();
|
||||||
|
void updateButtonState();
|
||||||
|
|
||||||
|
Ui::MemoryCardEditorDialog m_ui;
|
||||||
|
|
||||||
|
Card m_card_a;
|
||||||
|
Card m_card_b;
|
||||||
|
};
|
290
src/duckstation-qt/memorycardeditordialog.ui
Normal file
290
src/duckstation-qt/memorycardeditordialog.ui
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MemoryCardEditorDialog</class>
|
||||||
|
<widget class="QDialog" name="MemoryCardEditorDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>846</width>
|
||||||
|
<height>515</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Memory Card Editor</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QTableWidget" name="cardA">
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>16</width>
|
||||||
|
<height>16</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Title</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>File Name</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Blocks</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Memory Card:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="cardBPath">
|
||||||
|
<property name="currentText">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="newCardB">
|
||||||
|
<property name="text">
|
||||||
|
<string>New...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="3">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0,0,0">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="cardBUsage">
|
||||||
|
<property name="text">
|
||||||
|
<string>0 blocks used</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="importFileToCardB">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Import File...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="importCardB">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Import Card...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="saveCardB">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0,0,0">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="cardAUsage">
|
||||||
|
<property name="text">
|
||||||
|
<string>0 blocks used</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="importFileToCardA">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Import File...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="importCardA">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Import Card...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="saveCardA">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Save</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Memory Card:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="cardAPath"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="newCardA">
|
||||||
|
<property name="text">
|
||||||
|
<string>New...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="3">
|
||||||
|
<widget class="QTableWidget" name="cardB">
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize">
|
||||||
|
<size>
|
||||||
|
<width>16</width>
|
||||||
|
<height>16</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||||
|
<bool>true</bool>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="verticalHeaderVisible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</attribute>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Title</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>File Name</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Blocks</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="deleteFile">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete File</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="exportFile">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Export File</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="moveLeft">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string><<</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="moveRight">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>>></string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
Loading…
Reference in a new issue