From b05c7a51a77fc111b94a28b0fc9bbef96cd8f105 Mon Sep 17 00:00:00 2001 From: WizzardSK <42868978+WizzardSK@users.noreply.github.com> Date: Sat, 26 Sep 2026 10:45:33 +0200 Subject: [PATCH] Drop the wx GUI application and the standalone's GitHub workflows This repository builds the libretro core; the standalone wx application was built from it only by upstream's workflows. Gone: src/gui/wxgui, the CemuBin target with its macOS bundle and Windows resources, the wx/GTK and SDL vcpkg features, dist/linux and dist/windows, the GUI's translations in bin/resources (the Wii U shared fonts there stay - the core loads them), src/tools, CMakeSettings.json, and the Build Cemu, Build check, Deploy release, Calculate Next Version and Generate translation template workflows. ENABLE_LIBRETRO is now always on; it stays a variable because the rest of the build, shared with upstream, still asks for it. At the next upstream sync, a modify/delete conflict on any of these is resolved by keeping the file deleted. Built on all seven targets plus iOS and tvOS. --- .github/workflows/build.yml | 302 -- .github/workflows/build_check.yml | 18 - .github/workflows/deploy_release.yml | 164 -- .../workflows/determine_release_version.yml | 74 - .github/workflows/generate_pot.yml | 43 - .github/workflows/libretro.yml | 2 +- CMakeSettings.json | 28 - bin/resources/ar/‏‏cemu.mo | Bin 81849 -> 0 bytes bin/resources/ca/cemu.mo | Bin 56852 -> 0 bytes bin/resources/de/cemu.mo | Bin 69412 -> 0 bytes bin/resources/es/cemu.mo | Bin 68744 -> 0 bytes bin/resources/fr/cemu.mo | Bin 73978 -> 0 bytes bin/resources/he/cemu.mo | Bin 23202 -> 0 bytes bin/resources/hu/cemu.mo | Bin 72404 -> 0 bytes bin/resources/it/cemu.mo | Bin 71398 -> 0 bytes bin/resources/ja/cemu.mo | Bin 68243 -> 0 bytes bin/resources/ko/cemu.mo | Bin 71668 -> 0 bytes bin/resources/nb/cemu.mo | Bin 41128 -> 0 bytes bin/resources/nl/cemu.mo | Bin 54744 -> 0 bytes bin/resources/pl/cemu.mo | Bin 17105 -> 0 bytes bin/resources/pt/cemu.mo | Bin 61652 -> 0 bytes bin/resources/ru/cemu.mo | Bin 91888 -> 0 bytes bin/resources/sv/cemu.mo | Bin 67980 -> 0 bytes bin/resources/tr/cemu.mo | Bin 28711 -> 0 bytes bin/resources/uk/cemu.mo | Bin 58231 -> 0 bytes bin/resources/zh/cemu.mo | Bin 60704 -> 0 bytes dist/linux/appimage.sh | 59 - dist/linux/info.cemu.Cemu.desktop | 27 - dist/linux/info.cemu.Cemu.metainfo.xml | 84 - dist/linux/info.cemu.Cemu.png | Bin 11984 -> 0 bytes dist/windows/Cemu.manifest | 16 - src/gui/wxgui/AudioDebuggerWindow.cpp | 305 -- src/gui/wxgui/AudioDebuggerWindow.h | 30 - src/gui/wxgui/CMakeLists.txt | 198 -- src/gui/wxgui/CemuApp.cpp | 678 ----- src/gui/wxgui/CemuApp.h | 47 - src/gui/wxgui/CemuUpdateWindow.cpp | 861 ------ src/gui/wxgui/CemuUpdateWindow.h | 84 - src/gui/wxgui/ChecksumTool.cpp | 792 ------ src/gui/wxgui/ChecksumTool.h | 51 - .../wxgui/DownloadCustomGraphicPackWindow.cpp | 418 --- .../wxgui/DownloadCustomGraphicPackWindow.h | 66 - src/gui/wxgui/DownloadGraphicPacksWindow.cpp | 404 --- src/gui/wxgui/DownloadGraphicPacksWindow.h | 62 - .../EmulatedUSBDeviceFrame.cpp | 842 ------ .../EmulatedUSBDeviceFrame.h | 102 - src/gui/wxgui/GameProfileWindow.cpp | 405 --- src/gui/wxgui/GameProfileWindow.h | 55 - src/gui/wxgui/GameUpdateWindow.cpp | 339 --- src/gui/wxgui/GameUpdateWindow.h | 67 - src/gui/wxgui/GeneralSettings2.cpp | 2509 ---------------- src/gui/wxgui/GeneralSettings2.h | 143 - src/gui/wxgui/GettingStartedDialog.cpp | 283 -- src/gui/wxgui/GettingStartedDialog.h | 48 - src/gui/wxgui/GraphicPacksWindow2.cpp | 741 ----- src/gui/wxgui/GraphicPacksWindow2.h | 80 - src/gui/wxgui/LoggingWindow.cpp | 85 - src/gui/wxgui/LoggingWindow.h | 29 - src/gui/wxgui/MainWindow.cpp | 2516 ----------------- src/gui/wxgui/MainWindow.h | 248 -- src/gui/wxgui/MemorySearcherTool.cpp | 742 ----- src/gui/wxgui/MemorySearcherTool.h | 264 -- src/gui/wxgui/PadViewFrame.cpp | 242 -- src/gui/wxgui/PadViewFrame.h | 38 - src/gui/wxgui/TitleManager.cpp | 907 ------ src/gui/wxgui/TitleManager.h | 104 - src/gui/wxgui/canvas/IRenderCanvas.h | 14 - src/gui/wxgui/canvas/MetalCanvas.cpp | 61 - src/gui/wxgui/canvas/MetalCanvas.h | 19 - src/gui/wxgui/canvas/OpenGLCanvas.cpp | 179 -- src/gui/wxgui/canvas/OpenGLCanvas.h | 4 - src/gui/wxgui/canvas/VulkanCanvas.cpp | 85 - src/gui/wxgui/canvas/VulkanCanvas.h | 23 - src/gui/wxgui/components/TextList.cpp | 320 --- src/gui/wxgui/components/TextList.h | 73 - .../components/wxDownloadManagerList.cpp | 813 ------ .../wxgui/components/wxDownloadManagerList.h | 152 - src/gui/wxgui/components/wxGameList.cpp | 1696 ----------- src/gui/wxgui/components/wxGameList.h | 166 -- src/gui/wxgui/components/wxInputDraw.cpp | 121 - src/gui/wxgui/components/wxInputDraw.h | 24 - src/gui/wxgui/components/wxLogCtrl.cpp | 161 -- src/gui/wxgui/components/wxLogCtrl.h | 52 - .../components/wxProgressDialogManager.h | 118 - .../wxgui/components/wxTitleManagerList.cpp | 1333 --------- src/gui/wxgui/components/wxTitleManagerList.h | 139 - src/gui/wxgui/debugger/BreakpointWindow.cpp | 321 --- src/gui/wxgui/debugger/BreakpointWindow.h | 28 - src/gui/wxgui/debugger/DebuggerWindow2.cpp | 936 ------ src/gui/wxgui/debugger/DebuggerWindow2.h | 173 -- src/gui/wxgui/debugger/DisasmCtrl.cpp | 895 ------ src/gui/wxgui/debugger/DisasmCtrl.h | 60 - src/gui/wxgui/debugger/DumpCtrl.cpp | 441 --- src/gui/wxgui/debugger/DumpCtrl.h | 38 - src/gui/wxgui/debugger/DumpWindow.cpp | 46 - src/gui/wxgui/debugger/DumpWindow.h | 18 - src/gui/wxgui/debugger/ModuleWindow.cpp | 132 - src/gui/wxgui/debugger/ModuleWindow.h | 20 - src/gui/wxgui/debugger/RegisterWindow.cpp | 485 ---- src/gui/wxgui/debugger/RegisterWindow.h | 29 - src/gui/wxgui/debugger/SymbolCtrl.cpp | 138 - src/gui/wxgui/debugger/SymbolCtrl.h | 32 - src/gui/wxgui/debugger/SymbolWindow.cpp | 52 - src/gui/wxgui/debugger/SymbolWindow.h | 23 - .../CreateAccount/wxCreateAccountDialog.cpp | 101 - .../CreateAccount/wxCreateAccountDialog.h | 18 - .../dialogs/SaveImport/SaveImportWindow.cpp | 330 --- .../dialogs/SaveImport/SaveImportWindow.h | 25 - .../wxgui/dialogs/SaveImport/SaveTransfer.cpp | 198 -- .../wxgui/dialogs/SaveImport/SaveTransfer.h | 24 - src/gui/wxgui/helpers/wxControlObject.h | 44 - src/gui/wxgui/helpers/wxCustomData.h | 49 - src/gui/wxgui/helpers/wxCustomEvents.cpp | 7 - src/gui/wxgui/helpers/wxCustomEvents.h | 74 - src/gui/wxgui/helpers/wxHelpers.cpp | 116 - src/gui/wxgui/helpers/wxHelpers.h | 108 - src/gui/wxgui/helpers/wxLogEvent.h | 32 - src/gui/wxgui/helpers/wxWayland.cpp | 24 - src/gui/wxgui/helpers/wxWayland.h | 93 - src/gui/wxgui/input/HotkeySettings.cpp | 522 ---- src/gui/wxgui/input/HotkeySettings.h | 60 - src/gui/wxgui/input/InputAPIAddWindow.cpp | 326 --- src/gui/wxgui/input/InputAPIAddWindow.h | 62 - src/gui/wxgui/input/InputSettings2.cpp | 976 ------- src/gui/wxgui/input/InputSettings2.h | 73 - src/gui/wxgui/input/PairingDialog.cpp | 300 -- src/gui/wxgui/input/PairingDialog.h | 38 - .../panels/ClassicControllerInputPanel.cpp | 118 - .../panels/ClassicControllerInputPanel.h | 19 - src/gui/wxgui/input/panels/InputPanel.cpp | 285 -- src/gui/wxgui/input/panels/InputPanel.h | 48 - .../input/panels/ProControllerInputPanel.cpp | 131 - .../input/panels/ProControllerInputPanel.h | 19 - src/gui/wxgui/input/panels/VPADInputPanel.cpp | 206 -- src/gui/wxgui/input/panels/VPADInputPanel.h | 25 - .../wxgui/input/panels/WiimoteInputPanel.cpp | 260 -- .../wxgui/input/panels/WiimoteInputPanel.h | 44 - .../settings/DefaultControllerSettings.cpp | 318 --- .../settings/DefaultControllerSettings.h | 42 - .../settings/WiimoteControllerSettings.cpp | 369 --- .../settings/WiimoteControllerSettings.h | 61 - .../DebugPPCThreadsWindow.cpp | 466 --- .../PPCThreadsViewer/DebugPPCThreadsWindow.h | 37 - .../TextureRelationWindow.cpp | 377 --- .../TextureRelationWindow.h | 33 - src/gui/wxgui/wxCemuConfig.cpp | 190 -- src/gui/wxgui/wxCemuConfig.h | 154 - src/gui/wxgui/wxHelper.h | 39 - src/gui/wxgui/wxWindowSystem.cpp | 352 --- src/gui/wxgui/wxcomponents/checktree.cpp | 613 ---- src/gui/wxgui/wxcomponents/checktree.h | 105 - src/gui/wxgui/wxgui.h | 47 - src/resource/MacOSXBundleInfo.plist.in | 69 - src/resource/cemu.icns | Bin 817145 -> 0 bytes src/resource/cemu.macos.entitlements | 13 - src/resource/cemu.rc | 129 - src/tools/ShaderCacheMerger.cpp | 145 - 157 files changed, 1 insertion(+), 33335 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/build_check.yml delete mode 100644 .github/workflows/deploy_release.yml delete mode 100644 .github/workflows/determine_release_version.yml delete mode 100644 .github/workflows/generate_pot.yml delete mode 100644 CMakeSettings.json delete mode 100644 bin/resources/ar/‏‏cemu.mo delete mode 100644 bin/resources/ca/cemu.mo delete mode 100644 bin/resources/de/cemu.mo delete mode 100644 bin/resources/es/cemu.mo delete mode 100644 bin/resources/fr/cemu.mo delete mode 100644 bin/resources/he/cemu.mo delete mode 100644 bin/resources/hu/cemu.mo delete mode 100644 bin/resources/it/cemu.mo delete mode 100644 bin/resources/ja/cemu.mo delete mode 100644 bin/resources/ko/cemu.mo delete mode 100644 bin/resources/nb/cemu.mo delete mode 100644 bin/resources/nl/cemu.mo delete mode 100644 bin/resources/pl/cemu.mo delete mode 100644 bin/resources/pt/cemu.mo delete mode 100644 bin/resources/ru/cemu.mo delete mode 100644 bin/resources/sv/cemu.mo delete mode 100644 bin/resources/tr/cemu.mo delete mode 100644 bin/resources/uk/cemu.mo delete mode 100644 bin/resources/zh/cemu.mo delete mode 100755 dist/linux/appimage.sh delete mode 100644 dist/linux/info.cemu.Cemu.desktop delete mode 100644 dist/linux/info.cemu.Cemu.metainfo.xml delete mode 100644 dist/linux/info.cemu.Cemu.png delete mode 100644 dist/windows/Cemu.manifest delete mode 100644 src/gui/wxgui/AudioDebuggerWindow.cpp delete mode 100644 src/gui/wxgui/AudioDebuggerWindow.h delete mode 100644 src/gui/wxgui/CMakeLists.txt delete mode 100644 src/gui/wxgui/CemuApp.cpp delete mode 100644 src/gui/wxgui/CemuApp.h delete mode 100644 src/gui/wxgui/CemuUpdateWindow.cpp delete mode 100644 src/gui/wxgui/CemuUpdateWindow.h delete mode 100644 src/gui/wxgui/ChecksumTool.cpp delete mode 100644 src/gui/wxgui/ChecksumTool.h delete mode 100644 src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp delete mode 100644 src/gui/wxgui/DownloadCustomGraphicPackWindow.h delete mode 100644 src/gui/wxgui/DownloadGraphicPacksWindow.cpp delete mode 100644 src/gui/wxgui/DownloadGraphicPacksWindow.h delete mode 100644 src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp delete mode 100644 src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h delete mode 100644 src/gui/wxgui/GameProfileWindow.cpp delete mode 100644 src/gui/wxgui/GameProfileWindow.h delete mode 100644 src/gui/wxgui/GameUpdateWindow.cpp delete mode 100644 src/gui/wxgui/GameUpdateWindow.h delete mode 100644 src/gui/wxgui/GeneralSettings2.cpp delete mode 100644 src/gui/wxgui/GeneralSettings2.h delete mode 100644 src/gui/wxgui/GettingStartedDialog.cpp delete mode 100644 src/gui/wxgui/GettingStartedDialog.h delete mode 100644 src/gui/wxgui/GraphicPacksWindow2.cpp delete mode 100644 src/gui/wxgui/GraphicPacksWindow2.h delete mode 100644 src/gui/wxgui/LoggingWindow.cpp delete mode 100644 src/gui/wxgui/LoggingWindow.h delete mode 100644 src/gui/wxgui/MainWindow.cpp delete mode 100644 src/gui/wxgui/MainWindow.h delete mode 100644 src/gui/wxgui/MemorySearcherTool.cpp delete mode 100644 src/gui/wxgui/MemorySearcherTool.h delete mode 100644 src/gui/wxgui/PadViewFrame.cpp delete mode 100644 src/gui/wxgui/PadViewFrame.h delete mode 100644 src/gui/wxgui/TitleManager.cpp delete mode 100644 src/gui/wxgui/TitleManager.h delete mode 100644 src/gui/wxgui/canvas/IRenderCanvas.h delete mode 100644 src/gui/wxgui/canvas/MetalCanvas.cpp delete mode 100644 src/gui/wxgui/canvas/MetalCanvas.h delete mode 100644 src/gui/wxgui/canvas/OpenGLCanvas.cpp delete mode 100644 src/gui/wxgui/canvas/OpenGLCanvas.h delete mode 100644 src/gui/wxgui/canvas/VulkanCanvas.cpp delete mode 100644 src/gui/wxgui/canvas/VulkanCanvas.h delete mode 100644 src/gui/wxgui/components/TextList.cpp delete mode 100644 src/gui/wxgui/components/TextList.h delete mode 100644 src/gui/wxgui/components/wxDownloadManagerList.cpp delete mode 100644 src/gui/wxgui/components/wxDownloadManagerList.h delete mode 100644 src/gui/wxgui/components/wxGameList.cpp delete mode 100644 src/gui/wxgui/components/wxGameList.h delete mode 100644 src/gui/wxgui/components/wxInputDraw.cpp delete mode 100644 src/gui/wxgui/components/wxInputDraw.h delete mode 100644 src/gui/wxgui/components/wxLogCtrl.cpp delete mode 100644 src/gui/wxgui/components/wxLogCtrl.h delete mode 100644 src/gui/wxgui/components/wxProgressDialogManager.h delete mode 100644 src/gui/wxgui/components/wxTitleManagerList.cpp delete mode 100644 src/gui/wxgui/components/wxTitleManagerList.h delete mode 100644 src/gui/wxgui/debugger/BreakpointWindow.cpp delete mode 100644 src/gui/wxgui/debugger/BreakpointWindow.h delete mode 100644 src/gui/wxgui/debugger/DebuggerWindow2.cpp delete mode 100644 src/gui/wxgui/debugger/DebuggerWindow2.h delete mode 100644 src/gui/wxgui/debugger/DisasmCtrl.cpp delete mode 100644 src/gui/wxgui/debugger/DisasmCtrl.h delete mode 100644 src/gui/wxgui/debugger/DumpCtrl.cpp delete mode 100644 src/gui/wxgui/debugger/DumpCtrl.h delete mode 100644 src/gui/wxgui/debugger/DumpWindow.cpp delete mode 100644 src/gui/wxgui/debugger/DumpWindow.h delete mode 100644 src/gui/wxgui/debugger/ModuleWindow.cpp delete mode 100644 src/gui/wxgui/debugger/ModuleWindow.h delete mode 100644 src/gui/wxgui/debugger/RegisterWindow.cpp delete mode 100644 src/gui/wxgui/debugger/RegisterWindow.h delete mode 100644 src/gui/wxgui/debugger/SymbolCtrl.cpp delete mode 100644 src/gui/wxgui/debugger/SymbolCtrl.h delete mode 100644 src/gui/wxgui/debugger/SymbolWindow.cpp delete mode 100644 src/gui/wxgui/debugger/SymbolWindow.h delete mode 100644 src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.cpp delete mode 100644 src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h delete mode 100644 src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.cpp delete mode 100644 src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.h delete mode 100644 src/gui/wxgui/dialogs/SaveImport/SaveTransfer.cpp delete mode 100644 src/gui/wxgui/dialogs/SaveImport/SaveTransfer.h delete mode 100644 src/gui/wxgui/helpers/wxControlObject.h delete mode 100644 src/gui/wxgui/helpers/wxCustomData.h delete mode 100644 src/gui/wxgui/helpers/wxCustomEvents.cpp delete mode 100644 src/gui/wxgui/helpers/wxCustomEvents.h delete mode 100644 src/gui/wxgui/helpers/wxHelpers.cpp delete mode 100644 src/gui/wxgui/helpers/wxHelpers.h delete mode 100644 src/gui/wxgui/helpers/wxLogEvent.h delete mode 100644 src/gui/wxgui/helpers/wxWayland.cpp delete mode 100644 src/gui/wxgui/helpers/wxWayland.h delete mode 100644 src/gui/wxgui/input/HotkeySettings.cpp delete mode 100644 src/gui/wxgui/input/HotkeySettings.h delete mode 100644 src/gui/wxgui/input/InputAPIAddWindow.cpp delete mode 100644 src/gui/wxgui/input/InputAPIAddWindow.h delete mode 100644 src/gui/wxgui/input/InputSettings2.cpp delete mode 100644 src/gui/wxgui/input/InputSettings2.h delete mode 100644 src/gui/wxgui/input/PairingDialog.cpp delete mode 100644 src/gui/wxgui/input/PairingDialog.h delete mode 100644 src/gui/wxgui/input/panels/ClassicControllerInputPanel.cpp delete mode 100644 src/gui/wxgui/input/panels/ClassicControllerInputPanel.h delete mode 100644 src/gui/wxgui/input/panels/InputPanel.cpp delete mode 100644 src/gui/wxgui/input/panels/InputPanel.h delete mode 100644 src/gui/wxgui/input/panels/ProControllerInputPanel.cpp delete mode 100644 src/gui/wxgui/input/panels/ProControllerInputPanel.h delete mode 100644 src/gui/wxgui/input/panels/VPADInputPanel.cpp delete mode 100644 src/gui/wxgui/input/panels/VPADInputPanel.h delete mode 100644 src/gui/wxgui/input/panels/WiimoteInputPanel.cpp delete mode 100644 src/gui/wxgui/input/panels/WiimoteInputPanel.h delete mode 100644 src/gui/wxgui/input/settings/DefaultControllerSettings.cpp delete mode 100644 src/gui/wxgui/input/settings/DefaultControllerSettings.h delete mode 100644 src/gui/wxgui/input/settings/WiimoteControllerSettings.cpp delete mode 100644 src/gui/wxgui/input/settings/WiimoteControllerSettings.h delete mode 100644 src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp delete mode 100644 src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.h delete mode 100644 src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.cpp delete mode 100644 src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.h delete mode 100644 src/gui/wxgui/wxCemuConfig.cpp delete mode 100644 src/gui/wxgui/wxCemuConfig.h delete mode 100644 src/gui/wxgui/wxHelper.h delete mode 100644 src/gui/wxgui/wxWindowSystem.cpp delete mode 100644 src/gui/wxgui/wxcomponents/checktree.cpp delete mode 100644 src/gui/wxgui/wxcomponents/checktree.h delete mode 100644 src/gui/wxgui/wxgui.h delete mode 100644 src/resource/MacOSXBundleInfo.plist.in delete mode 100644 src/resource/cemu.icns delete mode 100644 src/resource/cemu.macos.entitlements delete mode 100644 src/resource/cemu.rc delete mode 100644 src/tools/ShaderCacheMerger.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 0aa9a7f2..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,302 +0,0 @@ -name: Build Cemu - -on: - workflow_call: - inputs: - next_version_major: - required: false - type: string - next_version_minor: - required: false - type: string - -env: - VCPKG_ROOT: "${{github.workspace}}/dependencies/vcpkg" - # No nuget binary cache: this fork does not publish packages, and the one it - # had was 537 vcpkg artifacts nothing here reads - the libretro core build in - # libretro.yml does not use it. - VCPKG_BINARY_SOURCES: 'clear' - VCPKG_FORCE_DOWNLOADED_BINARIES: true - -jobs: - build-ubuntu: - continue-on-error: true - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-22.04 - arch: x64 - - os: ubuntu-22.04-arm - arch: arm - - name: build-ubuntu-${{ matrix.arch }} - runs-on: ${{ matrix.os }} - steps: - - name: "Checkout repo" - uses: actions/checkout@v6 - with: - submodules: "recursive" - fetch-depth: 0 - - - name: Setup release mode parameters - run: | - echo "BUILD_MODE=release" >> $GITHUB_ENV - echo "BUILD_FLAGS=" >> $GITHUB_ENV - echo "Build mode is release" - - - name: Setup build flags for version - if: ${{ inputs.next_version_major != '' }} - run: | - echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" >> $GITHUB_ENV - - - name: "Install system dependencies" - run: | - sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev nasm ninja-build libbluetooth-dev - - - name: "Bootstrap vcpkg" - run: | - bash ./dependencies/vcpkg/bootstrap-vcpkg.sh - - - name: 'Setup NuGet Credentials for vcpkg' - shell: 'bash' - run: | - mono `./dependencies/vcpkg/vcpkg fetch nuget | tail -n 1` \ - sources add \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - -storepasswordincleartext \ - -name "GitHub" \ - -username "${{ github.repository_owner }}" \ - -password "${{ secrets.GITHUB_TOKEN }}" - mono `./dependencies/vcpkg/vcpkg fetch nuget | tail -n 1` \ - setapikey "${{ secrets.GITHUB_TOKEN }}" \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - - - name: "cmake" - run: | - cmake -S . -B build ${{ env.BUILD_FLAGS }} -DCMAKE_BUILD_TYPE=${{ env.BUILD_MODE }} -DCMAKE_C_COMPILER=/usr/bin/clang-15 -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 -G Ninja -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja - - - name: "Build Cemu" - run: | - cmake --build build - - - name: Prepare artifact - run: mv bin/Cemu_release bin/Cemu - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: cemu-bin-linux-${{ matrix.arch }} - path: ./bin/Cemu - - build-appimage: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-22.04 - arch: x64 - - os: ubuntu-22.04-arm - arch: arm - - name: build-appimage-${{ matrix.arch }} - runs-on: ${{ matrix.os }} - needs: build-ubuntu - steps: - - name: Checkout Upstream Repo - uses: actions/checkout@v6 - - - uses: actions/download-artifact@v8 - with: - name: cemu-bin-linux-${{ matrix.arch }} - path: bin - - - name: "Install system dependencies" - run: | - sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev nasm ninja-build appstream libbluetooth-dev - - - name: "Build AppImage" - run: | - export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH" - export DEPLOY_GTK_VERSION=3 - dist/linux/appimage.sh ${{ runner.arch }} - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: cemu-appimage-${{ matrix.arch }} - path: artifacts - - build-windows: - runs-on: windows-2022 - steps: - - name: "Checkout repo" - uses: actions/checkout@v6 - with: - submodules: "recursive" - - - name: Setup release mode parameters - run: | - echo "BUILD_MODE=release" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - echo "BUILD_FLAGS=" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - echo "Build mode is release" - - - name: Setup build flags for version - if: ${{ inputs.next_version_major != '' }} - run: | - echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - - - name: "Setup cmake" - uses: jwlawson/actions-setup-cmake@v2 - with: - cmake-version: '3.29.0' - - - name: "Bootstrap vcpkg" - run: | - ./dependencies/vcpkg/bootstrap-vcpkg.bat - - - name: 'Setup NuGet Credentials for vcpkg' - shell: 'bash' - run: | - `./dependencies/vcpkg/vcpkg.exe fetch nuget | tail -n 1` \ - sources add \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - -storepasswordincleartext \ - -name "GitHub" \ - -username "${{ github.repository_owner }}" \ - -password "${{ secrets.GITHUB_TOKEN }}" - `./dependencies/vcpkg/vcpkg.exe fetch nuget | tail -n 1` \ - setapikey "${{ secrets.GITHUB_TOKEN }}" \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - - - name: "cmake" - run: | - mkdir -p build - cd build - echo "[INFO] BUILD_FLAGS: ${{ env.BUILD_FLAGS }}" - echo "[INFO] BUILD_MODE: ${{ env.BUILD_MODE }}" - cmake .. ${{ env.BUILD_FLAGS }} -DCMAKE_BUILD_TYPE=${{ env.BUILD_MODE }} -DVCPKG_INSTALL_OPTIONS="--clean-after-build" - - - name: "Build Cemu" - run: | - cd build - cmake --build . --config ${{ env.BUILD_MODE }} - - - name: Prepare artifact - run: Rename-Item bin/Cemu_release.exe Cemu.exe - - - name: Build NSIS Installer - shell: cmd - run: | - cd src\resource - makensis /DPRODUCT_VERSION=${{ inputs.next_version_major }}.${{ inputs.next_version_minor }} installer.nsi - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: cemu-bin-windows-x64 - path: ./bin/Cemu.exe - - - name: Upload NSIS Installer - uses: actions/upload-artifact@v6 - with: - name: cemu-installer-windows-x64 - path: ./src/resource/cemu-${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}-windows-x64-installer.exe - - build-macos: - runs-on: macos-14 - strategy: - fail-fast: false - matrix: - arch: [x86_64, arm64] - steps: - - name: "Checkout repo" - uses: actions/checkout@v6 - with: - submodules: "recursive" - - - name: Setup release mode parameters - run: | - echo "BUILD_MODE=release" >> $GITHUB_ENV - echo "BUILD_FLAGS=" >> $GITHUB_ENV - echo "Build mode is release" - - - name: Setup build flags for version - if: ${{ inputs.next_version_major != '' }} - run: | - echo "[INFO] Version ${{ inputs.next_version_major }}.${{ inputs.next_version_minor }}" - echo "BUILD_FLAGS=${{ env.BUILD_FLAGS }} -DEMULATOR_VERSION_MAJOR=${{ inputs.next_version_major }} -DEMULATOR_VERSION_MINOR=${{ inputs.next_version_minor }}" >> $GITHUB_ENV - - - name: "Install system dependencies" - run: | - # Removing azure/bicep and aws to silence warnings in Github Actions - brew uninstall bicep - brew untap aws/tap azure/bicep - brew update - brew install nasm automake libtool - - - name: "Install molten-vk" - run: | - curl -L -O https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.1/MoltenVK-macos-privateapi.tar - tar xf MoltenVK-macos-privateapi.tar - sudo mkdir -p /usr/local/lib - sudo cp MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib /usr/local/lib - - - name: "Setup cmake" - uses: jwlawson/actions-setup-cmake@v2 - with: - cmake-version: '3.29.0' - - - name: "Bootstrap vcpkg" - run: | - bash ./dependencies/vcpkg/bootstrap-vcpkg.sh - - - name: 'Setup NuGet Credentials for vcpkg' - shell: 'bash' - run: | - mono `./dependencies/vcpkg/vcpkg fetch nuget | tail -n 1` \ - sources add \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ - -storepasswordincleartext \ - -name "GitHub" \ - -username "${{ github.repository_owner }}" \ - -password "${{ secrets.GITHUB_TOKEN }}" - mono `./dependencies/vcpkg/vcpkg fetch nuget | tail -n 1` \ - setapikey "${{ secrets.GITHUB_TOKEN }}" \ - -source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - - - name: "cmake" - run: | - mkdir build - cd build - cmake .. ${{ env.BUILD_FLAGS }} \ - -DCMAKE_BUILD_TYPE=${{ env.BUILD_MODE }} \ - -DCMAKE_OSX_ARCHITECTURES=${{ matrix.arch }} \ - -DMACOS_BUNDLE=ON \ - -G Ninja - - - name: "Build Cemu" - run: | - cmake --build build - - - name: Prepare artifact - run: | - mkdir bin/Cemu_app - mv bin/Cemu_release.app bin/Cemu_app/Cemu.app - mv bin/Cemu_app/Cemu.app/Contents/MacOS/Cemu_release bin/Cemu_app/Cemu.app/Contents/MacOS/Cemu - sed -i '' 's/Cemu_release/Cemu/g' bin/Cemu_app/Cemu.app/Contents/Info.plist - codesign --entitlements ${{github.workspace}}/src/resource/cemu.macos.entitlements --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - --timestamp --options runtime bin/Cemu_app/Cemu.app/Contents/MacOS/Cemu - ln -s /Applications bin/Cemu_app/Applications - hdiutil create ./bin/tmp.dmg -ov -volname "Cemu" -fs HFS+ -srcfolder "./bin/Cemu_app" - hdiutil convert ./bin/tmp.dmg -format UDZO -o bin/Cemu.dmg - rm bin/tmp.dmg - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: cemu-bin-macos-${{ matrix.arch }} - path: ./bin/Cemu.dmg diff --git a/.github/workflows/build_check.yml b/.github/workflows/build_check.yml deleted file mode 100644 index 5d24b0c6..00000000 --- a/.github/workflows/build_check.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Build check -on: - pull_request: - paths-ignore: - - "*.md" - types: - - opened - - synchronize - - reopened - push: - paths-ignore: - - "*.md" - branches: - - main - -jobs: - build: - uses: ./.github/workflows/build.yml diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml deleted file mode 100644 index caa4a57a..00000000 --- a/.github/workflows/deploy_release.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: Deploy release -on: - workflow_dispatch: - inputs: - changelog0: - description: 'Enter the changelog lines for this release. Each line is a feature / bullet point. Do not use dash.' - required: true - type: string - changelog1: - description: 'Feature 2' - required: false - type: string - changelog2: - description: 'Feature 3' - required: false - type: string - changelog3: - description: 'Feature 4' - required: false - type: string - changelog4: - description: 'Feature 5' - required: false - type: string - changelog5: - description: 'Feature 6' - required: false - type: string - changelog6: - description: 'Feature 7' - required: false - type: string - changelog7: - description: 'Feature 8' - required: false - type: string - changelog8: - description: 'Feature 9' - required: false - type: string - changelog9: - description: 'Feature 10' - required: false - type: string - -jobs: - calculate-version: - name: Calculate Version - uses: ./.github/workflows/determine_release_version.yml - call-release-build: - uses: ./.github/workflows/build.yml - needs: calculate-version - with: - next_version_major: ${{ needs.calculate-version.outputs.next_version_major }} - next_version_minor: ${{ needs.calculate-version.outputs.next_version_minor }} - deploy: - name: Deploy release - runs-on: ubuntu-22.04 - needs: [call-release-build, calculate-version] - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Generate changelog - id: generate_changelog - run: | - CHANGELOG="" - if [ -n "${{ github.event.inputs.changelog0 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog0 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog1 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog1 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog2 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog2 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog3 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog3 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog4 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog4 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog5 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog5 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog6 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog6 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog7 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog7 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog8 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog8 }}\n"; fi - if [ -n "${{ github.event.inputs.changelog9 }}" ]; then CHANGELOG="$CHANGELOG- ${{ github.event.inputs.changelog9 }}\n"; fi - echo -e "$CHANGELOG" - echo "RELEASE_BODY=$CHANGELOG" >> $GITHUB_ENV - - uses: actions/download-artifact@v8 - with: - name: cemu-bin-linux-x64 - path: cemu-bin-linux-x64 - - - uses: actions/download-artifact@v8 - with: - name: cemu-appimage-x64 - path: cemu-appimage-x64 - - - uses: actions/download-artifact@v8 - with: - name: cemu-bin-windows-x64 - path: cemu-bin-windows-x64 - - - uses: actions/download-artifact@v8 - with: - name: cemu-installer-windows-x64 - path: cemu-installer-windows-x64 - - - uses: actions/download-artifact@v8 - with: - pattern: cemu-bin-macos* - path: cemu-macos - - - name: Initialize - run: | - mkdir upload - sudo apt install zip - - - name: Set version dependent vars - run: | - echo "Version: ${{ needs.calculate-version.outputs.next_version }}" - echo "CEMU_FOLDER_NAME=Cemu_${{ needs.calculate-version.outputs.next_version }}" - echo "CEMU_VERSION=${{ needs.calculate-version.outputs.next_version }}" - echo "CEMU_FOLDER_NAME=Cemu_${{ needs.calculate-version.outputs.next_version }}" >> $GITHUB_ENV - echo "CEMU_VERSION=${{ needs.calculate-version.outputs.next_version }}" >> $GITHUB_ENV - - - name: Create release from windows-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-windows-x64/Cemu.exe ./${{ env.CEMU_FOLDER_NAME }}/Cemu.exe - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-windows-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create release from windows-installer - run: cp cemu-installer-windows-x64/cemu-${{ env.CEMU_VERSION }}-windows-x64-installer.exe upload/cemu-${{ env.CEMU_VERSION }}-windows-x64-installer.exe - - - name: Create appimage - run: | - VERSION=${{ env.CEMU_VERSION }} - echo "Cemu Version is $VERSION" - ls cemu-appimage-x64 - mv cemu-appimage-x64/Cemu-*-x86_64.AppImage upload/Cemu-$VERSION-x86_64.AppImage - - - name: Create release from linux-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-linux-x64/Cemu ./${{ env.CEMU_FOLDER_NAME }}/Cemu - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-ubuntu-22.04-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create release from macos-bin - run: | - cd cemu-macos - for bin_dir in cemu-bin-macos-*; do - arch="${bin_dir##cemu-bin-macos-}" - cp $bin_dir/Cemu.dmg ../upload/cemu-${{ env.CEMU_VERSION }}-macos-12-$arch.dmg - done - - - name: Create release - run: | - wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.15.0/ghr_v0.15.0_linux_amd64.tar.gz - tar xvzf ghr.tar.gz; rm ghr.tar.gz - echo "[INFO] Release tag: v${{ env.CEMU_VERSION }}" - CHANGELOG_UNESCAPED=$(printf "%s\n" "${{ env.RELEASE_BODY }}" | sed 's/\\n/\n/g') - RELEASE_BODY=$(printf "%s\n%s" \ - "**Changelog:**" \ - "$CHANGELOG_UNESCAPED") - ghr_v0.15.0_linux_amd64/ghr -draft -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }}" -b "$RELEASE_BODY" "v${{ env.CEMU_VERSION }}" ./upload diff --git a/.github/workflows/determine_release_version.yml b/.github/workflows/determine_release_version.yml deleted file mode 100644 index a2efcd17..00000000 --- a/.github/workflows/determine_release_version.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Calculate Next Version from release history - -on: - workflow_dispatch: - workflow_call: - outputs: - next_version: - description: "The next semantic version" - value: ${{ jobs.calculate-version.outputs.next_version }} - next_version_major: - description: "The next semantic version (major)" - value: ${{ jobs.calculate-version.outputs.next_version_major }} - next_version_minor: - description: "The next semantic version (minor)" - value: ${{ jobs.calculate-version.outputs.next_version_minor }} - -jobs: - calculate-version: - runs-on: ubuntu-latest - outputs: - next_version: ${{ steps.calculate_next_version.outputs.next_version }} - next_version_major: ${{ steps.calculate_next_version.outputs.next_version_major }} - next_version_minor: ${{ steps.calculate_next_version.outputs.next_version_minor }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Get all releases - id: get_all_releases - run: | - # Fetch all releases and check for API errors - RESPONSE=$(curl -s -o response.json -w "%{http_code}" "https://api.github.com/repos/${{ github.repository }}/releases?per_page=100") - if [ "$RESPONSE" -ne 200 ]; then - echo "Failed to fetch releases. HTTP status: $RESPONSE" - cat response.json - exit 1 - fi - - # Extract and sort tags - ALL_TAGS=$(jq -r '.[].tag_name' response.json | grep -E '^v[0-9]+\.[0-9]+(-[0-9]+)?$' | sed 's/-.*//' | sort -V | tail -n 1) - - # Exit if no tags were found - if [ -z "$ALL_TAGS" ]; then - echo "No valid tags found." - exit 1 - fi - - echo "::set-output name=tag::$ALL_TAGS" - # echo "tag=$ALL_TAGS" >> $GITHUB_STATE - - - name: Calculate next semver minor - id: calculate_next_version - run: | - LATEST_VERSION=${{ steps.get_all_releases.outputs.tag }} - - # strip 'v' prefix and split into major.minor - LATEST_VERSION=${LATEST_VERSION//v/} - IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_VERSION" - - MAJOR=${VERSION_PARTS[0]} - MINOR=${VERSION_PARTS[1]} - - # increment the minor version - MINOR=$((MINOR + 1)) - - NEXT_VERSION="${MAJOR}.${MINOR}" - - echo "Major: $MAJOR" - echo "Minor: $MINOR" - - echo "Next version: $NEXT_VERSION" - echo "::set-output name=next_version::$NEXT_VERSION" - echo "::set-output name=next_version_major::$MAJOR" - echo "::set-output name=next_version_minor::$MINOR" \ No newline at end of file diff --git a/.github/workflows/generate_pot.yml b/.github/workflows/generate_pot.yml deleted file mode 100644 index f8f1864c..00000000 --- a/.github/workflows/generate_pot.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Generate translation template - -on: - pull_request: - paths-ignore: - - "*.md" - types: - - opened - - synchronize - - reopened - push: - paths-ignore: - - "*.md" - branches: - - main - -jobs: - generate-pot: - runs-on: ubuntu-latest - steps: - - name: "Checkout repo" - uses: actions/checkout@v6 - - - name: "Install gettext" - run: | - sudo apt update -qq - sudo apt install -y gettext - - - name: "Generate POT file using xgettext" - run: > - find src -name *.cpp -o -name *.hpp -o -name *.h | - xargs xgettext --from-code=utf-8 -w 100 - --keyword="_" --keyword="wxTRANSLATE" --keyword="wxPLURAL:1,2" - --keyword="_tr" --keyword="TR_NOOP" - --check=space-ellipsis --omit-header - -o cemu.pot - - - name: Upload artifact - uses: actions/upload-artifact@v6 - with: - name: POT file - path: ./cemu.pot - if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml index 79be11fb..bad1c20a 100644 --- a/.github/workflows/libretro.yml +++ b/.github/workflows/libretro.yml @@ -101,7 +101,7 @@ jobs: - name: "Install molten-vk" # The Vulkan renderer links against the loader; on macOS that is - # MoltenVK. Same release the standalone macOS build in build.yml uses. + # MoltenVK, the release upstream's macOS build uses. run: | curl -L -O https://github.com/KhronosGroup/MoltenVK/releases/download/v1.4.1/MoltenVK-macos-privateapi.tar tar xf MoltenVK-macos-privateapi.tar diff --git a/CMakeSettings.json b/CMakeSettings.json deleted file mode 100644 index 0927e98b..00000000 --- a/CMakeSettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "configurations": [ - { - "name": "RelWithDebInfo", - "configurationType": "RelWithDebInfo", - "generator": "Ninja", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}" - }, - { - "name": "Release", - "configurationType": "Release", - "generator": "Ninja", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}" - }, - { - "name": "Debug", - "configurationType": "Debug", - "generator": "Ninja", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}" - } - ] -} \ No newline at end of file diff --git a/bin/resources/ar/‏‏cemu.mo b/bin/resources/ar/‏‏cemu.mo deleted file mode 100644 index 4062628be1486a0ffe6a0d2731c6853e4b133880..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81849 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 Vgzj&`LH}xLZ2dy=?u{P|{|}7B5uN}5 diff --git a/bin/resources/ca/cemu.mo b/bin/resources/ca/cemu.mo deleted file mode 100644 index 556402151e31bb69c95fd2711e167c150a65009c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56852 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 nQ*LLUZkx5IixrT z3o0Utf+!%0fQk#2MNv^v+!aw=ao{UP99glB=O&mvIQ ztprux>JUC3)P3@x+BpiUedD0+_o|To4p8O27gRew4ywG*gR1YNpsxE4cnJ7QQ1{(s zf!}XmQ1@vCH4cY?N{Q^||{|c&~cI@){#Gv}I4OBTtfGYnua3639xHtIHkiIV9#h|WxRR~`V z>VDUO>ff6{wdZzF{q;#u`5ysQ-q%32>lffb;Ip9W{lbMF-wvvM^FWo`3#$Ll2UXq> zsCKLc)nBg$w*ub*D*p|j^4|(7-$%d$!G}QQ|0y^R{42N-JZ4c8?FZfks{BVm_1h0X zmHRZ<3;qsNJ-Zfr`DcJv5c7L5c)q!y;+KOoF}fOrRHMhi6Ty9!dOLf; zHxe#@+knr3Q^6^xI_>~&O?WTR`UzD3wu9=wx!}>@*`V@W4eI(ggRp|=22ky}7u*hf z2;3fg6jc9wE8x?h^8E`GJ?(Uw-)BFth46H6TW~h0emM?QeNF^*zm=f)&sib77VIE= zF{u8(15`gh3ThmF0g4{}4vOA(T^8sORJlih$~PZ;5!eGB1HK#-ecS@-x=(@M0Urk+ z1n*ex{ajx`-w?hBd>QyhFb2WknC8+wo0aX9r7UCZQmG4pTSnwO*ao~=tJboc4K6@Ida)vrv}V{%C{a= zxfg?K*Og!!cpa#D`8iN@_AsdOz78t?_rd+ZpMd*-&x7jEz0UT0hk?5iJ`$V_c7bZg zT5u5j2v`L7JIC`~5%8_x!NlJJs-0g34+Vb!svg_C1fC1-25R2U1l7+=LDBE|p!#8B zh`$CyO3ei2F>bc{&o_`vs{+S2%gA2h$;0Hj_>F+`H%Up;-`Idod_bO2LErR=k zmx3zy9iZsw!y)|vQ2qQ}@DlLHU;!LB-{T(zRo*v2jnD5vm9yPTz1#ypwdZJ1{k#NJ zJzffGJ`92ffG-CR2VW1)0Y4GKzX3(x`{fu5unX(}*MrA^H-j_4AA(1L+xPkPCxOQj z9s^bGEuhBz8v(xuiavfE!aoPqAHN0FUt80ey5HX5{@|&g${7TA0Ivbnu6KcI@13CL z@k8KN;5Wc_@Y|rS-*$kp1zSP2=Th*+;BBD#?Q7sn@E4%kvsd2v-8^tQ;qyTC^X1@N z@CL9I{4V$+@Of|=xIe_HcAgZl3ltrm2C9DNg!lre{9_?}4XAOr4m<(8JH-Dn;NL;D zI~wwS+YvN621PG3LG{NeA-xCG_?`()1^YvI6cj&Q4{iru4$|c4b>Oz(A4B}#LDBt` zVV_UCgX*ti!9&1vK;^##oDN3?N&-Uebm4%{Dn z0PF>~2lsoKU)Kt%+#^8ElX)TjOz;JS^WfItFnAj{3aVcFk2oE*f*Sv$K+%6UX!*e% zlpj?6#zFPd72wX`)u8HmLx{f(R6XtiTfm1w(dlEL#^EVYTxnCI$8{B{7wZm&(8xjj_W~{ z|2k0hd~b-q1w5JXr$F`7Kfr^*mTDAT03HD<{)3?I^Knr3`5dVE{UE6MKc(jNeKDx^ z9ttYo5umQ01B&0C0-gevL5<^w!9Bq*fU55g!9Bpg1>CXj;}(O8KLXqhTmb6&)u8HI z0(IXvfqQ}P2;n=y9SMIN)Ob7;!jFLm5`F^Ic>EL8xJ_N_?b#JPlJFc*{WS<`oJK(P zLmkvOT^PdO1zA#~pMV;tQ9vEKRbY*6`Dfa>4XU<|$t)VRF{RKGkN@Ef4|;Ym>U z`8BBa{~7E8+s5g0a1>PfouJ0`WRkg6f|Qpze1usOzo-_W^GL)z14smGd=Fbnz6Z>!(7@t>8YO z%AE_ggQtS~f$PB+gV%tf=exnf!Eb}gxAiMrPM8B8OSk~e0dE95!Eb=dzxPHTzxkl* zl>-k0$H7_PO`!PD<01Yj@P&kTxzPFKVW8@{3RL~;A^dhw{rM?SPf0<%FLF=YdPE@^Y^NI|)As zz8>7}RW2XC1r!}`0v`jP0}lcpeYNW;zXgvW9KXit{0#7YgkKFF%l&7)*6DNjb>6<$ zfhUlDD=5198K{2$3%C>bPf+FWNF@#dXM&=uRiNmlA5?#C0CnBv;8gJSU>o?h5Pv_Y zes~1b^;^He>1;1>E5a`XMaKt%w}3~1^T8j08i)95@0XK6h9=qoGGx&|L5=q%Z}f7n z0!1I!fufHOg44l!LD9(*pziZ0Q1zaA4ZIfI6I6VEz;SSA!fyb3z;}U%gMS2d-GOg% zzC8m}crmyQxC&%SM`wocw?NhZ2cY`-Sy26Y%(dQsU7-5w9PnT;4{is(8dSTk0o6a( z1-u^In(&RF^1UCt9=shq2VC}MZ~rZz+Vcre8_{E6+n3qq3`w)JP!PNcsc$fFXVc;HwPYKu?a5&&AK-K4J z@P*(zL6v(?z%PNS*OQ>i`5CDE&w{GQwl_Ha?E`B3r-2%WBSGa~3aVb`g34b9b^R5f z==K^={r65#_x&KK{`)Mbet8Jg_1_5iYf$BE{cdmPUZBdE32Ho#2{;c_zbye(&Pq`E z&IL7&L!jtv1E}k+1y%3sLFNA_sO#?sb=~Jd_3LBcbnus;_`|L@`uu$nsCKo1s^3wd zu3HGI{FNd8Oi=eJfa=$aK(+faQ2ljXz}rBrpZ9|*=ewZl^%SUj|1rex_#SW1-e4>7 zGeUS7sQR1_YFtJ@m9r7lIA00A7<>datMN2C6^y z2UY$YP;}b`#^5qg?HL8zz)M2-R#5%_X;A&}HBj~ZIjH;p3DkZ50jm6#o4lN9;8ui> z2E{Lr2i5;wpz41*sQZkB@U>tU;oHGCfqw#D3|{#@FYkk(?*AE3_kSE52cHb_=e*zB z^Kx(#@#ElP@T8l){+9=Q8>o5tL9iQq7Caj~?G_KeA3TuoGvK}8-XC!K{tBpmejHSL zo&Z&kUxA|czk>UNJKyTp9|j&xcqzCIcoEnFUJi;*t_G)q9|-tSu!ZoaK$Z9TfIkA& zZ+`=IzimF~;oU&>`+=b1XM*bQ<3Q2pi6Q=U@F2qHhVVvE{roEMIPg*MP;k2s`M4Yb zzKHNra2hxSs-CX`MTc(!RsY+;!@!5ZlfgfN8voh1IXx`_51hih0iP%Ss@u_Rz$@-> z{{MaOBEkoK*!$}S@KnN|235bQclx-m0CynV2Qt)A3FI%@^CLdq4}nU56jVFE3#uN^ zg6j9(KI;9t52$vwf(L`Ag4=;*Q1fOTcsh6?xD5O%*aE)jE?>{vK=s2yaC>kixF>if zxFa|UDt!ZZ9(Wn3`~3tw65QcqUaxsz58*YS>U}Gy>+S}1zt4iZgI@&I&p!k;&d-1k zg4^8f^57T2^9gTt4|M_i!5I7~sPX#-cmViEQ1{>EUcdhW@OZ*!gX_U-!PVeSANO(S z2X`ZUDY&)LLDla%Fb3ZXYJNNb?g#z^)cu|ZMgRMM!ui^<;B>-ggNnZd><6z0p8%(P z(%0iB!4nAo0n~ll@AG=k12ujx0Y%q+pvH9&)VOT~mH%>3`Q8fd1zsQUqoBs=3jrSk z_aOW|@F?(ipxSrvr+oZofXaUiD0-L&9sn)^_XYbwwfhyI#_b(o4BiaR0Y48e0iOfQ zVArRk=wk3La98l?&-ncpf$fCP237u5;1S^aK#lX`A^s z9|lF|`+dp#X$3f)@Fn2(;4R>`;GLl6#l7GT;Fm)Dx4|~TPlWLHUv~Q21w4)TBfu8$ z0&qw03UDXzO(A?EIE(P@py=@lQ1|&WxDB}dqkjF)0rvxS|AWC@z*(UB{}gZxTmtHT z9|xy`4}hANkAkY_&%wjNUBBXd_e4(r*N}Cj23gDHq)d>VDP7e0{wL zJc;m)py>YzQ0dPG-1h5U{_ddK9fO*Ohk?6+r+~`83fu!cFN7=L?u0J{ML%x@H4e9d zy8d(E9^kjZUBF+1H-Y~EuLN)W2DA;%c--mftzh>QK`;^<5w0uLgD9t>6p6JHaL3mq5|UUf=ij9131Tcr_^c_ywr@JO{o2 z-1-MT-rIqP542&_;pa%b^Orz_L_j}!2OAT8>sr- z1!|nW0;*j<0YwkbgKFQ-Pxv@>fU4h1!5A!ny6>w&jl;C~B4sP=!<_tI+RKHe0mA4L5zRST4;Oj$p>XYujv^%JAx*AkJyc<-%-U6!MKLM&9 z_k(K3mqYl6pz84}@I~NXK+(ebLiSqT|nk z)4)f-W5Hj8r+^3l#Pj7r-RELZ{qh!2_4oj|0K5zA0sjV`3od)g({BY;|Br!c??a&O z|4mT!{1d45Px-0WV=Ab8dw{COvf-eQtFZYAG{y9+c_a49T@jndIe4Ybt z3$6gS2UmkC_dHPaTMp@O2>2FI^|~J10lWiLxt|1e{bQio_dRe&@K>Pd_!&_5+3C05 zFMEQ@e+;O6^T6%EWuW@6H^dKts%H&+0eCg2a^4iu-wtj~_-1eh_#vW?m8F96RV{3cM>KMfuV?)E3AqvOF7 z37-Luai43!I^k{p;`DF1l08#!4H971n&UXM^kLP+onu0x;p~go%l|0C$JaPeCz}F0j~k~18)UY z-v_|+!LNY2{}C^kVtnm1Q1u)G&jl|5j{%qc-Y>9>K$g7<(i_&cy0+;vCqmmIhY;UQ3Tvkp}KuK_i$-VSP9 z{sO9=yX@rS+8*#IQ2lTMsO#o|qSM8o`lB1v_^t*u4x^y@wF>IGi$eM5fEwTX zL5=5Qp!(rSQ2gW1py=m0P;@bM=P5R?4*_-Ea!~os1NQ>+p!m-Pp!(xNQ1!YBRKC}N zdGK0L>)x{gcVV-DX=y*vwSH4W#N(X5i(?u`k+?&-!DAdx>PGxN%TeXHjLIcCp??*P9ct<{ca)c zCB%K5&_eJB9Pi=yDaWa#>9-H~5yIKumqYlEU?<1H9G?zx%J-RYZF0?rNqZ5;-bCyU zz5@IydF~6>Oas5H%*0(6;uL&>^S6Q1!EY*pUkiEOPxwhtb9WE$6!1#oKF^W-V5!kx zNc$G3J)KP<;Q;b{h4AM%^n0zY17E}O2*)hKqQ$Rr{9flA=X3m$L&W$ku%A=$i31fE zezi~@OGGq_bJ68tq~D#%^L@@QBmPyS-^4M*@x73~6=mzUfbgXpCvkpmNK^bd9CJ86 zrU1W}alD=Dj{|=J-aua1R`dd}rVRX;?zX-h%=rlCXMkGA{v68vCFk$s{3>M*zh?;l zE}ZX6dX+=J5_M4DstY{{hE`!!^%`w8se_ z#qmbMPjZ|Yu6>mAPZED2$LolDfbc~eH*hTBNPdTL@~d#oGEh8FzZpMlSFT%yeU#3R;+as~o^g!CmG6^@IDYvFjBbNw!KFx#k3-_3CbX|Q{< z6DjTh&d&t%;BFkfT;B%%nxoEf6^DL9#NQ3B=GZ#WfZ~5gdJo5gr0e&3&i7JY@H7tn zmV=jt^nVe4C*cP<^M{m) z-(teAC43b4X2Ngb{52eFIsQ!eQ1Dt%>(W^qj}bnIybmi6;Zg8!9LeuuPCh_*JC5l| zBzQB|==TNiO(FkBIX{>4apJG%eCLqIGV<@GgntG8DCAfC@{ng7ED~Nt+P3uT!I?ZC zB5ecV5%5Ni*K?dq_$%OZAzugQ?VNvs<3$|LQI^)+Z7Az$(mIK|k0bfT#2t|dEB=QZ z7Zd;a5Z^`kK#o(1JB7Giz*`ANx>m^V#}4-Y7~&+lJI#PhY>!T#bX|Hm;rD?15x9i&3pk!4yf5LO zhr9&sH#>wsNZR*_xu0W@bN!AdZCxh+U&FcNsxiv}|03_FiKmP0SK)sjabE#{&G9~t zM?-m>+V8f2r-b;$T>twJcMs<;4)^&7cxgC)h`ev%xGaQ2_Na%rju3ZZDDRIXwsCx% z@HQOpBmKLC?+4#TcrS2Ij`KMFM7Yi&8DRn8S95%v^ZDQ^;_uz&i13#=jty~d2q<~)FC3rb_zmg%aqQ0VLXML-^y}a_A>4PB z#rVI!litU1Nl2@K%Sg(Dt2yo>t(W7woWGs;?{dBr+#A&IH^l1~+kf=%5WWi>A?|$g zy^iBw97l3|gX3b3T}gWt$8{Y4;Mj@di^S`9JjXjjISOwZ;&r}|V+m<{a}+|J6{J5# zd_VX@jvsKWC2bshmiQw$e+KL#{0%VqeU0!>2)vQ>9XWpi#|Fax1be|d!D~45`#E_| z1wRHBi2E(aKRMqYd>!%c3i*yvW`6%5O}}>&zb)q#jx~foM0kazum*B|DCakk_8rb2 z<#-vH-t4ds20@KfRZv~ca=#8t!jF{BN0Ji+mBk{(cH-KBs0 zzEAp8;;-PmkK+@H%=`+Rw~~Gj_yBka`Rknjkn|1tdLi6^ErORaWcmp#Lpwo434>klivfxUrU^Rp93ET*ML`X zbaVa((ysx(2c8IC32qAccMF&!UcdFE{UpRGypZrM;Jq9#<@_R!^Ev-BsNcKE`vmwY z@a-Hg;r!#IUCr^AP|h;WpXYoEi^3~7zk>6xgMT3J?Hs#t=r_diUgE#R@g4H)3jP}W zfTeOR=LZq@N{(HKI}Y4PoPOWpd@A@O!iRu6fID*hg5!l8`rQHEO1^D4^gA!a?+Sj6 zo)dayggjUY4jz|-@tJ{#~|TPkoODVI|v`844L1{iQAs=TfxtPOGBNM zWq>0l{AP|%aa=&2IUL=buU7_s4{`o1ac6V+CDnYw?2oXg!+V+23EUss^wxnF6K%@_1uufjOoJi*m&GqsFAd^t2sEDW(yfs3^+% zIGVn2piqm4xJ^7(DGwHkT_aoi!^I#Dea4;r7;G&QQFc{wU#Ruy|pzBz|1gg zF#|}Sc64-50C@_!>8ZJ~oR8#bWd?wlWw|SRbnv2J-FP ziB_-5SL>p}Re7WPRrPAs`llyHX_5Xtqf{{P0-r@GVfU8p zy49s>eQb<=fkr#Km(nv&uq^uANYxnd0!jk~*< zYx%zVkj0K-yK~j*x^iV; zf#^q9+5NmMv82&H25T&2ipxf{ak&(am+O`Ij6xw^9go(lUei*pugIOmOl!jwmGkD( z3ZkGixR&Z!sM*~ou^y(C^tF1uw>Itk=$4%MdQ>>SU2NWdh;%u~=!=Hkeo6 zxRZmge${xOfOBnaD{&}au0f;KTD``a z5Os=c`F}A=&Z;#Q;6jeGni3eSHl&R5D8dCCiXm52#g`cii2aDaj>j;738IiPPe`v? z_ezTDS0iXmRm+%I0&6oX*;v%MzEF+k=cuvhym2d6iXMsE z7U_ygc^&edX234<$-z#dunP#Dl8Pl8Gx>dA*Ztl~n$XV&o8oh^ZKCXWLn6cnL_0fogXMMNiIvu(2UY_TPHbevy>!)wVAFywP)oD zyC8|Si*&Wk%a{p4J^S-Tfru0#OCqUKlc@BjNrac7EFE!Ip^BW0@Ha0~SG2J7YUJY! zW^HU);1gs(RB*xUjV&wb@`8A2u3kgrtE}!p_&^SGq*7$;r zG(wpN;4;2=i35qTCNt7jt8}3;c1s_l7D$F4=xAA~KFwyX#zQ8YYALj-Za0{XD$+4a z!eg#aqLmh*boO?%w4A{*lgu9Hdg64dNOCI|ynLu~tSe#BK{hCsp@)i=a3=3S3@gwr zJG^CsIXcxA<+Rl*wV(M5Esqxa)75I6YV?w1L{+G2uCzDeGJAcBr76;cX%ICi25)$2$2^= zI!+vEF0W-{`l>@Dm`c)-kjLB+p(Y{_ovYFCku+eU2@`ASTnq780}{oS zo)qEJgVZq$Io(#O(yN-|iR3ii(wS7!moKaI%j+nz1N31^K*=OY%5SQfx{oS`3B9;$ zJ~yuSd3nr~nPJ`2iY>a-%E#Qvvphqe*1am;y4@zSTHD^9w>7wZXrRwZO9WNsAJ-~P z7eUs_-6drJC8v*$r1MI7l#2yg{K%7@*gB_4!6Fn^Imt&!zUDZX9Vu=$STM}Og|wTm zj?~IyK_)4d`>iXD${N?D)MG?>RrcmWtQHuVG%i{ahjNI7=tvqVe`nu zX$kinKE-pRQYJN^eRcSP%|X{Yluo_@m@YxlX{G3)Y6ig<%YrWE4`p`^f ziq=f`#P-7)5>hK=nB6F43?q{`s})m#Db~ug9SUr&7YwyPpF8UqD_2di3i6O{75Ya# z;tgTllAR}qO#q5Tra}|9n=xMp%K55<1H@1nZs>GRr_~CWgxS<9EaMfm&ZXy|nQ@-FAN3AfTz7JYI9H~6%RUMcMQ}t5Z zb|gPu?WnDXlp!M4cJF%xUs)lg!uBhR@fp+J{cc{o3AipW=HNepnSVLqTY$Qmx%Ar(+T27K~ z(p{QCYEuyO!!?p&O!ts+#EITkw*E>ElB&8K>uZ~*s`FWrAS)I&&E&QDB653$zO)`w zf&c5WOyHEEH!=8_A*J7LK=}~vIC4w{b;oX{o{ifIrT$`_RgVl(6HL;# zwIV-oE*W{3Syvvjdch2AoHIj<)w!H#g^V_*e;e&`Ws;QT;()|gZFzT7eJt}x8cDY> z+cElS5v^j@*A2{qCBqm}oK!cnm^LxXu(5)YR~)DH$rWP5IXBvgv%u9^-;zbmi8htm zf+?%%T4r`#yEhr?ZV@oJSmyhMP*IL`1JBXg&<7%o~aM z2cZ%**vKf@BEm}Lq&W}m%5sP;siZ)%zJzAfc8T`|ZYT<_c5t)0bOK+BwZwG=DJ$K! zNh746&{aWr8p>>qR2x_MLemHbQBCFJS(a3oNrl;KCb>t$u&9ifV(B6}3>vd?6``tll&c}8E!*<1-Z#}A zDlHeCUo*~q1zGYL8?*yu@g0@8TG#*`S8`aU81Jf?bp|W4Ky|@e87Sx$3EjubR@p%@ z5K^^`w(!jEg{ykwmEFBdSFXUIVT9~UWFlNk(i%i=wgtn=(<=_Hoor{kg&XvO*t5$aLRv^R2^$qwt+*p>Ghz8Mf0~9v44N zQqm-d1t7u9A5+;h^y#<}`Gn7THs59CF2K8CAwvQdq(fXu}GY6q4-cpqo~) z(NE(}z74xjjF=h)laS2=6UoToDM8%8EG$7YjW*k?>uMcMk>YlhI-Ht5YDzF2p%H}z zV+h&|Lk^g8!fC?7`ezi5*Du4uyJmqXPXslYlq$N5L>PHOT0v4Nb;#@ zuTOe~Ry7Sy&Wta`hUq+9!;EbZownH+aJa7B*H;8)DjcLaAI9H=qR1QU2uN_88{KP_ zGovq4yb;w{9~+TciWhbFM2jrD)zmDBOdHzZ8!*|mJ3jP+jfZA(BU&UT7gJnI3uALxLwVrA?qe(Avnam+(M_gNBG+w(n z#$b&o-Qi_XnH?~xc|+JUAcD<={)ikHSODa!Ou7cRpvBe;Qz9mUWH+3Rqr?YmruK{E z&%<4m>Bx9*q`DY}7v^$*P}f*=n45q`YC|QDX5DkaST{S}~40jl(F6{zS&)G4~m!_0W*E zfS6BVHu_AHZ;ctgxjf0^8&gwWxUdbZyQ|j79Tv))nZCVVQiBmP$4i zhs03P)K|}-f5^=Udut+3-tbfOVWt68>;3b5SmtvgKc36 zmL@eN9d=R25fI2y99AGR#tAKHX?L`g+eG-YB>oD84SqiuR+Cjq3{r*BT5-0-*--oH zCK#{FAqFEVSn^_G={Oni@Oy)7)J!w+mgoUfZYUqSn~V&Jt7jf}8r%XJ_G-6}`d$c^ zOAe6^A1yUeFF=EbjE5L8F&1qphm93KN_CrHPE1TK#Fv$rSdTex2%rRH!7B;+w?1%e$hb z)g~9PpmJ#yLN&3Wfl^UKdl}3BhYn#&(ICoY_GpU*tk!J)9cnw0Y^JKsW;;wshD|0E zIjP5iKrlzMSw?m1W3baI>wx@}jMj#6DYflyX*43oKJDk_*Fz*M8ln*Smuh#zb|kg; zr2RC6CUb6}7ji}HM+4d|2?IA1Q??aZt;xMCF)M|1yNRunNg?wl+(WfU1<1f5|J`kp z8&vYiEef?s%G5SoqZkC-ztd5w?JyTM3{@holq zT$48ArQREijYr5wf!k}tb{NB$oL4l#RR?VxNmHg@Wxq}8V{01y3}0i@!v;9?I`ckQ z?wWg8f`%D^Q(-QYW^8C)>hM>|v&)1HMI8;tkeNB#0A;O5LAh({zLUoI3xqaQT!ydz#2 zJZZE;Z`P=@dyPW(uR{ONUg}B>*zSSiMNWzw1e%^3^R~L5CYNg z^sX@h&lW&hOg?`k> zVI;rJuEI;ieBqY|-`Rfc#o4B%+m~%8Op4Lm$ShI(!_imgz;qKe{hZ3;xyL_QHoA)_ z46?u5zjz50A1#-Ou-*1}%;qmo!ivYTjn15{`u;5X30%)TN`sBkoJLL2GOcA=GA$C0 z02^~(3Ylc3&q`>U7p+*`6RlwVjVA7lC*iQKPWt*Dk{ zAPH!%d74suafx$@M@c3Uev-s?khMg0FlkN8i%3x#ZpsyH?U1Z! zUNFddkZGzJn%rjcf(gkTaVLyT?kbykng(%D#Y=f$Xfj*l)o6ttMyMm*^5wjhz1=ny zdKND1UbS*LyAIl!LC;30z?m#dpLSPgr*6UK+KxUZFLO0qZbA5Q*d&ft;#kcg8(p-r z#GhH~dR)T|p+RaRB1Yjum}R<2C*RzgJGoe#ODm3gNs>Z+tRr4z&cN24Lv@}%V6G#R zi8i(T!ujPI)l`54N9=2w&DEv^Ot<)bqTH<1Vlq)A)4rmoG)&HoxKvf4R9KUhklHgm zs6JG^TtS-WVm zPTOb3b6li6v>G?eHOS97?XqZcA^&tl-7>!EUme3ho-_CBh-3h_#}BLvjN`-n&D{WR z+u3twxFBYogJGzV!TXOoG7Qbv%EV=l)=)NFXP~T`U~0f-Zy9|P$~HD^q!!|fRRTUZpsHtb7W8fpsuAJ>s3kXX-Fzz*J*Pusw?eMmPM(tUhNqWIpu{IQaQyP`jb`N+i zPB)&ckO|#5p#ZC_$U&7=8FJuDFmA&NHp42!!?u5)ZD+>)X( zvC z@4-51lB;YNUfZ1PC^U9yg34|-A1fqz%qHA=u(6-QIN($Pf!dyF0pAJJ9HJhEteAJL z);z|>B&jhxnfQ#h+auSUhNM2huG)jzizP03ins@piu|?Z-!p>#-D3;Av3P_P$quPM zn9{UI*CUOz49MT!?9Eyf71RT#w$Avo4GU2Sw1L!9qrQ0k^wh?20Enl{w5?bVEz?d? z66j%ntQdz3zP%jvq}$Alp!OFvg(2!jC$kt~c{2Z^o;pS%Ea=Ho(LFA{`YyCs3Mt5P z@~Tn*n{2AC26IdVL-a?f14sbxH7|{*7kMORV480)#f-}Mqrc=~isIUj7l%&ev2Txy zN4@G|m9eI6x$BKTh)|4^_AE^?O9&N7p=5r#HzK5M<8nW2I#JxtCq*hbN$th zm}kJSXhO|;KFIYYL{YOI2ZdstBz)f?GGVmQKipdQJQ+t0tW#sO zq*R;1ou&6A%e^xmvn&K_jJh{@+*>|3sBy@^4FMq)gRAvEFN^nKw909xb!H5rR+MV3 z*4S2goV3npncyX^zOe<=IyW)=byzjmq)JXo3ap@5Vbq2O?W>ZB)tr?{rXtIbl0`LX zO=jKHVwkR-74s`*@S0aq?<^C_)F~Q$RWN5eamE_BvpT=YlC4?g^$)$PYt6Kl)}yg8 z=Rh}H@aTy^vjwcE@{iSsw7E|aI@#XRovbGLrcvzX_eDJz33nrw@0ANGY1<=-f7DJ8jci*k?)}63oTJ`So!Lm5sUtXI6+7 zOITR-cm^Fo&ij8fSgKW=<|b^$=*WZ>O{=CyC5<*wimHKrrL6eMDFq~zKeB^z1k#c_ z`l_^0D>&o8B%ZW%50s8w$iz2#6Yg^~9m|YB2Wh_ETyNbA*E9U*i<|Atp~0y`dYHXx zxIa;=neNk+X8P^_S0%9PA&D$C+EM`)R?(d1ihB)e{!0af&*=Rq$pfgye6VgwbYT|( zGr9Ffb9IvO$8Ka~C;`V_H{krSIkR3vkY%LI0WDd2=p(#ULr+Po%Tq4am@s;1$_%q& zRk8rp$fs4ByCmTb+ZB~OliiyXj4G*!o^Cd#ilMz_W}YhImxT>r zH!DooM@_rJoWM4}#LNZ8ozffH3Z(%FMusq+>9%rG4{1pLa5($DzP)PO5*d?jo;NKR z-?ZEK;-Fcf(i1Y##Lw)Aj*44B*;Xfn0rp?R{O6-KbuTiG(li}#9Z*Rn+G?G`XE#5*?I2kN$Fr?R>&40`E}%)AgMa=e}p z)ohb`3r~jj2J0renutZ;*yCU=!IYhOl?rWd)K+ttDZm4683q{I#y*bbyUkto*dpu( z?I$nw9qM?sk--sWUGRr;yx%g8H>N?cIm1Iw>61H_DY)YYX{^dhQaY|<_Tk?1p~C_} zyR0jw<|+hSE{GY45kme?CMQZHj#(s1wfbn(YsQGBUEySTcr}wI50%3dpe`l{$v57S>SA%CX>ge{_LAMGq4UA*`CkFEFO9R zRLU6K3ySSDdumg1Vm)i>8#(diMA-0hzp zj5cYVzLg4P6F*%Ke8KFo;Xb=lk*ni#FEraIGvG^(qMfI%ZxPixB}hn7nAy$GkkKT+ooVtW26RGwZ*5 z$g3)|3)9_%BvF0XV6msXR+$+M(O?%-9+ebr9#2slnIX3^#dV5JshoWgc!YL00}* z&L6yBoUbw^g|*NxRD(k%2Cs2Og(rR4o~&%t$KIZ>hNtzgxt_7QlcS4c$^7VyTt#~| z3C`7HR2X}Vd}tcHe8LOinx1LX(nsgv!NAVA$qs?CVVL2a1&Ij;evAxc1|NF~93sFl z??PauPZdtBwJ?x;4)qAu$T`8(pmOX1e(sr57TS}B>_*hFGWPabpPstge?BO9W( zwAE~`;vNxXE{iz}h3l-K88sf5avcdlTT8dG3Q41zSz~-so9Z^nCNbtLZZUV(>|@Y? z)>f0#mQLoAlreu{p8R(dN~C3VwVVUzS+zvx#Q3s91;RsgT6t3O+l5cYJ=HM1W3|U5 zaitACX-2~Z8D>QVObSpAW~3SkT?8ovz6LqCj1me8ehN$^HYe3v&DE0KPSp-iOslI* zCbCTl-en}i;{dMW-fYGuS6r7HubLhxX2+JCo*u}wLmtQ~M!dqkWWB7JJRFO&D>R80 zlg5RsOdE986UyQam`P;2f;Ky3v0&<$c{OEuR(QFN;MU|Vq+Ze+Z|r&ac&J$JgY)17 zqUQs}?&r3cdw;tgufz|5di$2LN82+EaXZwfCnB=Roy&T8;$h*6-i50g=?(LL%fg-o z#QQwXM2D9y%7a*r;##0E#mlgq(1hZg{?h)R@H$2l@Zb%w=Ypme(}c;QAvWV4Lm6KG z*3r@_3-mx)FS(G7NuAFly?U&}UoJqyG_Te<4Oqc+HQj6*8_I4|g*%z=gJxml)luRz zvPx6qKty6J$z!GAexqTXG1m0!ge6NmL}|EJDg%hb6l|VoK=cATUq5HyF1f zd)ZEunHy>+rVrI75XNxaMwsuCYW9d9XfhTQr;tjLWTfV7HNuQ39+q{ zq-XNTD(l(p_N4>uLDraSo!HX7vV9e93z9zCyO^nS<2keD9MwL1cKe(;@$6Z1=Nx(X ztl6_>wJgJA*>3tKreM9Jh8TajcYAL>H<}60g(zE=FI~Q{ai`fGv+T_P5LdhOOCIZI zJM9RjTA_4;=#dvO)=pa8yQuxRM!uliwlBnsS{`9@Pd4>VCE|2q_$E#?WB12j1yYUY!A8HS_Ud?_(tMUg!4dP(ITw6Ti5U8!U{pcjMs39GR}l zvV4cDxbZ^v!D&7v@RoNdsIuuM<@47IsK4gpgl}~UZ+fR5Oq>F5c!y*+&;ajNWceZw zxbYr(>pfMx(Pnl_hHy31Yc&`->l<2R4b9hUHZ0kCQd@h3+1~t4?Z-mpmlTSFs#|4v z)7{o<6gHIW%ePM`bg8R#8ddL?nSm5y^u9{+tdze19*;e{zdg2*Pzd&`5Z$B2mr{b| zMX!dZGo-}uTKMobcsvmp+hC8+|f_k+^nFyiv z=4xn_O1cZVreyrN(Lfvp**N4SjwO4)d@nN`!?3~{HYdA@Z<%lJ#HQ8Hn|mj|XkNr? z!w1-#=4rOON905I1TP2bYcHFJ`l(=+Jz^%!7n|nbLOqkuEMB2onpVeD-*jJXgT{lW z`r13|gO&PV`V#u=_{~R@V|Eo#8f1{ro9GF~Yq1FWoUs^{!V(hKmF{Frm(x|Tt}~^@5}Q|3@TUk#7GY+ zX83gA!7avwGYq*s^SmPs$fg*_f#YJ-iFvagy@ro9ua}?(JO=@TY#uzlcCJ(wRZ z*5bAV@Ge(eK;Oq3u<1I;+)ngu~KUr51r=uGDb`$_CVO=(36H0)alJu zI&>}+w0d<&)CaHE`|BrJ@;okHgHG9Cq0y4e`nO2SAbYfIB-G&=M!3n`3bp3aw)_ky zH=Wq>*%2>9JkH}#Z7;xQCD2MlHzpUOS`Ns2!dF$JRkEhctrneKuR_Tiu)47Hrp&0* zUO2<^_teWrC=hzSy?YSl&O4Iob~V`Kgj+B`+*VB6@7lgh>PJ6t(9ErUA2aCYilw(u9)?f`blr5^Q(AK2X=!^Hw|kYX9a^+j0##!a>TJ` zbF&GrZ2Zr!*5}PY;$(h3Z`Y^Rn2TIip=DZI=8L##2)hHKnh|wus5kq6P_rlMH5DwZ zy4;G4i$R`pNgOoQK;mG{3ZjtV8kmEy2O{cj6Q=F|W!E@_6>kS{RtY6>B*Z(e*; z(|x2eEuO#vF4%}R&g91*R{P*HCn+zyqF?XLhZ=0ZxcxM9hGeEnsuB;d_eE_ni*cHC za`WC{rZMjUhlJL$weNBZ{#pgaI72c?E-VY-1=7r>bs8w22kEo%`8Zn{%BF=--G@WBqWoM5(`54}k z$O-Bb7omA6yS>$)9L?f%cV*LkJZ73r7pG*!cf(hr2~ksIQ`KLOVss)2*<1c`ztgOY zY?iS%VLUuo;>A9b1YRvLzc9kf{?&}dym%4uf-lbS4ki=-VjjEEEC02G+Drc@jpzM~ z;$}ED7chx=S)9t58eAh`x$e!DR zZYn#hi**-)s1}`Ob|A@NrKk%+k#mJHL5*b~o1whqdMzN%)gTAB4_ENq4n&D~M3sAiGY6H8*gEaG|pcPujrSk$U*Dgk7A-l#}EO(q>4} z z)`B;zBIBb1Dho@HvulQYOH#EjgDy|_7&De82Z-}@w5-51x20ycTr-(uUES7q6E>Rp6Ys)DOo1~XXXQ(9Tgpsokn+rlxK4!01lgK6zedH#@#<-M+ zVfI6t?&FDSIzV&H1rDc5iLO(}9y0A-O}mCXsYx z9SC1cP*f`#!a$O!&4Ph=2}&F<#@B}v=m+UC$&=z?fqehfuVq(nPv^JE6x-J5YZ!L$v|qMTInQMV{9I>&ZaX z*v7<~t}Q`^%jB&_k((|PTc*miP~sQ{0IN^?gmp8M52;S;9g2(UBz4ceve2GXExE0} zj{*Z2;>EU-R?Q>@b*X3R1k=1kfZUt6*P7LUwg)a(YiVkU$t7m@;yV_&quc$1>}=)2 zlQPYd0v36VWmt#k^^yRc(AJpmEwuOOdkdMV%X*YAif?P0gm^JIO$j<^RMD(;>x|&7 zPi;&nt+l<~-)xzDcp+7>d@4#hH`9!!7VT~|ZMGF#Em|x)gF&-`8*0hebQ4?7)-__? zScF);WIVLCOA3MHDl)ps%qwv^lQ$bJt6(n5*apuqVc+^-C4J$Hndb7!WqqT;RS!1?W%FHHVCOEj;WpQY z0ZQA$P&FLX42BvOnMs53Hk2sQ>&F+rE6l5W>Bkqpldl~z7*xo$C5M4PaID&a=VPZl{0 zkipggT8>{CP0blmL~o7L%5L_KZ@|> z&xNbF?3W~(t1-$x!yqrOGlOl)kS4vx%nBNxbw-#;+bmk|la(YXWsQN>u+SN%@1#ns zb*YaID_)b$SN-9Wp_Ek5UfV?P`pz#e!4GHIuR^p3!%il3f+3}G8DfeC%Zf9ZKqRs} z4_ia+gv!S3^AHl2O}DTmzSOcrwpK>=3|o?ezO9jq>22)4P^bFa7Q#y0%f`wXw&aNn z;5Zw^t&u8BVH(+VH}*YL) zF_|W|Vm|W%8B?kR@BOEcX5Io1^B`x z?nZOd)U1_&>3|HWTTiGL<*%ej9z;qwC+<1Wh?$nPD8P-Sf}xrwnv&O(;V|pQg!0JM zYA#2u?HF9_b!&74cfc{p#y}FBS&@o4`n*M#XHs*0o>qphG$rz?T4WnNj#3yEUC>rv z|9NAHeTyUUns_{?sLb7DO%mA_g`Fz zOrN#d1Y1CB@U#@L3Q_d$?8Kv;|XZFt6H2$eZh)9>MXHUHNNZ|Q=IwQ3Vg>z+1PQxV7HGGmz4 z{4gq6rOYxL`~4@5Q{2`dQWM0b$R;Jy7HOlQ<}yARm70k`-^!3QszuC|ZhaI&7F<+a zvnIz~VNWr&^&)6tK1Nm=f_vC2F=}uKR$k@@47FJs+dPK;O{^GvMFTIRh`ml7li74E zS}%(%d2K53d=?d$CbiTjHG1+q;NE1?(HAu)iN&DPBi5Iz@Orak`?Nu>sXCe@W>ncN z=&omWHHHK}-GMxA4XGCY4;oAnzw#C$c&HFD}&f7F6 zl24rbC1Oj+f?1kq2nRusXiOtFML-aVvW(d?+&jn(o9=60mPc)p_@EVz1=FS}bd~G| zx0~`7j8+IfkeQ^>C`g;}5ZZb{UnnM?I@%?y?v_IPditg_KE z#GU{`MzAdLb&f7Fu~`!0H`GIln{RbT42#+a&|U)Gfdg7T0%uN%NZ-1ULs$7bilG~3tfp*(IJ5;RwsD2tjF{4Mp4 z3ihp$#?68xq&C{eK;%8&>YKssJxmOi%wGUj1+e5 ze0Iz);BYM$RX6D%4kjN3X>GE{nOMWZ(CmZa8cDRUeK{|Qv~PaUJHDx(;g6XQg`~-; zbTePRe^LR`q*SsoGe!7hhct|~C@ zOkP}^8b6(Jb%&3LH0>YxS42=Axe$L}+o#b^Bd@zeM}T7|#JrNPiD*aiW%+?J8!990 z&}yKt+y@Oq?`beOFqnLucQ9{L}?#aiPyt^%86L1gU`hHYxLn_P9(zby1R=1UJUF zhkf9tRB7;au1@U*n_${alDEi_Ro9!{3*}ax zI_gc+>os&zgE4|Px50MW6PLSxS4^@e{XZ;0T&}erEzCZ7lJwyJv@j@bp^Py&6v^Zj zVHuM+MiazRcnMiYEZ-=FS`is zK)IJt$($)|sVOMTHb;FbrVR*kY38c%5tSV5T*Ou4>my?rgvEff=ZNT-T=}31f^I^% zA*(6TMV?xhRNuJT_?DRl5HTCqO4O1wu+R2lc|@yOV^gtFfOvUxS0*Fb{K*w#!D=kx zob+rn2Lu+>G|x;>$j(a?!_=NfPwJaTav!0n!6t)>SfZx{?Ox(f5~Xuq-5uobHj?K4B^bh5eIgkqrDkR`O>;a>oEmMEW976Z8?lnj6!B`#}}! z_Hf1aH>^66hGi{EBrRx?RlKnor@c2m?t)~cT&{(9UeC!Yu?)uhKj~V$dHD{Q`2sL7 zqcp_DJPK<_3!68gPxb@5VU_g?YXv(adY{yUoFWDHptMon(w1%80=XUY4wvSxvZ>ga zy_L(uEg@qiM zTN6yP&Rvz5#2awkbVoBrmK_xKzU@7VZa=RC4LAAfOrr`Z9dt0fq7W-qpPX^ork(H1 z@H$pkXB6;{&a|6ur;pAg9hhM=csa2Jov0CTEWIsFthC8%nlIBx&v36cYgA(EX$mk6 zc+gZ1O6JNd)*yp<^CxQ>Dt8_~^r9)|R*HO*+k4=zRDeqv?$rCnL>P~-9rDbnf7b@9 z4rU1Vua*2PE01?dBG)o0%@{Mf>84FLAkAvSD&$PAq#wd0hu1)op+MV>wfsH3s}}nw zYuLCVb8v%9=w`8E_>vX|_RhqSym4lGPR0RgoYpGaa)5O-RvQzYtlGhNW^O2rbn~n- zhcwTWuuY0{?}TDd}2o$3=eC_VOVm-GtUw!=n@afpWVSHgwiA6xP~ zvTxpGNqL=&Ki;;nK5QR!H4%(q6*n_z3{D`?$7DiIwt6nPr#OASd{McDwU zkFkGB))q6rLu67S^+g;raayG4Ob@VW@!_HORI6^1Q9D|azKwedd~C+6$EkUpz|U=C z(PS!KF5!;#5}%XqG($;nKJt&{a3>9v4~HD2!)dS(CJIBA0BwgiLtG+2`*sc@x1>0H zB>2WiccQuYH*^-6&0@2uFFnADGSijs=)1X$VUus7`WC_2j1vBSPhJ{fy^hJtQuQ?* zO;m0G74i;q|B?>pEMU?}Sr&NVjjZRgtR(v@dYToZr`*m$0Il(Acb)OxPM}GRtOWp|T~rZk+pvc}&3~lzf>7 zc0I{sB|-XDU47zm1$@noKiTsjwJ@M*deeFG07@nq1`iF4Vqim*6FvUDj9<0rF3ZqQXt-SmN3xPkJ-VIT z`6js}Uz!(5`nD0?!c7Y)5!FuM4KPKlEI8CrFmJxlTZ>W>49n#_)!*xmR}D6Qxz?ri z54Xbm(JUo$bIBG#yTiQZCVF;WO=b_?xG(F~_!!SI`!-Ydx(Q$V(cDLd*Y_r|!;6c^ zO~_;xU-5W&o6%7=UkSvEbSFI)!WNl&%l8rzSK{o=Vs~Za^)VCN_N=#6l-PHeK*rbg z_;O7nO@C_<2o#})ZQd4QeK;6kCWCyv>=QuYL9vF4Dt#A03m31%f(M0UJgi^?P*C$1 zfy|fLp3P};Hb9!|<6~uNHj;py{KtJOG$bBV^Z({ifMDiNG@i}3XEckM%@Xx$=t~s5 zLHlO1mnIV0SVp#6ruR!Tf%Jx!C>W6fI@M<^x3)9w+X8&913iN>n=Hm{s6H$LNVU8J zL+{a@fs9^@mgjkCDWoKxmFzPo&nFSa6fi-@(1cN+P`)kq^-|Mr=mgU}UroVSFN00o zIWmIVqI?V^^jd_s(IA9C8rquICY;k4DubqqG@sEJGjR#}le|nMdv#&I3MYRF8gy4< z8_#-CbjBD{RN~2nBUUDlyK$IH7CTutmDp-Pt?5LMPF80$=*Hc0B~1$|llgWL!VVjr zsU_7PFigU5na1;l%L^ks`w2(NT%FUGj|zo$dqma5L?$w3PS$u6mr!Q3KSdjYr$%Bi zu+JIcb*b&UqW^E7Zr)2C(3gu!W&X(&WuG&eA9gxv9Sn~Rg&z0YkaQDlRN)&(3!qyp zn0o&y=2pIO#FU2>ASudOxEOlY_)bP*qFaIsbD~vEo4^3_Nk* z3vL!Jbd5lAi{I5K(7e*}wT^gx-X86;p;7Oc!XaMJS;A7F$IHztu~@EdVDX^Wu^dzo z!-DTI&Gm{*xF7j?^~8kU!@i{q;Yns2afI*9+?Se`nRJ`a8Bvm!QPoqUGsK)4)RSjJ zeAb)U)Ydu(DLi9+c5rm1$(AW8Ir(%G)YQb^lXR^Y**l4v6IvA|q(*Rt)?WWemu$)M z#}v2aOb+P(r?<1&Z7VsVFxPqtjut^CFulvh8^eY)kTbTCj6EK}I~t;8#T0GOA{+?F z+vFMY0`p?|&Z(;IzNF|ukYF~pNZ#AGZ+CV5o~i;=U=Tc)ljgf<*}Ka5t^8i+>^tCE zC`+Wh{Zpxyfyjr0`}`DOe{f{h#geyb`GmfODiEO$pPzf*qcbu>IhEV9_mrlWu znrJ27MARi_-33_-|0g&1;g5yl`%+%cS;2ypJj;;{r64BF+f&s5ZU)8op%7rk>@cA^ zd;IQohqde4pLQQsB<;x;Cg;u90N-oc$NearFa}=kAPZR%B+>e1nO<)vC5?zBjG_)d zu$^$pC+DQ7kpEyS{pRsr!EYC;C!qM`E;c zswt6FT384%a^RXYH6@V{H7U3wjsj{i0o&WT{r1RvGAPI!*?qpPN(P`KvgcI% zsIyowj`mY?uOB^N7<=J>2Z&YAKCEKU9q^fj+HS*iiVn|}2&b5mGbI0ZoT|0z`kwI3ytH)2V| z++`V5mcN-Zupu7iZF{QUZfN!LHRxg#gD_6i0uz7EI_8Egu_~l za!}BRryZFamW%jU2zd?V0~xWa>B=7+nOz(K88U?@D?U!*r2()6wJ5YE5>8nJ$~RKj zXjxmAx3$6BCJ&y1W;f-62!#{lDKybDDfD?OR(hAz@_S3Ca z(-1H%Mt&PgPWY1~xFB=$7Q}1D;_(8=R@t<9uwVoU*=D%%)Q6ul4M)(yUY_X;g*GmQ zTc65GYx=l<#<@4SGF0>K?V8pr0q%^yX{er^Uv!L=b?gD`40g7_RqpBa9M#>&LUubr z;WQqcvMy*`dc)whC<@*9S1;6bx#2OXz|ynpT)3+o zI9b$6fIWq#1YD*{kD5IVANSpEEJMH6RdCwJw;ExNuHY`6`N_n#ct+JR7>s?(d)GtB z>}aHl`eMVeovhuyFBL7u9IW`&m%LiI@L&`9MTTQ}Bi$5^*aH%H;Ot86M5=Vk7osMz z)aS62zkPS;5gZ0myX~agS{n?FnL~n*?g9D5Q+EsJ5N8&>wf~&1Xm%1e=M0k|Mn`(t zvl0&4l+;|ZZ1F2NAt-Kpo}S@!*ixKRE}rrG!6Q}3;ckuE=Q)Wf(a)9O8S23@oUbGe z(A@+-)|NSJK4w3%CMORt@u*N^PqKbGRbw9xIlE9M8gC-eO$0o32sjngA6bPAv0VwL z#2?pd+LKk8tNa-Ly8Q4Xc0O#jE;+|hy}p|DHMi?J(OdX7sOtp~f<%zkpG;*mwh5x2 z)i4>h@~TJhik~=}8Oc(YvPI>caCXP^w0pXh*+P_zpS_mFG5_=qcC*zd#)(CG2zw!USMgjIz}xhsWfp!0Xv>$KJNMTBJHsh=YglA88d$*EW= zT7u#ydBp)s57;@_tLt})j}caCI(+y|fBm-|r`rs^vR6GKi=G)NK3YwwHLQR+acgTP z{id}wlhmNE8Tf7YHKRHNORoA(oUChnwR3^z*Yx!$+t#+Zg{>t{tRgF%zyPu+)ISbk zwZp&8{Q%Q~t4vySPLe&( z-qF`6Sc=}a*EI6uJo5Ipx}CiW=k>Gz5yW8KlVunK5zf}<9r;vWkb%G z%W&L8MHQDbUIBk1%$+o~Gt`ozuV#8qT+dcID$$<3j7k^bkdVKU^XrOZP*Zvbt&NZo zR)7erRsD3~K21I7L{v^P-;I0XKr)$)$KLuhW{BH`|uVb<7kpfA)Ms4jFnWJZ*@ zE|+N$w8yLcL0y*HAsjtARbp3TR%#hwPckVr4T0#CCh!?ABE0Htyl0IIXw3MSa_H71 z_OGg~wLcijEr{H)>%`537S)>eXW?5?-?XUU;EemAZ`CNdr*)bZ)LO2PSn=wOY{cnQ zLdKriXF7Lbb3KUvk>NwlCiY5a808tQV<_B&C4|eB^Fk}NX_*(%iMpr>s=*l=Z7y48 zg(?+dMq1&?5_uQr-$n$bEa23a$Xj{Cf#KY0Bt2+S4Nm@zjyKvXo5GM;7c|4Y=c8t4 zWsgXMG}U5(Pc(@uMT(5f9%d4-4r05JJk*wP+=Mu#1rD57rW64i(S5q><1~GD0=IR& z5fdV<*m#dS3R&~~mx4b>3B#~AS{{;pf^nL%qmG4?wz=YPe$*vjgjX+Ob2{_;L6RX8 zG)$zjR0AU@*PM#dBU%E2!Y+itlsce#68Vg5OruXWcgqiJap5yswhakU@<%+(8hpa1 z-`+;}#WK_WB1Zo6{MG6s4@B7z!S}&*x4>TrvAX2H9c&sp(@to46n(>O_#mwq??SDB zs7?|xZMR@axsuFoGGOquVZ`KMzWnIm59blVVlWA1l;+eZVR_S8h|n~->Vn;^J9M@? z;;e8ICMz0q>IH`cwMC0KnP$*hBP3O4F$PA3?T`rvm%H(s1#d|a;lf6r1&G{Sp^zz9 z$HlCpc7u4nay-h1Lti8DT+tz#|44{R?(m=_E zYB?mq8agLOM}$9n^&mb~$Q%i#!;reO-p^bv#JTmDbRB8=sEzpPhWb*HX=DJucgkIe z&Bd8J&9N4gjfWh$_L92dOiY|ON|7{;R_vmgWbq0GnJp}d+)4_GGt;}>M@q=OC)UoO z(t``O#K$Y}W9jx0w24r#v&X!oClO(bV}i=Sq+m0=A|Ar_o~#SRqrH!LbdFz6v#5U{ zzN2ZMK5HT&HHQ1DU-y(j5u7P+@j`F{9UxV2rSO1rg*5*md0n$u7p0P8|gct$JW_YV_J2zTl}qew8IIEn6)F2CIL#=ttPCb2wO>i?_GGhY8)}woROG_z$RI<58(%#p3opOh;ZBh6 z_II6bkgACGH|9HO(}#pei9QQCGsyUA)K?i8;x5U3m(J0YXFRp>lvfiTlY;B%p1VCC zbYB^jxFd4KI%DnO3qwZI`s_1rTAWmCRh1SfprBl_X;WK5D$2mP%ko?!gq#~S`7QF) z4&v;*#a^X{{V&Q?gLf(h!^dNF9w=O3b|wW#-ZO1tg_+A)%PJ~hClu#${Y=KgUc*1q z@;?b4FxpID?R=Bbi;`(E3JW?xv}$$KeBuj27M(Vl0G6XC+W@{L;&+jCF=9<^8K~i3 zct*3VV?~P6H6z9?1G?m=AfnjnLJDB<57`P_6PCR3Z{b|o&J{vnQz$koXfNogGc%!S}_>~5Y;p9kXZN?kMzge4a^iYu39FbvQ4Mj1i9R2IRfSu4|aIe>( zHeEdj`Tr4)O#(pded|KQ^nvvhTG(2#xc>TD4gguhsl#Y1*fUL_?;x!%4= z*|DYwX zbIsy+N$JC0g-=BV=E!)HCh9*#M#Ld<7P`&$n21$(rp0`Md&=o0Y-LDyOXe`km>UL3 z7EhCI z;0NJ%Yt@ru)`KV9o+D$Zw)->qWpr7>Qm~GE@^nV}YhkQ_$pO!hmMBYf9?%E$!j>=; z%F9*3{0tkeTsfOzdQJ`AFv3zaAId$b-E%C)OZ{3652b<&Ix*$v zQiQYLA^QQ9I%PJ%j$~@dVzt7+k5a!LojCb(QI8(;4k{)bmsC&0Ds6V(9onAPZ^kjY z^>Q+8o!D}SD@Av>GMXqJT)|ofe>!RHH<~$(2lGbJyG~SyBo5J-l}$0JrA%83CTT9C z5gfh(?KOAZ%(pvC64ofIUv`_gL34&O`=?F0T<%mOO0jE6IBKn3-l-czoDaTvqDskC z`U&#L5Vam^!j%`8W7-sVIBTi*D9SOaxb1%FZ?ztZQX4*lo0{{!H$0IqONUd3v5n*AP6r#slbm)@qhlA+Ay zNGh{;mM=j>#31g%1@pqzH~a7P8n*#}`~o!v6Spv%oemuz4`wp??f6{Y8o|<`j2ZSH zz>S=YQk+P6Vxcl%4Pq03WoR!meAJ>?DZn4#nsLxOX^$kfm+oWgwk68Rl%}0rB0UlV Zu<)^-LM?ODwf@&ef4*pqBjc~l>_5!PG}8b8 diff --git a/bin/resources/es/cemu.mo b/bin/resources/es/cemu.mo deleted file mode 100644 index a43d4a1dc46c8db327cd91f2ae906dccb734169b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68744 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 P7(aN>SL(Qk2XFZ|u*4-$ diff --git a/bin/resources/fr/cemu.mo b/bin/resources/fr/cemu.mo deleted file mode 100644 index f3f3b49871a8078edf991f90f995d71f14e80f94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73978 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 diff --git a/bin/resources/he/cemu.mo b/bin/resources/he/cemu.mo deleted file mode 100644 index 072e48c61e40f0228e3cc80d0ab8099a2fb8a09c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23202 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

2}4#iKRS*o|7I)3 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 R@mKH5d#fM7qmb~M{{q908czTK diff --git a/bin/resources/ja/cemu.mo b/bin/resources/ja/cemu.mo deleted file mode 100644 index ff2fadd85815b19acf6d96a587241bf73280a090..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68243 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

QG$oG=F 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 diff --git a/bin/resources/ko/cemu.mo b/bin/resources/ko/cemu.mo deleted file mode 100644 index 5ea5e1da1a39c3ac7d15b9b270c5a01a89a4ed07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71668 zcmcGX34B$>`M(eD#(m%Q;1Yw7u!tKj$S%qf35aX;Cb=P3lH8b^2)MK$K~YdqK~Mov zK|nzRqJoNTtzB(v7p=C|R%;jK-n6#1w%YCgeBYUK?!6(_-}cXk!*gaib7pyG=AC!u zB)`~yr$qrjr|lR7N5a<*3xd%*1;PB217zYO<)--WxwH$8vlne+S@ zDqb)m2#x@I!hK*F+#8it{cuJD^s?f5EGx^F|p`v@Ki|KZ&So#)gz42a{mD;pMQrc$KIn|z7B)R*NIT^&xX6g z3*j#C61W?@3hKQnC^?w{4~BR7@HL(rpq_iqyT1zczVARi_c~O0I^pi{-%$0w`}wZE z4~BaF1gP};K*b*hkAah*%F*Wi=R(!P{ZRG!Bvia_!4mjBRCzvvYRA9BqhOBkLT29u!5eF;>0DX8>Xq2k{FkA@4No_i7whF^g5&a0666{tOjwr*Y2z5J;B~E`>wjgYYc)E>yi7 zJ)Sm#CqtG0>rnOnis$Q4a`sE8bbk%^gnxyq$DJza3-EZTc$Y#weU}w=`uzk-9(TIXg&z(j52aA`RSuQU{!sZC4mEy` z^X{pz0(To!yR3w&|F6M);oDGh^?N9J+Kzt{rR{+~dlyX&Q{ z-S>ftcerN-RJ@^ZF|342?|n#D2@bm~2u8s=cqn`V?hn5S)m}e?C&52JOZRdo#})7k z_}4X@@J^I1@tUYXFoy4uX=u5%35&1|9_Kz59#sFx>Y;^_NXBgs(y6=P$4l z?#4^k!%Lvbwd{uZNQNCa82;pz<{bsvT~JigzzO7%ugE3M$@r zpwj&j+z)?-I13V+) z;yvp5EIc0nZ$Xu-6P^hF3DxgUsHUx9e|RJu168jncoh5+RJoUV{|=~pd;`*@f;XV@ z*{8>;N&XuNzYQgq`_{U8xD+a01JwIkpyX#RRC*6V>rX!XJ5crc4x9zw zhbee<()qsz)&4(+YKM=Z(%Fm3QM$)MmE%l!2pk7@hILT#)c{rgX|Ok32nWEgc=tz8 za(v7bH?N-$HNJfjo(k8)Uhvm&Al$p&JwFVpe_sog?h|kq_%qLU;ZC^U_wG)p`uH=H zyp&MLli`t2a$gCRP6Jf?ErBZ6N~rQa3nkazg&G%r26u$NgXJ&_mCj*T(?;+VsB$fW z$HA{a)!(mSANY6J3zkiFA5E z^4aA5Z-9zF+q+jmwZ}RrIsdlz-?`DHwQde7gX^1EBwrF$fl|M5`y?+^Ea!=dD20z4ho!iM5TwZpqm z`R;_O&rjjraHkgc`~gt@J)!c~8>(H-g!{pfQ0;dyRDEQiAJocx--EFMs@#vax$*8PsPx`~>h~W))!WBV@BcSceeHLx3qJ{}KKen$ zI}Pgn=R&1B1}gqUsQfiT)yFi?8{k2>XF)x`7^)suK-J3=Z~)v2mF|a7{q!GD>8Nm$ zqn)9`dqdURX;A%c6jZtisD5`f)N{8(<@;`^=k9~n{yuy?RK0J6%HLLa8T>KS`}6{GJ&Zk4YceLjvK0F2W{w64Sng-Qw*Foj)4ygKG1MNLf`TMc=e-mon?}VzCJ~LcD z8U(M$eI=CtTTt)$0O~z|f*KG00ySRrzs}{m3aXrwq2gT$^?Ws~fX(n+_#iwKz5|uN zPvBv2Z(geWmwBEA)lQY(eI=BfWT2j(1CNKxq2Bv7sB*sI-S0y6hYz6I;bZUK;RZL3 z?gP~h6;SPU22^>@g(t&msQS7WO1>U|s)tqZGWdjdm)sZxm*YMFs-2pl;?0CA&#h4T zTLx90RZ#7^2`b*#q2B*}sQ0`M)jxg-kA}a22g9Ad=+Zd?D*ka$@lS#(_ds|QJRcqj z>!IS!fvV^EFoX}mGvP~6_46;!J-+1X<4~yl9tTwqz2R_pB^(A<`tbLm+V?}K_xuZn zaOasWol>as^@j={bM9`0*wj0{58h;*W(#&x1i);&vxg31Uww~NO&Slz+>U9@OZe+ zyMG8(pMQpG?|(v-@_&&z$e#*Lb!=#a{)>c+W54 zMYwmn!^OK2j>SC_4u;={N_X!&og7~P@d|E-ec+J`oV<^QKf*l=9s?)dMc;<4Q2pXj zsQ&OaTm|>No9B4{Gw^uazrV+o?|_AF{OJex#=imX2Cs*zpLVG9?}jJ9jZpIUCX`%# z09BvCBKO=LQ1yNwEQ3dT|4~r&Pzlw}w?Unq^pQ0+1V9ty99 zYKPfS&n<*{&!dnk4j%IyLgG8p!QO=D;{Wpp-MDzv3RjME;UN6S!NcIK@ND=9R6gH@ zlB?gs1K}T_>S?EkT>F+lNR zf`{FEcY_Mw59<8~dv|Zoeo)Vy0`=UPa1S^Ps$Ir=_a#v6p7ic!&l{l1Hy7>??}9zy zN~rRD1FC=h1ghPC1NFX-pz`$zlpOpEo&XPe#J&GasPc`1iZ>1_e^qb?c$p9X0+jqF zq1tm3R5`y6Pw9a?K=p%tSGjbLf+|NZsC*CfJlk`m=Xj|6Ukne2^-yv$8>$@^dH3T! zd@EEw-}K>cL(M}UL6v)tN8R&>LA}2>RQRdz2sjR!T*AJ%XM6vrpx*l`R66fMJ^u&K zPoc`a^JUkVgdY3`H=SClXFI4{5K)vS~@BdZU2ltQRu`pQc@_QUq{L?%~K(%WX zR5|LQ%GD0_+!CnrJP#G`drWtQ1876mcS+-J_9QLJRiOkD&L!+>gx@t@_qspZ`XD1 z`9nRAg9@*Jde2!<@449fH$#>Ci{AepsP{h(Rjy~?QSeo$_jP*zeb&41GN^QiK;`QS zcp{t$Rj+H{uJAdibYFss|2^0PegM_~vryyUzo7DW=o4;S?FUt^fp8Bv0`3AYgnPk@ z;c2kWyO+bWaIb?*3Bd_%i%@u1#!;8U(1j(~EPdX~ezaSw#b?=YzG<6_ts zX5fzS5x6g04NKu>C^`Hk+#UYK^WRYE@4m^^=f0k0Q1y1M=Qz*Hq1xwasCI9IlKVU0 zo^Tb^`!_<~zaSl8J_k5`Qt?~X_q1xeP*b{yq zD&B|Qf6uKh{9rf+{}7%88{q|T3DkRj4OP!s@80o6=U)O<@B4Z8F`lLHLj22NIs6h- zJ+Ft7mtVp({1EN~ullmfM>ACWUI$g5x4;A7a;SJ4Ax#;)1doB!zT(!=i{Vh*Z^5JC z(O-4)d$#8o*a!d1px!?h4uki>JK@_bNcrJn(*Oq$s8h9k`C!yNq zyHM@%Q>b?Bgr~tHzUlU*tKfCGe+X62W4`6u@e0@*cPrHME1>FW6I8nI!NcK)Q2E>A z+m5|phb#iq&RQ$=1E*&J`40!nWT>UPDJ#jw()ec*sf z?b08rp3Z~&!U<6FTLUF`Q=rnn6DHw$cqTmH$8NqF4;61N><2rb>fv3e`aA43*IuVU z^^;4W%HISB!8NcPzUR5$>#m%GJx9U=2p?iKI zzEJIU7F7AphZ+~gLDf?RD!+Hb1K|^JKlnAMc7Gk}`Ja1k^ZXlB{&so8z2|T^ANPq+ z&u#L29_sm*pz7%dQ19>YrptFJR69(BC&4d3mG@StcDdJcHB|rG3>E*!Q0cx6mG7)~ z>oCG0xc7z^!czD>I34P_kw0_KRYIlz1*ra$hI;ROsCf6m9pEG0{U~Gz4mzOXwY=r> z^ChVA+z$_iUxp$4iFf}Msy_Dqx#K}l=^qJ8U@25N`a|Vsun({F?n|Mbp9&SP1xhYw zLe>9rsP}G!`@nBQmH(%(1O5?y2|oNbvImd-MG#yC?}FocFfKzqH{=~BpBKUtaLyL4|+f-8;VP@>K#=Pe((=?+sPn^P%ET^5Iv)<8U`XrMCbo zKZ~K_uZEYwjZp93`&UkmkAaGR9PAHIf(pMJs$Lr5;cymII!j?sxEkuYZ+rd#O5We_ z?hiaagi7zPQ1!jrdyILo6z&Lbhf3!zsPZg8wKE}e3y`j`M!j%KKQ&Gqi3o|~ZZ|6Qo}XW=ez z-}ha*hd{XpK*{wPo})Y`!E*d-pz?bcR6du(BjA(p9QX=UIwc=Cx!DiO-4jlRWl+!0 zgtx%kp~j1Ywz+sGL8aRt?hc1Sm1hh*3|OCvqad0&}629WY-}n3& z>is)q-F&ehJQVjCo)e(bnF>{(EpQOL4W0$R4wc^>a*l^U<+l%1zRreU8h#2jxB+s{Mw;5?Jm18$DaS|MhTB!f*Ee_rMcz-w&0am*8IT4XFD1jdyi0e0@BJLp^sn90;4C(pv@9PoMJc*I{4W??K7a0e^7$9{?4v5}pcM zJlDcAalZpkhev+s`qd~6-=P!Y(r$z8=_!QLp{s<+1 z2mHygujd6&?K%aj{4=4-dka+gAB4T&GjI?14pjUcRDJIJkt^Tfa6jB-gw!+kbX ze`tdH!Wr=C9`r+~_CEI$GnoaKL&?i7pSpRcCme^n3XXseL-pJD;b?fm-(5YXq3UbC z=Yvr3H^QrV&);A(?wWr(Id~1$;{FIqZZH0qYsZzI@4(yfKl0yBt{#JD<9-cx!u^9D zHopWtdYE3^8;-(14OQM};Y;v+_(k6TIL3fJxKG-thw06e;bh#mL)FJ`pz8f&sQTS; z=N=}1hrp9?p9Lj1Q(#Xx52}6Edp_^^ZK(eD22^`|3J1U*yY#Sj83^^hbD+Y?!~gFSi#=YtEO%Doh-J|Ba7!#AMH^;`G={43lM z-m_NIzaQ`2qdhC3(wXde8Qcl?l|H-{>U~q8^1JKa zuAE0emFEnLggK_@>9tC$f$nkin zd1)xr^H+P$hf3!OsC+-|-CN+UxW58-gWvMuKZL5^*S-5wsOSF$)qXo4?4COiYWzMD zD*t8PJpgL_ILEt3drpFSPXel*uZE@YR;c#q@O;_xO{o0lpx*x>lsx_ws-GQxh?B1o zQ17pTigyK6K5L<#&p^rjG*|=M;m+`V&)>ryxS!>Uen!)d9XOtc6S;oMbuW<)gaf#; zT>AM9*X8(cFc)J2_wRBYO8CQEhjWiu1c#E5ak&3Myvw-m!R>8l)a;|?9 zrk{}O7Or!-=5hU#F!g`^oTLim=Mv(gMg(6XOfs_#mT~x`y)N}n6|7GxO{C^J5!@ULS=NsG?{z!&b5KljY zx%TsAs4yS;4PhVo`x|+7DehOfG#2Y;XX5`H|6e(0a5(q-aOvk4#QVO#cRtuhaKFm+ zcRVH#-r>WU*6rtR{%1(DpJpGn*}LT#^1f%_KsXe>L7XvKm@oYX z_g8SQ;?lem{b)@60bwWLKQQuys1d;u{85{Nf$%26)xnSCI*R*ETpw|r%cXJfL*kTj zJ&(H*UIlN08XNR;s?Xn_@%uVqz47Y5UyQ$&(EQLegeUm^S+_)q)~hFyQYjmK`}YY)Qi zBQC03u&a-^BbznzJ@ko(bGOsl~$T=Tge;kusd zTCO9x^wRMbu-V-Pn~47{u49Nd7r);T_ER6$0@o_syLTc^yH8^){v)~ngs^wG z@A`8DaW5de9|89fegpTv;%eo7c0v3<5#Gl2DA(!u|Bh=o@$_>tX%zmH;=hdWGVggA z?n&G?6r^_%_rK(No3MiktMy^4x&I26e!l0^AK;w9dEWg+;vVb$=5YTw*STCj!*6%; zGK~8Rxb!oLXO8ptA3;>k?S7i@lisq3>q}f)h<75_`hu9#{ByT>O5fB^hIqr^i(GFI zHU~;)yo>8%uIOiGbOT@F8q0N?PwO?(IFl>-`J4Bbdv~rD!p6aK3j9kc=N8-@TsIT` zO-1JCN8JC|r&X!2f@dDWe-YOST+6vmCH^~lh@TI9y!W_&l(323J;p-a`b@Xdx5g5_ z6h7(WOjQ6seej>pH5>oKxYFF~XD)mP_fNcAey4DM4MfGZpA5VgzcE~=`gqfzZg1q8 zN%&XcARpG-Klc-RnzI(T&*T1wT#J0z$;9o;^?`pzQFo!Wu!hhlOa0&jGa-a0^_V@9=0sn#DH29K_`vUiW z#P9p?53n!kyw82~b2T@wMh-ZL>#IDwf@^=`o{jqtKCNH#{8Rq^P3{-_dp&*!ZvE`# z5d4L(7w~(KYhTj&wvT%z;lIak7VOLQGS}XOM?WdT>bSm!$IDPZm+|Z{?mvc|T=(Ob z6uJI||8%IIlemuJejR>iaZUC9f97647kc*yPx(#uVb|k#4DN5^K9uWrt}o(#-N&ut z{%>4g=h9ELf9@~vc=G>c{6<&^dN$8I;-4Gh`7h6d{BzTZ`)lu~$4=*}!hI0eQKa`X zAMbhmZ&d{Ub2|Pdgm1-zoy6H6 z)5X8 z+aLcUz2957D}0)}d49$70m4>t{|0fZxW5;&JP#(}_a4;Gsc=u+U+~X3Ut|LJLpVMq z{Cw`O<9>pVbFhDQe^^fbj^=vThuw(Zo5URi>+#FNK7=2DUk>+!P(L4V?Z@>?uHO)6 z0oT*qe;3XpUMu&}&+)k1xb$-q*SmIyToU&%{P*H23CgX2yT|-{I2FaSp)_xbGoO5_g(wPu|;(-v+Kx z_#eUbJN&0W{rnyOJK&*Q-@i34g)gKMU94K819yBhE(L^>9brzlFVhyfpWd zxgYG^YjB^!bs$$iu2H0M5YHS4FIF7n`qJfnVn0k>EqzkHOWr zpW%Ke)K4?_#}Z*z_&w(pEarYb_jf2xX}^m815TM9W}1`9TDgiG`GEd$lmYSG!1(U; z_-;sbx~(-FmTYVbN{7_cw6!E!lVNqDr6rYY$%JJWwKYslH1$$gYwFr$n5d~qw>7om zuB{E5lGDOuQ)|l%oNcwKbU0+v#bLUs0r$|BWMXP_I@Q#g2}*}mPU62rU2=R?*qE+u zYe;6ohV+yvsirCT*C%VHhIQ$du&ue4mn(jKswtUCa+7Y#q#Kf9L!xO)TVjg&G?V(U zbn}dGVycy((Zh?Kjd3&|VO*loo@+^_no_Od828?=Y<$&(WOKTuweR>U{~Ag;jBHc> zOg#ZLZ4`6(m|$V};$_s|4r!_UC6EsP57miId zk(m~J+8UeP{~;CJ!gJw-A!Fs!T2mjCj;JMHQ^;o6+>)+KHISO~YDqNLr)t9HM9oxc z#r&GAG$R_@8pvmDIH_uAI6Qf6sz%i|qG?J)Dx(Ca^Z8@x$i%hjmXz`~($XBsi}kvZ zZ4C_>@}F!9N=NCf1?3x+Y)ZBy8p2GnwUyQ((NW2k#)M*~Thrn2R3?#W#8HqlvPUWO z{z6}z=Or5`_2{O!szx_8BWn4iM>l0!6AcZn=&fqaK3)bwq=A~RsHji^*CrZLwc(J; z(P3N0T6Z*$rIe}j6U_-HDHo(^Ief+>+9+gbsl~2osn+^%)bOEUrnRjaUz*91Mi#=# z%3)zmx~Vlez14(}*C>V;&aufx+C6j)=pC74qJ;v}uH!}y3rfc~C!4JQ@OVS5yv=7U zf>Y#CP|Fs8l{{Nh^nv&$tWGwhr-g}@q-8cay*b&^ znyCoKBolPbbX{GTsteniGHoq1du6IAq|q7^FnUWRg55Ty&3e~_WL--#Q}5~@$>Dty zl2af(U$>2E()9h?yE63&iWt@;T#HXgHp$u6oJrLt%P9seFd>;~Q;(aFw1HtlTP9;| zRizvjR6X!`8%FMah;_-HedYFFA!SID?jPwX7oY#6=XzB+pvg z#)#OfwO+Z4A|-5G?GjDGnlE0esV+^DHc*GwR}vB$2F)21s4RoPFtSZfO-<2NHHJ?1 z5_f5`g$J7&Qq_z~4dHncCsy{=e^p^i@)|^hp}SvjmLHQsjXdWB=Y&m2R6K?w4Yl~S zHBD`z@qIv`J~ZUw@Y*!vk~JT_qF|n3nz1>=lWJo0Z?sGYk)Pg%3q_D9q<8s=RsBYj*n$cS8 zNh;xrODXW83oscdpQ%=RdzT4UJ)3S!zKW(>?`)>OCR+pMUR@-g3|rDvP~5E~XqG2U zCuNyl72)Vs`aXkfM%_)LZ4@_^%uw@W!*Z(l86}#M>@WkkGMX1JHBuBmO(hslsYy#y z<+P_sInykmt5v$no4i8}EG45YTVcJ%fVL)0XvDIo`q0W#nQFmDMwxI5@2#6V|q%O16|p>2$)`mO%n|ReejkDczP~Le{I?Fg6VpsGgy=iA(Vk zc^PFoZ)LIyO!;*GYh30o(~1_8N^sYjw*SI1bhvaQIvXPvO^!c}YxAEuH2i2dpAj+u zbkRo@xl4G)s#p4NjS4|&GMc9~B%sqU#Wn{+rl&H&&;&V_oagTbm5ifuQcm|_^o*le zgAYyBv{k2Sf}yDfbl^k_H!ZF8wTT(Q&~%ysrnx00rEW%8R^GqY_I{_xua)eQFD4%z zcdmwHU2AmTlA2PF1JRxumW|XCc1zYvo#dF>?tHxlKeQ!14SigCJXeU)K@-tMoIWP6 zE_dp%*1U`a#8zg4VYEoKW_IqHPHd&3(dv_-6eU#1`jCc3pZ7xy<7P`+lcw#SK6*iK z(-I>ZR-@pw)G&2E^~`C~d$$XTJkT#_mGi5{kFy6NZ+l2jTNe)LJLIz_*&s-s(POnX zP-x^8zcli2X(&pClx5a6DBFVxZDzvO@Q_I~HlqS)jOorZEW3WtjlD{wLp8K!T<2i+ zQ?jj;kIbPqRz4S*WgqXEGt%Xpr%(Vligchx$s zwxVRb+Nm&dCY)jm49&zg(%1|7vxcqcC!If0EtOMqU_5wKl$1DANhzQY+OOh7WpBV?#|mS5*;TTaV(e1{E(muks2;VOFAe0QOy}?U{tPJ z!?kQele?2zQ!VhFG&(jY_$q9|mXcBLLQe8eJIO{KY@o96R_Q9qx;i?9f7Iy=!7z(M z=k#~PMqE_PmbwI5aAd)V2F8*h*CKpYfGCKXl;Bf?3u~YOSQ;vkKEgh_zyyDTT#fH5EzaH6>X}4b$99ocPA3S#=7g zK0nFY(COn9`!U$@pfeo}t7p1I*-mDrwx*lCO4N|9ajOg;8ku<*C8B@^bbO}YVGWvl zhc%>71*)c|nnRSDR>oC{1R+QiV&aErrY>|pss%L)ZN6qkH_zFuV#`*SCP`s*(wfAL z=`3?qC(@NAAzDexm}K%{whZc~fdU1?P&9c;^%zCgsOKgW({Gq)nF!1%#VhJkQ>0m^ zWO^Y0ceK*-@0Vwkk3=-|jZ3z&8lvdQ7TK1RPWe;=9IsU%yP6r9Go{}75GVIa4}r>Pc&+ss|KxZW6ZGr z>DKd!)whh)s#!GkksR8x7~39Nx({thGY~eC zMm;)$8-}G6FkrT(DLc|xoG%936m{*jRE)_h+7(yM&^s772toH;5?U>!kX(c!T5 z+RNz6wdrI=iUS(53~w~N3#Zg_nDSdxD@^e%DxDi){GyTfBx~xMq=Qpzx|y>F{HIJ^ z>lP=ij1DTEA0GUqj($|&h2=L>pRA2MtdC}i#!50_(oB-p(+YZ6 zTLWfq=BipOz%!aj$c)g{Nw+pJS4C@Ts_NbwBpBY(-9A-;fjD0IY9yYToRN`kXZA?u zeOLD=+A=?DbzP$4AD(y&yNo+TwCT3XUAD1`ka zvE)vVM}v+vo150q042d1Z3^qaO#Z`K64(x9%?O6si#@{83^G{SXM`i&*rO+-u&Svu zn2uyKEc8uIPhxDB$X079;?^H7QR%zdC+H>wtBh>3`A4+IOR%`}Pj(Y-YF->c%QwID z_0LF|v4xF_GHBO0a{BGsyu6vc5p4Rpx-Nmui@|d^ z&39e8DanntS!|a@3+Vi!w>OL4?z~yW4o}u4n318)eGUcE;RCu|Gh7z0FOs!a5V0}M zf9#Q@%+SYuE6Q4C5I33*16Fo6GrL_B)^s#MI^WI8$Ive(s4(vNiabMoJnAlMITtYcHE1>ogn?svXV|7gvs$FfkloIdSy( zao8zL*NTMO>C94+>QH%YuCWP#9e791MMmX=g-MTy^(vLgX5C$80_A##G&n9(>H)?r^` zu~H&qh}CwYA(LjU;RYT*?V@q|Stm*)+6!V~$?id2Ps-}{!AEGgB}d+#WA-W2nTX*+ zyo!KHS(;$%ZFXI^n5ro9)R@-N#(11=Dh@CuKJSJ3VG0uH)5TxvmiwQ2Iz0@Xo)vbh##ShC%)01R1GJEl7#l4O-Yh=I3ZwhpMM0y%4uheyVg>Nj^ zp2?Q3ab@d_a*}E^`+>bg8iV#b3Ub26uf=4qja@2+x~NlVv8$5s;O9M~k^jFOU3%`7 zHmd%YA1jz_v}T;~+4(7msdWh=WfdxM`Y%0XvEwTE>_=?7-M_&tD+;I3&v{O@+D*0w z1tZ#8Xt}_Sgp8Q3X5-#k&o(;PtM^;{-Z#@$Bb5-nAd07?U4G&xG&kYZ6Y#|$lk$Fn@`Cr;k8cpG<8=J zXgjOcB$3TAZvTpKBJ~+>del>BttYEFj^hoDwFT+3l2QlCw56P}b11d~*7TU1u&1Wd zlrk<2YWE5b^R65;l7B)Ub9YF7KNR!4Ipg*vHiLKV=ad0Vi$X{az+&JeIN ziIgPt)Yi64L1x%jm$l!jsqk2dNqW9qeu)+7MTH5pNk?IZcG4Ly!pT;?7Noq^CUh#S zuwGrCEHVxv&WvLD_ga+LLbzS19Rji;Hm{hYsgA_R&2o37Q^OoWw$^oUiNb3?#ztF{ zPiTHsM+d5S ziz1zTW4?Q0EaR8&CzI%R{(#s>R2yR1fgr6q1N~>)vr+>_T|7W~;Bb~{WxXsYT0Z!t zMxmcqL4#4Hc6v;<0-2htWE{4kXp~e){b#b8akrY>@^{Wt2Z3}%#{9LE)I<3VGAH~D zCnlJZZ{oQA---c2yF7F>-%VW?)UhJ7ZDaF~;d)@5de zDzKYLBBE%vKt%dSr6;DtvSF@tqk1`ar233%NmN(s^@W*oi>H9F_#9%9BTSlJQy5Nb z6~xYjrKK8xyZW+Bbc3FkP};5g&0k7UlpKmjiv|tllEsP+R$*5)WrJRysAm5N!xNp= zZ!YH-H+`rsjn=GcnGE`bt<$vseQhd(_K%K_o`|ZVWVKXRo?{Cs>kX>XnUJY#y|h3T zno^WMIMjnQM%8hVF6UB5`(vrr zsR;C)EPv;uFLKjj1-H?$RsWcpGhWwoz*YUSAjn?P#V@bVjE;5>dBmN8oj7)QFgjCo zSj{i+G|jOPvX(=jS=>&fssBG7T^r4MC7otFw;_cQn|;C)ZS#_SSC!eUi;+aJ=ftv6 zOKvYeSf|-nBfD+Q49gidy>z0=M6sSj!)930`bpASt*yVL&bcxxNq}^MSeqTTd8w@^ zZM~tD**OSmA<=+5)N1p@H(Vbq+m>XeRR`fBQ9Y>qdOznCzE{5B< z^n*;D4M)4I&^FFP#~FCN_IB$rwxM@eYbg)fN7A~SdXnTHm8P40v zRG^WADYP*WSUh*u$L1-UjP1mD5T6^rAeod1=C`#1^XcYXNnj_;-TzI&nCOFqF$u&| zQw9_47`Xf-#)J3hi9j7!Bt-hi?DLu%*qx*sz4J-;f^Tw?9%%8YUu_ zn^@$;givQofQoRuKX;?ajJeKl0vB242J^@??7=!UHzQyX5{#9$I@Y%C;(4LgS)ao) z7Ho7oedmy1dv~ncZ;`<-y00=V7!7G`s)l7|JxbwrPjM8)K3W*-&*#=?Q_XfCoweDv zw6ta#6qv<||M);e_n_3YPZc<9r?&>t4tFpv&AjqQEG&QJ~Yxx_GXg2Ok9kjzPsBUs0<1YxtPpq_sP}PVLl@rE~Wfwv_ zH7vsI@A0b8AW$pcai&l=WF21=?ZsjZ8fL6`nW@#xqGVJO}#D$F>#qwS^qf zcNUd8jIIrRizVZRJJ3c0yK@2g1$j`g-mcVPe=s;q6ri=Vq^w8|JK3bD|;tDF=S;b2v}m zWZv6lL%N!}U!R z>#Y2zQ4{EB$Y_#pC8TZm50fTDpNX)`6Mb)mk;AuK94hJdn`(uDENXd!8N>?3HxqeU zTNGN!Okca6Gy3i+*IIe)DOOmm%o-5gH&#l=yli5fpwCIdQg*B96y7l{Rg3w>Im{Bn ztp*c$iRyu--uvOijaqK>B^St7{*0tO!%ETRj~H0R!GnQ3kl z(hTG#4{eQ%*}(*ikamhb`p~V4{t)OtKYUniM`w#{8n!?iE@RWUdHIfL_xZl-(u(HB zpi0}kfm6IZly7P8`GLxIk6oN7(3Th8-#)R?92^g;q-352do|s7*&C2w zC5uvthWxx5}A4X&VnDk z+tstpB(~;4X1!Vy#gHEBCz@DA@`vqo)`iSq((-xfHg04U+Ag;GpFhyy4)58?Or{Py zXQ@_nlg=orx00dFzht||{E58(v*#vWRQM`iL#DX-zGrin>m7Y$_)AuHMG}r{TT!w;*%(jDg_F4 zOkbbT>WNTa0;(kX7?1~00hv(z0W>*vA_U2^?v)SH+T0sX!xQ@PCsGwvrItm_Eu$DA z+|Cn0ekQf^8D6T$M{Hl@P@QY>l zn5_)mZHt>`ox>SJ)FzgCq%D{$-y_|a=Hldiosg<@WGug4F{e%<`2Od@rHr_!lB2?^ z^hfF4Y@BhQF1lShS23z6zKZJllx^E8_iSTU$6?U(uti|Vb9-1r!pg}X&faJpQS4Bk`Btc zpo-Nqwzufh>niLeI`s{#Aj~l z=5qHlcS|yjw+`!@fm(B{ocdT+b1?U^k&IxOp*^drR*KKrqN>&z96$(5Web5%RK?re2!ysYH6qYQ@@>W&u|?S_rm8k9M3~&8PtB{GQtWoYH9`@B z=B~3_a$u#67s1{-6EMEI@6$b`)=#ukBEdx25oqV)*ULCaX544X#G%J?_8&uoZsU)? zgNbSa8)VIXM^aRmC6#I_XDromR2#vn+fof2)AG8TA~z;###dF8gm%2t9JVKCFOIEn z&Af^lat2su337T?eSD~y{WH})OHfrJYL|}ck-js->P=>dDMy=jvxPPoW-*o+ff z8K5jeEEbulGzHVJ=GJ&BZn3k-G+yzf>?+H2JXMNXUm4^y9a3vU2X9v671_4ct6ctl z?dn;x;KljK?n+!ss~mXogW@-5>}C46=SFD6|hkhQwhbk+Ok_B%R$A z$=hgRjX&OgjMtg=3wZ`BBknQ3CG2)_y6mjRRbl7OwtvK|4>mf*FSHp-4dQJUzCGiO z<|rRpaJetb*#UMNuD01xU?@)VTOw@SwAZ-FLA^q$M`EImU$95osL=|^wmhK>aK_H> zh}-we{u^C=wCjE^kxwnZSf9q;F~?Xr5$C2{A5X1N!f$$*${v8FEhE3iUPOwszy z91LElcWl4cy5DCI>*_W!=oj|Lc#LqO$LkYP^){`R8IzH{x;9f?O~s;a?006AcmsLC zuYsZL`P`}x(*^hfu3!N4Wn*_p{oVSmYHTECi1Kqr`)&70R{p+btZ*OlxOyu%v11D| zeoX)$O2uFMSR{Yw-)mzTW|F4UEBg0#HSa6T3$)X9HK@5I1f!f+Ga{P?du>`KO>`e2 zFiAAEwlxM5&9*UmxW9=`tvK&^TZM7l%_Zg0lwyzhiGVqmX-6%|O0hF8J1GB1ap7NmWCFN$v{= zQt~65x=Z)I@wxi#jyW(NnE1s5tm%=imy$8lxu{ZJ!si5`eyLSFK)B;mY}VQ5om%6n z=}D0GOqKH`3H>9Ju50D@jZsxJ3A&VZ0HR3xD8vx>Y z!A+bM1!eW8ebrA(Diit`p!-1N!hf0?bt*T;*oZ8R-IUaBQnr;#MbX;ARtWCHDtA=Q zHd?((WoK*LDJ3^v+2)BA!v+)WH}K((^6FGUzcN-$YdfW=0Pef#|4&)aAn}=5upuLG zBS(IFd^?Yxk&in8>EnN1r}WXN_IU2|>2&=f(#5nH$=y=n1;duB^LRemP6McoK94IL zG7-4O!R~z;mGiBFam8LDU-y|AgFXWgzB<4ge^ln2iiHfJg}Yps_4Yq~QkIeVg>G-k zlBC}6+t@c{lbBoVlO=3J=%wyc$5BYN&Vs0K`8wr0|3GmY?evFk3y#a!%)lpXY^Gxx z8WiMLxxqzM{938tBKFzirKsxw>Z9>m8ySqNEQwjK*;#T}~gt{ z)FED#!|I{YQN{D8g~Q0qTA|nTh1x}?@}Sl!Qht25;{l%$=#^5f$qGKQFojkpx7_&P zp5;_(K}vp3bPuY)J`# z>(h-%HUKz0#SvV(ggsqf9X)QRy|@2ebpDsmO-pbTX0m=KmCR%8AF-RK^KKgXrVzmI zD56B#WwtYF&co|R=5q;)qf7iMCjW7jThE{rOLw&HEX{E{%ZclFw#@2ge;CsfuNc&? z|Ea82uFXWZB}3>RS~$61%%>APaz&c0RwX*xH`uZzS;WUp4P?N7aM!G{K2m0#qDb4s zt3g+>Xo|KREn`j7D}TP20fh|MOsyF78e6mViIif-9<*YoPxyrVNd<(ReM!|;akgY* zQ?}iADtZ0I_9OVEMKTiBavt0jo3=?bOw&i(jBlX^BzEKU4M9OUbbdH0o;%~}((1$K zvK&>#z{)u$wF^(#%FPWAJ}usWNkeuf6r>7S7Z7hVg~r2@C7y0W$tpT8q*~IiwyD$i z?%|Y%bTy*`$3XNgp%%J>O6-t-xjx05#vC0Tr*Yo)WkXnw^y#~Z!r&oeCi30Gh;b7~ zOvs1lP5C7ws)ph3`dWdv|K*N4|Ep8rnnW<0%Lp9Fg#4WR#r5Ci*FKsOM*n39O{sg) zP3SCYVl%|il>T?eRg?^owYWB|-&i4gkOose^3}&3?w1EpF!ifxP6jMts+wxHO$tS~ zuELw_*n@gu{&z%a#ViXlIrc(izk}3&g6iMb$m+xWW-fcjiRif2@U@aUtU&I}p8Lfv zYE-FNPBV(6C06XT=CY%^?oro?CBbH}u!jml+$ckeDR&;6U*utL)FN5B0>9TIvJgaG zffiB$n&>pF&NTsDLci$)>n+vm4N-yp=A59QZ>XG@DU=%@r^4G*I2&v=FJl~oj@jlB zbBf%+Z)ep~K%bPlbr@5lHs5u^J`NaL*~RV7lorMD%cO4n$~BqmcSJ@XI=DFg_m|kA zoVZNUZ#A)Xb?j?jc?X*7gE}3P52j2XU^%KEaOyyJ!$2SS`i*MH9bmP!k@|Gd#2U9H zXsWBRCz_hBtWNuaa$naR`Epk|Vbbu*0N44ix%a z#9q<10ix@cFwtkVfiItL*xJ~wD&ag-6l_fvxTT%$1MCKbHFkYrq4#IM)J7L~P9<<= z_Q1Zx_rDA{;CEoQIdz(Eot)BTy^+Zxs$aA2e8$mR_E1?e_>GjJL%1Zz?o&(PnbA!n zvk^zC<2xNFOY0-`^3k>BUYQwW)mBnDzI+15Eu`L*4`-Me6b|S&U|@N_)5`~(9`-+F zP``e?`<>CRU&$D3qvf`+#pbNvmUA9c_xtmodGDrs!n5jITbl>hyT^YQ$OyL<#ky>+^6P?SG}pc%-1byE zkzC~2bHmO>>$8uo4Rg;d+}1JE`7WE^dG9Lyje+dy`R0?|JU_c)Zf@D8&ROfi&O6uT zmTm65ZF5=aQ2i$Vg1LPp3 zO71LU_V)_13m0Zr+}hdRk$rSsAjih`ZO>B1;k|-w9Sh!@L)zTtZdy;4>^6JTrtFH9 z*^UP+8Nv!Z!fl&ol2C5>0vGr8m7NRQ?Y47Cdv5tU`^`exyl=LBW%l-^+2u2{_bj!l z(`z@*&n<5cd61lUE@}^RbC+0Sk09*X&DrHkIu~vYJLl8h`2f`jHC5=dctQ4wb-u}O z+nimn)cycMZuzR*mL+ASB$it;%igKmhv#G;K~8l0=z?t<*W@~0wDOJY6?D#@pIi1q zS*dQx)AGA=x2?42@W`!b&u!dvB#CUtk}%i4(#nEQ_RcwY zDkV7x8MsvG-PIkqrI1~6pl^w}w-$H>mYp8Op3e)3DS9IRLrgPB}No#J=ZMkJLH4TMG zdhY(UE((%9d#*%aQM*-=9;4__xt?)8b+~A0)N?K%Y~ISwdv6WnK2laXhL278?3dc; zoHHw`28(53*=OcvA6%DP;-q*?FReex1Mj!rk)L~hP3N53^|0?8l4X)qKY4bu@72y{ z?Lu|+N7sdsJdPz=?73~@%-rl-O$P9{zusWOROg+WvbWRK7jLq#@qFsS{>t{gg#u6* zTN&9hC(LzhE%dcN1Hm73h&v!-*;1Mc8GSl8huY^o)F`qa=6zKC2w-;IX4;nuQ0vZ{ zo1M3^B2>v%ZQ8bRg(`6Erf}PaP4qA|%WC9eZb05?66GSdcmbp73KPmoTIbGnJlHvR zU2cnsW+{G=NI$lg5i!isyc$t>X3Ltg(g{==!|Ljps`GXR7CfTic*W9f8=OyWj>b@{ zxaSt+<{{YmuE~so7m*k1y_7Q7zSRDLMCWXi1hSiZZedwz6^}mEt{$^&N$5&S-=eY@bzKt^ zBhJ3XS4rHpsRsx8@E!2Wu--7 zw2<1VV?(jOb6FqFuA$j(zTN6Uk47E0aOB0G@NG{s|LE3cm)rvjtgn(gM60ZH5&=b3 zRxr_7Xf!go`lE8P>kz08&Ax6Pv=1Y^z;0k}@zq70>x&603Are&M*?zgR_^wp?J5Y7 zB+an%uBFtkwPo~oE8I9_dH}ujrgf$cY<{{dd)tESqc8B9rR|+Jtt7*nY4ozv$@p}j zM~KY158-twT^bA6c7MwvcmKlN#`XG376sFfO)dIs7SY(V#`+gZLT>TR6p4`BqWPM8 zpDtGVGepFNL zoQ9_vl|1=kGb=870WEKhTChMj#A_2x8Wd@TkxMPefH!Ae;PgU2j#&;KS!44Fc=+@iTwq~&wtmR(`vbfh);vBmcnH*fe?`eFsSM_$O?JimA#RiaL| zrJA^>bIaPf)5O!cXodF!Tanp({z`I-7UiBn6Gcb5>xKA96A)#Q___N#`osZLxGS!k zq%5KK6`iw}1*Yn*a1Db(WopiU|1V_tRO09PBN>HC1Try;$w~9H+HN)Sz-Yc~zJH;L z;j~NX74w#A&N7+awq^cc{i>)g583o0hN)g zZB#$gl%|>YfdzS;EH44}>(WhTeU74hxGI;*#$(whXKFfkT|`Z_j77!9_Uy(v%-xKp z%2u{xX>Rs1iKLBjj6>0=Lz~)=9*yi8@ifCc@1 z_<+ts&^+AIjao$^9J-)xioHqd5LEi-Ig7v}_X-knaN9d4z*XoK{;j>~$AS@;Co> zDOMe8?T(JKWKlqEpwtF*vt0X%+}x#1I4rRiF=*Le+UVAw-#V)5p0_4s?U-9c%{)9m zxA`H&Ql6GpfotVmc-p5zxz#M3P#9_F6%5xdKns=b7qm=uFG^Fi+1UOFN6wav*?C*+ zivy>tXCJ)P{y0a03tP1WU0)@{zGWxynx`0I7X{G(#W3Skc9mk;3t@KAe94%U;QT<9 zeQG9-SYVhXBZ29uMFh{W3h1Uoc>ybqe@}~38`san)cmDZE z=tyV@OziEs_Pl@>z28UHRCy1xfqx_8muo?WAnJ)a^SmL+yUyn?X8ETcB$A1X{a zswtFsTN8H9TBk{uD|-*xcgG9ao0g!a3=Wt)OoxB&A=4^X-ep7VY*xqUB88s*?}RYT zZ+q$mHJ0p9{OV{8kyI^8eRe@K<@rAq;`^3AC8~(IX!OV~SX07?gThPCQzaDsoshib zjK!`ZRY!Suqj7hIL>#0l*ot4$7c1HY>IQ2));(^Lob4Bcm?kthq9HI!#x>^tK!pDb zAsPZ^&x%G#I&Av}rP1{b8erWRT2Kik`FDD4J!&hoAFci@ZL<(YFR&$W!Fa;D%a5$l zJmp5_*(z}E;d%L7$JOL2Pqk{DeUY8@l&)N$X&{6hgrZ?S-+6Cg(Mw$KV`!6U-)YF| zWI8YEg47aHSlxt`5_t7LPmUb@Pq^81FXAKm~b6gHq<+9Li_A3jmVG4%ffJq2- zPD$|BN}LhWn${FA%`e#NFaktP?pwr7t-qnlsK8N;qSIpDxxnRbm$~c)gx5xSPoekMAEw?yB5v5yxjeM z+4yglIGcUW70shiNk>B(kX4LfJA_njkN?5JNYUgu}txHlH{lH7*&Z2JOM zH!?y8xu;ncvk-Agkn@ywph=)xPr|Uf`LMvkWe6&uw8C_UnTp z<1tE3L0IRkO^nL9dCLL~R`+X;4$u?Gh)fS!CEoOe8K^eR)%5KrAFE2UAA6%z``qjU zHoueS&fCy7S=5m7qw52itUfa|s;>LmG1~@i4V!<~hZY;AnLnKXhg{4YskHFpt7TW6kG3 z5t#y(vIbv-w(je->n!UlC6eHHRfSzqim$BL%-E|o=N{WC@l>liW43G9?i$B+75_dW z&;q8+V9cfeJ+v~AS=a8c{T4a$+XThlZni4+Lz!28Zhrv_BZbm%?N%?$>9)ng*e)I9 zUKCsO%N5#@DB0F=w`Oeej_POjPYQ=(wuE9bDQY3Wd`=fw?2Ra+_E#VCJUpkXj#P8AX70)#hlLjA40g)`@A(m zqPBDmYM-WNdao)!Z$q+NxHM22{Zy#;!HMGy}Q zpA#`YEo@0kG$IqMJR+0Bl6Bg7TgB2MQsP5Ql4==EDvQ>rQ#s=n6{#k1gNNFueKXS% z`Jo0_Y}9BL^*W4Aabb4J0#$ybk+MBlyyS?~2M&B#M_eK$oxj~uI2D_$MH_i0?Vqud zvPIfW9ns9f3_=-@xV70k=lhKlMmoQ%7%(w)E_^{-lk+<0nJ;X6R(;546?u>n9%$@y zO}l)llxaWI`q1uIwZ4qBYw=D_`1#S}9i7jMT9#Y3l^3w${ck*Ci&M-R^Hxw+zoIS3 ztkZ#9Z86C83Eg7c*PO2r#u!ZgD2fXtAI=0V8P(F7)k%?#V8X#4j4|D+AYEsL{U0Y- z;x{tABE!Z~cJ)d|bH+V-lI_p_tl zlFj5ncK2A8WQ#0t*dT>$T48P2wr%}qkb_ujb(4Uf0ir)aqZYq^ZM;Y5 zRcjM^M@si`RjE12dxZ+g~Lp%Fr$Yrc$(8}#gt;@`wg|*CU8N_1> z(n^61;b1lDn*UOG*j_zbLBK zSUB1X0wa;&>@zbFZMJ{sOV`30yzQwC92apTuU~qZG@4GA-^VH` zabpIGfF8UbQ}%YRdU`3UOTn&f?zVMqtASb#vk!Ji%i&ax)2qwqb<~cx8 zXAImfVX4PFC+Nz&{tOJG7DfjVli@e#=X5INS&9X4-wryL`OPRPaqe^laI`g(H{|8_+ZX-`L zpn9L1Kq!xisZ;;enQmkEEy`sH`YWR|)|kR)oLo_hS|8uZ%Mcs)WS1fqTdR5o?hseO zlphI2q?OY`GHn!0x^6c%^5;BDD4%v#a=h0f=W8;sV3v1V)_i8LEfKqUln!N`#5RAe zUv8H;Q~jzXltGYg$gIb)R2jBb)5=u1+vJP?@Q;t_HsrL)Y$H&-L5#Ync{9I$D~fNn zVS4CHWR`(2-VL>-mtWF((dgzm-{4+o9>kJimK|H@nP%$>;j-plr9aS;9{@FGtk|4e zLU&&@A5FO1_!KxA4eUsw-cL^&#Dvu+IU?W={Fh{HGOabVc^Tt!!@UK&7b@%%nHzBvrskNha4^JI8O`d0lB9acH4~9B;sW zm0y#G`DbIL!ls0Wmgb&c=l-OSYg025beAsusZjExzcZA#ofieGneyVhU82rFCRN+I zpmfCTH)R-{3asA}F?y@imHZulcZeFGa{R2oA0n#iqVnkv4AF~p+>;W>5>E@th7xF~ zv~vK=RXW5|kc&kMUEwzc3S~#Ku5p#ae|By$R!?l`;2<7aTbHe+f<=_KgXiaqfE+Pc zO50|4Lg!34IVgpJjUa}12?csInol?B27V(s-xr-nK{5Vcac7fU*O6S|JH{$N4jfd3 zLhT;dQ?I}t*zR6|7vV`qK>R^~B-|ofO$uy5Fc>B&uq}5$6h)C?a5EYL4}0kEoXo7M zd*1_Fa)j*>@a}(QRprmglT~AeOYXe-fjx?bndDsb6?a@M$kCe+>f0nL9}O>PKpT~! zQEuY#Yuv{xUlfiq+TMWI)6>g8?6+rjBFIR6%+sML)8UQKzuoAzH3f zKtRx!y>-Mp5!?Zi;t@IuFxAKD1;R9?Zij9eB1woBii=}zNdK6jFncd2^ zmk45#{e)jLl5Acd$MT0JQfM0WOk@B;&a+$XogRZlIKv+^NYgxYK@XTyPs+E+U5VIp zsX~6=-(cN&){M0Ocee=yM8D5JHi(VWV(|n=RuEP*QUOSAubwE!Bn}yriCqU zsEZ`@4?LCF-;Zon04Qv5QA=U4+y>s2{j}>OU1bBp#5sD%9OgG|a*aUWt10|j3(=?Q zi{i6q59=zj|A)qTIscW8*Zko8m-cQ7)TVVm3M(|2(Y-UnnTor|5)OMq!)$APID_UF zw@71T(4GeAV{Oy49fb}|Uqv~eGU{?B6crMkkx34R!5|yy^<8}atzu}6)_c$WNwCwu zBlqTT>JQg)l#eE(Yy z8g280!hopH2y<-uXkWZZaz$+P6y!xplBy>nhG;s)>d-uA8fd&~J+g%brkTd}PMV*| z@D%0e3u;#IXgw#=^*w*LFD)+V-G6rR;(d~5w=^yliHI-b#IU^tLQfupV+?&q)ElO! zWX@nY-qH6wI1Y$}NlbIbfse8qBA59v?&+fsT{LDV8W_O**_gD5=^qnpxS%6*0uP~U z*Pn`prc9-)OY`3dZbxs{Q@zc2Icqidqto+%A?i3+(>{T-9^Lv*+sh5dW|nMt(42B% zqq!{X(e_b*#7hEr59Bp6pf;?&{=C#MpCU_LnJqf-|C$}Htc1t+E0V5q!BS6ZAfCUv z#h`i8eE^M6K=EUx>7t7;yucZ!%qrcmf?^@)Lvk-1FVT$O^VN<#s!$~feJE;_Ymf2QJKe?7MhhI8QBppBGZ{bY zrNsog@&yH9nEK&3IodOSop0}rtEp{I%rZtVAmjB8$gEH_5Y^^bCw~bYukEL#(*EPj zW+*Zv&OW7t!nKIIE(E&QJjk9?4!~dGEW7pD)qLhu>F$SUmfTuroHlfT(J`tTH@=)M z+~1LR(Cmdw^;jJjyH1riL5jkRB&kWc?>C>x9ajAG=H=??w|YHl)@675ap#P^xmcdh`w@oyo7=0Tpd0E0$65*jf;x^R2gFI za6jd->*#1kn?z_!q#jF@G7-77xLa@buzAeK=uAxB-%S)RiKfc1$Hyx)4zb1Zt6KkN4VEe6Pk!Lxt9K zpLW-(27eEs@a*CR0@_Hmy7KUSvetl9MUj&0g!e-K51KWTU|H(5I)c@4Kqa>)X5zVR zVh$1JH9WLVeNx_~llC-oBU9Q;Km1c@=WvH{1G0_`_tUjFZX8BNN5r98p zk`jUCJ}8m;lux!o?Z~tI>))`Km%?;r9zTD{Pb3kpNo%ASN$baMNXaN)TseFs8?~`P zR+7A+@5qvhghRcAF%$J5+{p6UdV>s|YGB|e$`(^?+`gF0mABRShkwn{G&7@@)#Ahvw=sL_RoSQ)r>k2($o_b@ zG0#y-Q3+bbkr*!VR2hDQOsP~{COhHCP!9&wUVIY-*fcZtkF)DX;^NRS{3>_S&Zjn)yrOZe-Z2Rs$WCiQHmATm7sl=Pt1lmzhh@2C z#Ej>yB<4dW08 zRmOpbJ1il9rjM$@@mSfW=Lfp5JZV&4iX^V6*5~zpGefP|0sS_a3^+gYyVrrY*~81p zV|L_s9-Najk9=jik+#_tv#0LQ%hw%Ryh^R>L^%(PBl(+%m9IU(iR{u?zpVqctgW&7 zB6yDN&U#x|+>m-q8kxng6u4rY_aC(~X8wHOUi`GWH@>=v`WtTi=U*$a`x7o7=TGdd zmhM98*i>H8OBy;cXbp_?+WF{gp(v%_(!$d>j_|8lVO;D~z&q+<42Zg2<#=G;kp$DPZ(oyFcz{xyK>`u zCPuEz!V*u-PHgu;p-!{<5;tgdt~&A$95Hm0PDMNMj}XP1CSbKRZeUp7@G3T0=$7x) zVlvBMx8E8sTEAkPJo(ZoV%dDvR5Q?-{0kBk`KWa7>cPDSO43qEgjO!ksjEiIutbYA zHP5Sx*&gxy-ZohxJQTWxO97Vo@%Cy=C2z}7KRzMNQe4}5`wXN}l7k{kGSWEEcy1qt zaPZ!d`-CYRD>atC>FcWS7?LIPdw7JukO6n}VEF)@bZILPC7f0vOZb(LExfeSgr(59 zV!a2D3l-Hvw;9vnv!t>^862yOrm+sF5=@psN0$o3UUG3_o2o&$?*>CJW6$uaa| zzwe(I>A{8?J&iDSB1!e^dW!~U5GQwg(GZtVFcT|CL=Y37OGLzAC_?ryifSGXXWInx zu5}IR;VDSplq7bplgz&cRP!IXXtb0eIrEbO4y%QS=5z+%M*0#5 z%6Ta3OaB=G+uI)L5N6TVzIAGR+uKh5cyz{ISRQi|uv7npfJWObmB~b?|q0j8}4;T5H8_Z`S z!;c0`KyW4LH^p`=yAtl}jq4GfUT51oOcM~n>h1A$qkGq(f-`Whf27f(yNowt!IauV zwKy=>q85Y8RMmS}DSR^3TkCCEm}jpEzt9^8gnS9UZ)m}bafZ5I9fBv#U`UV^l6h^a zf!12z9iYN`%h}1{b@|x?CY;8~(xDQt&v^P?s{zpw8Z^}=aM{SgwJALKJb4jd_WeCB z18gpRz(o#s+t-D(J%;C4Xtjv!%YA|BSoZ9N7`sk4mBOb2sbj%5}dd7y0@zmqGC+BiU3eZ$6 zBB-%J?S7nXWRsb%p(%-TWx;v9qMDINv5AfuDKh<9h=)2&R<|)IRN1(Eh)ehSUB+Ma zo)6!ZV^Tf^7SaM~t9aA^7oy*mENw$Uj?sMqt!;ezCyz;?;*+E#3MgqudkQnvF=&K7kpxc)Ey792sf zFXT4nt6gZrJ@f$XsjicQl%*s-Hf>lzB!&_`{qyW{msdLi3Jvb%Ysc>Yh4$QQrqv_) zb1fljJxe-8Z*65oim_Zzgb{Lq(d8fhU0>d&r=a4Cn}^$f1GXflpiGd$)X(mo|8P8U z(8WphNK>7D>b)(wC3|BJM`|Kf655TWY^l z;t_7h1uSuLchig`E@Pn`?8z0y+kO0t+<3JkH0o+Ym`cWvbc}v8Qa?n8BUo!oPqS zQ&EW;p)LB%Vsp{vUlIhuy(Ji0{{J|c{L)eHkg)8DR_TU?Oyr8v73(sMeoga1i7x8@vGO}Jw*P3RbwIT#PaB?K~P*!Dl)+NxY~^vE*p9f zBK*a7fc-DiPqpFvYJaO+;(TG&K}rv-hfy}XkDW8<%U&odIXn~N5`%I+FUDcv&Jz8!Fc(7fzY7L($CG zPtem=04x@fb&Hlgg{(7DNWZCrbr5=ewPB(@?Kx(%hq|$zzzLRPRp}@%ghJ5XTt+nW z@S`ONr`eW+HboUXb~g2JV~EJp*o)__>5le7e{>qwg?_mT^>fC~n-wzUiPIb+_Qe+C zpeyy2X7D8wWZ~qp=;K$fzgk}eQJ&fE(<#&=JG8AGA8l$5)sU>@F(pg6jPzh^V zuLfbPsz_ZhLo$ZaZt4SUk0NkfDAfD;`@3Z4&g#@DRkg2`fKiPPNV-zUyZiVrG;{Uk z393#v*%8!aI6a{Yn5p`%Fw^qk0gDDz+Wp_bnudE3LwdRKVW~i-%2hE)&3rCtv;h07 zCS>%b@&tfLQ(XB-e6;ad!6v26i`7ynS+5~)ueK%8)-K$N#heC5F+6Zj2b|MH8`95> z@|X%hrJ_p3nyck^*+^nCj#uv*ps_Vl>128Xo{{#6*J0=8pBz9Nz1VtYU@Lv{|NhlE z?)mCb;m&@@DTGS3>DaaT!N`d%j#)_vC{4s{_+ie#WWi$vu~DG6pC4OXIYpw*nLSy(XFxs@>;x}!g%a| zu$Y3eP1Kv#zH;lB@Bqv%c?sXkqiWM8Dm#} zgrK+5k+3F%rv%s)=^FSwGBP+z&F7IxJAcQrN{?i^$UYFy$X#pYCU}`L|3kS+VF^TF zXNLeqVYD`^*BWVJZ5G);t|TCg%mZ)w*tU!eJCsu6)cU58mrvODWuFpSJcK>69lpPB z8mU2SHjj`0%g5VVsQq8{XWTcE{y+rQ_)02#C71^Uh8$$&)7U89mVR~zNSGE%M z67aqNss!3e=8l5?n*>V z@gt}V{%RUldnwe)@8Tb6DVJCOsez|;C&usbSIh;D>JtAUmCg7H2SBNLJ7xZ< zv|D?A`a1P-aJzLZiXH3hOW$2C~y9We(R4@BrFdw zjf&Si@vH1%E|_UK!t}KJ1Eo#K&IqkVCP0ML{OCMwBUHh9)45X{boi>G983ce97}Kx zSX-e==8pg-%fn5Vc?}vSosp;H{01W8P;1IxO|yE2T=f`MQw;3N{+pIo6u!s6Rk=&T zMzy}sBw4yn!y%f8x`iuSpt<}enaqkupS@!{ExeUCxkMggY2V zk$ihfxb)4MRIW`?y`jd(PIBpDuZ|yTbv;)v#$Iz$N_!~U?c9%Zd1GHLak!~H7U(P3 zt>Yv6*#MJaPJQ_jA?~J_WR1 z-@g-^+eSwd(WW%OkGl`(R21y~1WQ~?EqGDQa& zUenSacOT9IwFz`SeM@5;2dq{i4TXXd;+Z2$lO diff --git a/bin/resources/nb/cemu.mo b/bin/resources/nb/cemu.mo deleted file mode 100644 index 21149d8258ac07881a6870774f38df9203095566..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41128 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%0DsQ4jr(iB=Y!wnI*UtsEB*b9^na0f19%PBXSk$) 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 diff --git a/bin/resources/pl/cemu.mo b/bin/resources/pl/cemu.mo deleted file mode 100644 index 2d86b139d2be09cbc555790bfb2eb52963dac47b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17105 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 diff --git a/bin/resources/pt/cemu.mo b/bin/resources/pt/cemu.mo deleted file mode 100644 index 8ff9b1678256aa9e7e41bfb99a3d3ddf6bc729d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61652 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

0NFgcp$Rt>Cx9gCgNux%B%HcoEN}YwXNb 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

Nintendo Wii U Emulator - Nintendo Wii U Emulator - Émulateur Nintendo Wii U - Nintendo Wii U Emulator - Εξομοιωτής Nintendo Wii U - Emulador de Nintendo Wii U - Emulador Nintendo Wii U - Emulatore Nintendo Wii U - Nintendo Wii U Emulaattori - Cemu Project - info.cemu.Cemu.desktop - CC0-1.0 - MPL-2.0 - -

Cemu is a Nintendo Wii U emulator that is able to run most Wii U games and homebrew in a playable state. Created by Exzap, and written in C/C++.

-

Cemu ist ein Nintendo Wii U-Emulator, der die meisten Wii U Spiele und Homebrew in einem spielbaren Zustand ausführen kann. Erstellt von Exzap, und geschrieben in C/C++.

-

Cemu est un émulateur de Wii U capable de lancer la plupart des jeux Wii U et des homebrews en interface de jeu. Crée par Exzap, et développé en C/C++.

-

Cemu is een Nintendo Wii U emulator die de meeste Wii U en Homebrew games speelbaar kan runnen. Het project is begonnen door Exzap, en het is geschreven in C/C++.

-

Το Cemu είναι ένας προσομοιωτής της κονσόλας Nintendo Wii U που προσομοιώνει επίσημα παιχνίδια, καθώς και μη επίσημα προγράμματα ("homebrew") του Wii U. Δημιουργήθηκε από τον Exzap σε C/C++.

-

Cemu es un emulador de Nintendo Wii U que es capaz de ejecutar la mayoría de los juegos de Wii U y homebrew en un estado jugable. Creado por Exzap, y escrito en C y C++.

-

Cemu é um emulador de Nintendo Wii U que é capaz de executar a maioría dos jogos de Wii U e homebrew em um estado jogavel. Criado por Exzap, e escrito em C e C++.

-

Cemu è un emulatore del Nintendo Wii U in grado di eseguire in stato giocabile la maggior parte dei giochi e homebrew per Wii U. Creato da Exzap, e scritto in C/C++.

-

Cemu on Wii U -emulaattori, joka pystyy toistamaan useimpia Wii U -pelejä ja homebrew'ta pelikelvollisesti. Luonut Exzap, kirjoitettu C:llä/C++:lla.

-

This emulator aims at providing both high-accuracy and performance, and is actively being developed with new features and fixes to increase compatibility, convenience and usability.

-

Dieser Emulator zielt darauf ab, sowohl hohe Genauigkeit als auch Leistung zu bieten, und wird aktiv mit neuen Funktionen und Korrekturen weiterentwickelt, um Kompatibilität, Komfort und Benutzerfreundlichkeit zu verbessern.

-

Cet émulateur vise à la fois à offrir fidélité et performance, il est activement développé avec des nouvelles fonctionnalités et des correctifs pour augmenter la compatibilité, la commodité et la facilité d'utilisation.

-

De emulator richt zich op integriteit en snelheid, en wordt continu verder ontwikkeld met nieuwe toevoegingen en fixes om de compatibiliteit, het gemak en de gebruiksvriendelijkheid te verbeteren.

-

Ο προσομοιωτής αυτός στοχεύει τόσο στην ακρίβεια, όσο και στην ταχύτητα, και βελτιώνεται συνεχώς με νέες δυνατότητες και διορθώσεις που τον καθιστούν πιο βολικό, εύκολο στην χρήση και συμβατό με περισσότερα παιχνίδια.

-

Este emulador tiene como objetivo proporcionar tanto alta precisión como rendimiento y se desarrolla activamente con nuevas características y correcciones para mejorar la compatibilidad, la comodidad y la usabilidad.

-

Esse emulador visa proporcionar tanto alta precisão como rendimento e está sendo ativamente desenvolvido com novas características e correções para melhorar a compatibilidade, a comodidade e a usabilidade.

-

Questo emulatore ha l'obiettivo di fornire sia alta precisione che alte prestazioni, ed è in continuo sviluppo con nuove funzionalità e correzioni per aumentare la compatibilità, la comodità e l'usabilità.

-

Tämä emulaattori pyrkii tarjoamaan korkeaa tarkkuutta sekä suorituskykyä, ja sitä kehitetään aktiivisesti uusilla ominaisuuksilla ja korjauksilla, jotka parantavat yhteensopivuutta, mukavuutta ja käytettävyyttä..

-

It was written from scratch and development on the project began roughly early 2015.

-

Er wird seit Anfang 2015 entwickelt.

-

Il a été écrit à partir de zéro et son développement a débuté vers le début de l'année 2015.

-

Ontwikkeling van Cemu begon ongeveer in het voorjaar van 2015.

-

Αρχισε απο το τίποτα και βρίσκεται υπό ανάπτυξη από το 2015.

-

Fue escrito desde cero y el desarrollo del proyecto comenzó aproximadamente a principios de 2015.

-

Foi escrito do zero e o desenvolvimento do projeto começou aproximadamente a princípio de 2015.

-

È stato scritto da zero e lo sviluppo del progetto è cominciato intorno all'inizio del 2015.

-

Se kirjoitettiin tyhjästä ja sen kehitys alkoi suunnilleen vuoden 2015 alussa.

- -
- - - https://upload.wikimedia.org/wikipedia/commons/5/58/Cemu_Screenshot.png - - - - - https://github.com/cemu-project/Cemu/releases/tag/v2.0 - - - https://cemu.info - https://github.com/cemu-project/Cemu/issues - https://cemu.info/faq.html - https://wiki.cemu.info - - - Game - Emulator - - - 8192 - - - pointing - keyboard - gamepad - - - - cemu - application/x-wii-u-rom - - - nintendo - - diff --git a/dist/linux/info.cemu.Cemu.png b/dist/linux/info.cemu.Cemu.png deleted file mode 100644 index 6f65507248160cd5f649d43be8295e90b5aa1622..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11984 zcmV;>E-%rEP)N2bZe?^J zG%heMF)~90YwQ33E=@^9K~#8N?VSf$lUEnV69{gtqf=XznaSQbP(;MNKy9_6RZ(%* zS*y~jZQXkh90=~Mb=Iwj8`(pKqN1RH;H>TcJNHe3s4qw$jQVk(=l2jGx#OJwJ?Gpv zZ$MDq%3FCWZ{@ALm16Vu^{wL3uU~BkH@Dgj4h|~VmzBtzP>sR4jz@A zI(Ud}TwS|ZyR=#>v$Xot%-Zg@L}8j`A{J*$q_S5EQ&X5R%*;MwOkqlEloMj9^reYd zlBKY)%rdugj5Tv`I^fj4%aW!YI(KmH)z8Sm!=p0WFe)?t_nN1tX9edjUJ_Fq`wr%I z_RGaGd5~10$dD@(uN4Xf%+1YUX=w>7E2|u=t*t+La<7+RVPT;Z1=&Pys(2+ew|FeG zusmnu(sG%Nb1PRnclR1(&i{@Oti8SU9NTq~ST}c_ps=>PB$mn2q%!$yb93_?hw~Yx z9HlszAXv1Jkj=z0#cdV^f*4b2Tz=*%m^}UV}cjYu9P4%-Zg5f#2&^n&O@gqoFoUhBoRF%J<$C8ijjZrw(13%WUoZDX%LXPtMJ~AS-TB38)kEr`AfIC{xGDjMKjkC#!^|7OfLWTUB|NpF{Lxbv&V`S z?V#^97GF&EL(O?dP-WG1R0v3f-nP3C?YIxou9T0P9rw7kR@@kmYD+Jo_RPJg_u~SX zc=U%wqh=%mCI+$hqJFiqlAhdRMt#97gd96fY_Ka2V zxhKpyo)pa-Z|UGfJ9{8LpRy5E)`dWK=R*khW`5n4+(C^cXHaj{JV@L3;B2TA!aI?a>?)^?W?*a*CU4w)uuH$;U#R?1qD@%8 zetmsI6O;9HoO#X)m)w)&LOpZ5Vn39z=z$Nf7QMNAID9?@~E?$Ge94KV~(<>U z7K@+19rD?(H{-$;%jb1w?}Og(e-DlxZMq;PR55fKRMo0#Y~XrRAQB9T0Q>qtkBZE4q=Ik&)9m{`F-#DMXvM@!?m$mUk?Rop5yZfPP$EzJ*PElYHw8EBn)E&DB=8c*tqXF5T zJ6WkjAd$(RSUR_u)6U!bQ>2h|q=-?Y>X7!6u5bt`u$GF1pz@0~5Jvn$*&!JGo1{rvrB8v~X-Xf?eEsE9XXU5zH;ZAc=SV+^*5Ey&&l=W6eldo)|&Sx%$m8umK_yUK4EED zCYS#sx3N2F=kETs%7sO1&h0&n%p4nEp|j^3=TqKi>XUTniK=U_lJm>+eTkvkWK0Bg zX6}N8Q&Z&?M3rw?gf@{#p2+PSW>6Sl?;jMtQHOd`bE~mps`%BRpA{ag2vM|Xi%%B+ zrL6Sdon%>o8B5J}GTOE@=xj}cE`uKDp!ym6osgdQ*6aBCEelYcrMo8$Urt!5ECzF> zX7*02iCmr}w{vQ)a%JI~!rEFwq5m>_yHee`HC3FJ_73>$*8uK_!aKRrgq$YaPR=C9 zRwnA~NJHJ-k6^I>F{}=y!{t~y+WeJ)4kt6v@s#pwyAv5`ML%nDG##dYrlUTs>r3jZ zCcR#dVEr-5lpttbtT1HPS@gX|D^CXMlNo)L%4Gi-h{dZ^?;>j6WS*WCWM*a)sOo>l zs(zMVtW;O{^u5NAVUoC_gquid!yLppD-|{Ad-DU2(D8HzMqYV>C6U?KmiP=O?!Umb zw3mp;c!`+ISBQP`3UMmf_fZ+I5SsoH!Kp8CDET@3Q1d` z0MllzP<_cMQeWviUq}wGKo0qW9@`#zi~;1Vi@)*g)oZ-``!)W4 z{Tik%exj(jncOhUzq~4#C97H@A{RXLl;?k1#dy1|+vw0gs`T0o*uBsOZ_*@4_z@S90oJbKE^>>Vz#%3s|X zb3WVsRaaf2u){9z@=T<-VPT$(ZA3YG;Dt==O?t-dbhcA9+2KWni6NaLL-2#=@V%M= zi+!o6y!9bBKi+wl06LVSzW8YsZB*K@swlvaN@cHYTevOo7&WSZcOe5pn-;CRN)@IV zERS#(Q9V}KkD@@UE))gcq-}O{j)ha>*yg?a%asoPKy&u= zspiJV1+}ML?Ug#=PS&YvimH0r`$A24%8Wj;?OMvm16*ZqB>4?K{9#Zf%>K zBLEiuDgS>tdNG7_p5`4`+mPWZlJgrMdW3+ur(C#ayNu81WsO%9of6WYVZwz6_`Z)I{tHVYl>#mxB@b`JPt z!BJ8e31C|>FNmC9nVj!_^f8Xo(JPB~6`!HYGEA&@GoNAdh5Ps{kVH^Yk$|GJ&d!IZ zJ7O-Z$ZzcArWODd_2Hcd4OgiA%-?)`msE3X)ozVMD*aon9#&AuTsxrBx=`iYyv3RS zRXe}k@y9rI?-{cHgU(m;>eVaU&UlKEr;|}LAX$|TiaQ!S#unBV$|9b< zRje>GOSNv?nAHJ!H;;a!YCE{PpODF9+zS~trZO0H8wAl7S_GH>*&>Puy)6&W{BRmh z-F=FuFDdMk5w*d~moM?+#S1)r`V4809wGT&3X+oVA>np1;)%rMyGXvDLce>AXV0GD z1wEgyA5D2lL_U0i@BU0c`_WS|YTA4(*|ZY}|2l)jq}zCs zmBopNukV8L{KZS0O-w`6ZL!b|yvMtCtwK?{(%LXc+&yxRgG>NoiR76>n+{{WSFX&v z5R}Qjt}QV!KEdAiR|~*~1n|w^Y0%k4M;pEYwE@)?^>#eOs;DgPT+L@lO~}rEis;x| z*t%;!hW`8;notC*->MI4bR3QfJtjl+!)%CrmO*#;8t9H(2hm7B=nY+oirx$GNuS?f z((5O59XAsT*Kf!5ut;P+DVh+TkPtWANJfK=F%WDmMmsWgJc8b)1Q>N4z(sGhA0-sk z8@Fj6knaT{ODEU5Vq@cTEN7@+Xl*9PSECm~xTl!HKRLe&?fy~08AzcsKA!;#3Jde` ziMO%BF97b}`QXb|UZ~(P0fJF}5X?9Z!HOFYY`6tMAcfngTVgrllmu%3Ru z>>6}_-H)0>mY~sy*_gL+7j7lp;rizrw;(*o$ik>2(WtsUo?A1oEwllpX}!QC81@)S z0+8nTk)?GzrzQa&`JDth4=gN-2Qc2GbIvEh%VuGz~XWmARMg zG)-Zzd-OOR2aldW*Zx1^)0SS)=`#a@-;Y4BHcIJO&OjeXVYyE@Y<`=PAe?srbw;m8 z_uu?*BFURPun?_&%)OSxu<14`%s2_VpZ&1u$T>XD z%;cTRhj{!X8*@%YqR#4Y8i)GMJ(jk}5x~f|1i&2yEiDTo0LQllKu3wsCi-b#0JQij zn^WL@;W6$$dBJP25Apo@b3{?DA3bFTzHIqDgg#3kSb0qr>}?5T@?xI*#~g!dNg;+ z;G0{1q*E05{a6U9Ef3}T)9O4KZwWx6p#X-H0POP-0Gq5fAm>-wd>fvRoM<|6<_K?T500qo)d z_jWLy0zR9#2Etub4Dk(E2tvu6e=Mu=3<})LM8^`}0-_K>xcDu$D`5{K!rL1rq?( z0679+kwDu5pap+QXYkb#8OVIfZ=n20po+iukH0{#*Hj2tHLdL(R1i23q@e2RXiPjB zfwasl-nr&SaC8F9=O3r&MaAH|{TBsJ00~79K<)^j7Qjy`0cb(3Isx}!zyATQq|njt znPw3nD=Qltw(mvV&ch&>eJaNhIbTZ()p$ZW&)aVb!{ww08m}e!K^nTOIR@R8k<=H9 zU|Lv%st-|MQ3PO@hX8g`j%5QBk%}aMq32VPlKGS~nkK|0B%=M`pP=ir6axRmH;XPU zsF1C_E*48K##42#6m&s(oRNts+fSnEyddqaqsRi_jR32)BLFg3-7WX9J}MnqPx%d# z7c6gXIdBAZJw`z=|9t5?UxjGnZFJigjf98kyz|ZtTSInsHW$e?pbbME-Zt?ZiVQJf`DcE<+WB>*0~G@BcG|(+*Kz zWyJZ6z)``&`+oSk z!ywe?JOUMZPlIsgF%l6Kf_!}!hT;*xDlG`$E~?UQ`R!^N(w=1VIyo;gpFF|V{YUUc z?_VHT8CG_7KZC-zC?FY){jcGNKUSb^pOLWcHW)^pL-1MWq2%aMsQjHTbO$bgV9W*x zeD^>w<1YvnT~eN6Hc~CaQUHUme@)2|z;Nvdz%oYwlylV+Eez!R+FOzl81ooUCiJ3$6YM6Dcl#>})VFBUE$Dw+)&xLDCfUxrIGIERU{)5dqxoRZjfsm#`yYRU zU<&OXuD@gz?~G9@sOAzt&ZShq36u-z^gHJ0w_rcxdF@;M6$R=m4*|&Ja&}+fhXgR1 zUjXA*Lb#o&RXyQC;Gc{Z2ks&KK?dj4g7SpI|H+`suo^(&e=b%3%#eKjmj&K(urcEVMP#tnetD$Yf}L1(rLH9?&0>MOkPLlMFv&U8}=W?7yW-%=6_BA zC2Z9H650>|yC;ynA;dl#Xi1Mb0c<0~Gc^~IigaEccQFO28JXO!E+~&5KgNQ<-Kain z83gO&l#Ar)zZ?~0ACZ47Cjcv%Sm{TibOo^K4r=))Vd;&B+}$*Mj?Rm__wHld0zXt9 zvyu8q;_ttlHxyL>7Vinb6rYVNssL`|%YY$FIf0LsZ55`t($6u$rz z9~8i7FZ28az}~pv8}tL{tc!>1z9d9yC;)a1;K;d)Fdjn@V0nmA*z5dI|MUDW5Z~`zVKeG3B*oQ+k z5&(MJQQ39BGj zMl}FaI8XoOfVA)eP^bhzyN_?c0-&=t4o!F6LfD-LJWkG!RGhhT z9j24kLa^joxc~|RCxCG63BWwJ0N5W-VDHFjh>!$eyFC^+lI|llHB}>mZid6gcRhrQ zt|$e-(|-tI0Z0oh09$?m1Tf>r7LIkXXs|H~ha>Lb!NZ4mr~y~Qqu@5x z54y9?DnlG!|K+@4LmUYp3=KU8mn{L*T_1%VAxTJi!0+h12#JhA=jm%9`u%vh0186U z1Tg%40Vovs^vA^zZX)Baizzf;_(fvF)dbv6d4LBG3Qly~Eeu?+2^HDf!7Rk_^j%Ia zNCcv_p=C?}ele)IIudg)+``?I6kbQ?#qB$HFm=;@RQ+`y^+gKj>%Xkv7tO8pE%=^S zoa8A0X~6}+{WS@4KKqyON&zf_(4PdrEwFG@AOVax7LBC4_mM&(C@6RD-NWL&$5C_g zb_mu)QeU)4p8iW4AyI2%3=9Kf@y!Mn@uE>}O*BM=E}=sRRS4F;|9Y)KA`q>;i3Xm7 zU_k=l{796#0EDZf(0O+_q7svN9i12V?%&7ea~Lki-o|}$swSMh5`xB4NdSwlDZ?CJ|D_2zusS(& z%*h1YO1_Wed-rjhNT6sJd*>d)lkOoX?k)~QBqJc?4pv>ejp^qSG4Mz{TJ4ELZT1&L z+21qd>swPe0fZDm0M>j0SP0=p%D;tGDq9>GNWAVQ4u;0#9))#2NAtm+8y*z{&v{#* zGn>3cxg90nh-2g(5($)i<&FVlw zHt+oN;x5_Yz=^Xk7&adQ_Veqm zirAz(+~NyLa`GMQJ{^qu({@3yGMu;X(uDn)7x$gfxDcz^x{_|+#+=yBd8D2s_P_xf$s@GD*vDWhVTnu^#qy6)8dO>pjvtT3B0BL7@7(hu?$#~%{I(FH!HboD;goYC{}=^^Y$IBA z?R^1A3M>Gd7wrE9YFz;Av+!gn`I;-(e=Q1k=zPy-l)OktOoZ>&eU%iG_LncFKe4x@k48AcmM6{9w(3J!rF@`DmkzLo3NF)Uj zz@Qufuop2n0gRXr;d%;B>PZ$BGFI)SS25%GO)jr95op5x3)di>u^&QKF!A+Y3h1m1 zL$hs>I2)D7U9{qxcV6r|d<=#?$3iguu+n)v{cB7v0hluZsQg%P0a)b{080h*m=nM{ z3Qw!(99|^Cp|>;y-2#GfJ^B`IQx4RGh?qF^^E-@6>|^3A3h<6mf{^p86O&KIA|`=< z&Uq0ZACDQ!*5Z?%Qy^GG1)#RpLn08ZxQhB+y|p6%Q+ixy#5^qtAPhpP1H>yXV)vzR zBqb&B8YeH<^S7V90?FK?5VBjuc=|6%gv&!=?jOz_ZIdXX@y$6eZia`$W7IFuWBGpt zMKqrN3q_6q252sTwtWV9DXgsD5r7PJhLZsNNTDl3mBmVt2t~C;moRbPB}B#DLL%jM zO$dvQ#qiCiQH6a{h?|c#=2SSUk_bl}jzYvO{<-GI!DD|x)@``*Zw4{PX=@$SmuUGF z)bGM40C`aaK>3Alz(Sz2^g0@?I*Sw6Bao25Z=k$LNJzl(t6^xq;v__iukiL?66jDJ z(kLJlhi=~D3RJ$i=0!|wEGErbjH=xxK(Ih9Wd3n9W>qK&;IftlFl;V_Ye}UnRB40d zA*en79Ht)#LR4%V=TuFIqx|WA<~mH5o`vp`>(nPLoNt`sV%ZJUUmb#F=VK6ai+_%J z!E*npa~I&$b0~C%tyJ}?#rcy6M9VIrerF~Cqe2M4()tCvg*Zn5^tjHjIZ6SrPv`Ir zstL<)a9?#6T|tv&$8qFR2xk;NW93DBLOdc#I17$lgUQk$=x`$78LK!T=hq^}9F9Op z%q@-Q9u*yp8B127W_uq9W*k%QTE1}#%gPXy0Q$pRYy<`QkzLmjYbpTtA_W}*6*4L6 z3?%`qp)kdqrxnOy^tI;fQ<$*r6vCpSaqCt*XG~29jf%zGqjWS}5loTbnyO!A-^GED zQdg~I*U@ip7=j|=XwFIj6cl#h=lH2JaOp+6f52P_mQo61PU9J?&`^J(rNNv4%$NW; zKav2%;tvbJz0UwIxuw+`0Z=MX3Sc$~fRvhJVC|r@=rT;_9>UgB!HA1f8dOulqGPb? z^i4Rfy^Kn%CLjm#ja4M5PaP6i-4)j{bblDmg~f8t6#4V4#PgSp=g{BkTD7sF!_%QJ@$5kqs_ymK$i z+V_Eu_W}r(Tvj@aXUrl&eTtR@p?;^g1z=K;2++3Q5Gn#~Ua+?g)h{%ait*{7=_CMB zE8l=cWyuwMI{hGi-EtIR5s|#cD+tkK;Iko7n0DkQZ2YdGI`ylwn1oO$F(_XrqSt@D z;u^Ybzllvjk+?}Ir!dZsrK&$D_!4>y8HY+8MnLcf<$lij+7oIHOaO~X03G|nG`9du zOg{XVLfa1*>1AeP|F!_c#`vP|PtaLKlU-U2kP(FQgCLu>9sY+;lOtm|gB2Eb@4&t* z5g32yCY=4R;j0x_QJD%(UG~8emLfQ4-1kuaPDIbEPG5~yUxVlN5G*+zfuPW6PGEfF z6@-wRq41r#0H3$~9)hv{v~HHF$XR^$C@K_HMGMcNLAzdW3BbtM7?$>q0S@{7=g>~W z#&xrBaLQyy2K5WgOpH*k2Nf-Jwp0#6lYe*6Lc{t}8bdVW1YCX(#KF@+h^9(8Hdb>X zurRA62CT3=aU&9|&xT{f{+sBq^#<(LUxS2%tWTo&W;yL{5?_7#YCzx1R$qma|8;oo z2*ISoq1YB2f#9%cs@mzONBuGQ#{3Ww5fNCuApnw=Js|8ii_ZP$RnAwfo!2gfh5Dli zpf~pv^jmg?sf2bt`H&s!4M_lwE!*V(zk@sW8T^g8twRt~3wsfFJ%9x8O~)UgyEs@m z0Lxv)!WC4SdJvsv_~Yo=i-?Mf;)GCCB1s4#5z)9D7KP*2Be9(vyWv7O)}9Z?>T}`n zqp$u!5!iVt0w+l@SHhzZ8p#~b-=8K#Mn+=m&OLDG_&r3P5Rpaf0}vpT;=7vu9C{#?>CQrqiWl>Xm!HS$S6ku3R01QO9xWbNu|p8 z2GtgeE}_cgUFbezJ&v3{j|eg#pJ9svb8Hle;yvDZUQuCl-?RT9nsxDlUb`U>Ox@4F zW-TLJaRoIdu7T9tk^~@83V>3&v585#P1EKbe0_a&RPKJi^%_09s(s5g%P9i<%~Zn) zfI5*_*`eyxtrWUQy^AjuFAFcC`p;X@dFo2+J$4e|;o*oPW3q_wUm+|k96R^ygR4hh zRCFHz!Q>sZHj1Ep>(w&CWtUOM=MTu4|L6qAe8`S~GINX6X6?H)Qn@ z$Yyl_7X?(E)EYidDS$=AxDa*U3xZH}!e+FYv=o8+4&nNZ5H6RpkXn{ubG#7}f{lUO z(5!QBRCMbP0sF_6?0&!8>nlb?3(w%24qi}D@y_{=U?%{Xja^8m!6T$9Kj+=(DA-J5 zZk5Ddr0#%qR^pKCA+*DovC9%{PJbRg zijm`{z@TYI=yd)O0$;Kf=X{>E6%)dx!KgiW24r%o{MlW=a0@FjXkot{K@Fcpa2YlYOV@A4 z`3pe^4GpDS8&;~YIRsq{#)>uT(8{wfYB_a=@cXF{Oh3rYTM6HqiUHa%qIoA!zim$_ zOpJ5r8UG?~-n_{zmEFW$8j7nGW-Pgo0yr48f~D*9A@Gcq)UWfPaUR{clHRWAN;Yt`FFZq}&jWkY(g zx+57y0I9+hpRhZG7t?_3pEs5$s)bHJgbITfL%-*Ed^`GAEL^<-hmIWM9CG8v4Nefn z0Soh2uU*6GGiR}GV;}~O7!OIaPN?MkEd+i4fZ*4?5X?KPoFni2ixC=AH2(zEY5gFl z+%KcTo%xIv@`fhj*Y<6@EXwx+(7Vmk)3buDTl?`wV(C-v2%!>z+{6eCTsxz}%mbuy zw$O9$Po`L4SVhhp z$KZ2nUZ`Kg1a+B&_1B4$+-{#deHJ==KM;mZ$??s~*1YErV!5?_5W6VE3JI=LQWWFUev_a> z#|`BK$Qd~fDgkoFBLmE(IZQhUol$F1x#utVvdeH-_ZWijMoz+n8S}At%?51Vz6<*g z9>S^9XSkGckt+4zOP7_9bJdU8&o2aB#K}{qxeGWO0=Hu3qGcE|!56K1^oM~^R4Wr$gSLGa672uG}j zZr|Tg*<&oe>NptUP6J@!F#yf`3`ZBAarl1Z&*(RL5(bQ!3~yopJ?{78MD!j$9$f~G zLep=(VNQjVVax8Q-Mk0%TJ?tx^(FA03&Dgy2!1=H-0|wUuz8e*4OC+a7oJ6(epATd zstwAVPZ${)!&s(BXxgrmqspb4H@9BBjN~@<7YwKtV2V;ggHS>Eb(?RYH)9_ukxk~b z3Y9TRj9la9Hq0z4#HdyfOxgkAm<zhLm;Jl?xw*zDwKc0bP0l}uY7^GL*xHfX zm}(!f4XbZt{Mw>v^TqAFy{o9)sd;1P#-?4q9bqIl&0_f^M+B5-q@=blx(|bhoeJ2F zWd9{I$0%h0HYc^S*xXpSo=H(+2F>^P!^)k^?#fZ~8~uzqnmva-m%WDTgBhDa-$5l<~EY^DE5Qk9r+?Q#7U;Etg-|Ml}>m)15kY zpXTY~TTSK0qBWP^gPW70q74iTat7i=K&e2iFhiXIzR;Pc+BIzVlv8}nI}N>=2k}+c zft>xhXsq%F3;hiZ3}49|8ZB(m>H9h=_Z7W4JNt^Po3-fJK;JON$jIp5+#-+~8$oPp zj@ko$f$m%ipG={%DF<`#ETEi1pt9$@Q>ZX~C%)?3A5wC*)P$n2$`35`>l+ySBX?|k zxkaDBwkp?YuQ@wAixjp_!}N{CPmRgxY(O?Iog4oFPwJ* zReoNFZ(4StoKIn&rQlpX(AU@h$3QFzc5rof_Vo1BRk=;O&Gm;NbuC-CuO<;ZW5*PXv^(;ZbN`a#H|0aGn2su+9=D<^N%cAIk&x^&L3>GKB+9G$s{%j}%n_AE7v z&8;6=yR_?6Y|d9Vm)^bWSu}02s-dyT(|1K6AyrC^3}Im36t(GWsyBT%gmcsjqC&Bp zf=hrtd3~}o6-MF@cC)858sd$@v z&;DOqHFsa3FP3Jr_Y2rW-iUy56RQg(Qq*^8k57DND1|_a5zJNzfqf*ZocCe2p$(%m z`zR{@vKgQD9tT6a#>!pKBCOiR%=wi6Rn8CAH#Tk}^YpBsa!-jgp?m*fU)#0wSY~2s znbA;R|6ewt+!K`0_beKi$W8H0i;k$(-xrl82SRt|AqcrJr&2mwI6Hqc*cts@qkMFz z*2=AyzGpTddkAMAgXs5ts4{LfKKpJI^qpF$HWTgocS1fp>NPMh{MXpr`ijD#v9qAK z!+su3*lOg&uN>NW^cP#$gf!4Mc+I`}z(N#FSPiFKBg#=UaeYS@d_m!?#*o>lI3)n0 z>HDDb#}No;9wSi@WCX#-ifVlvvyRf5k3#47!_b?$3zdKJL#-bsqh9MCFtW1eY$hd} zv9Gyp$v)k~B5wVL`k6AvrpFw+dbKF_od2Q5Cg9YyXA>hc>vgO+q@xIT4_fYtGt@$0 zu&AK#;EZ~%UGcf+5Y*{C0kx=R_+;R8RQH*Q>O*FIeE7^ljlnZe(|aoC>^i;1;fs#_ zQQx^O46GfLdEHPS658=Bn!am4I)0;JL;cqV5?Q>$q3JX`_s(B?dTuLS=jU!NJ$iiM z(6RdxiIx3>hK5G}vdYwiBEq{1VMtg`qx7H_1bZx2J=cV42NS96|*ewB202$A8ka`+(LK&0G5$n_1qc zJoOKY2u!_vi{oHwe+x3iN6WiwC3tMYA}9;>R9mrJpQUhU5@*-C^Ta0I`+Sq{ef?#n zx%V7aqmf6SmbR_h`^oJZB^rt)*$qetEGjTDFjey{{Qni^aPAsDIi2lxeM7R5!aPfA z<9O1-*|op(w_cKV-oAx@SFeBwc1{oo?#bB;CxZS%{5`dtlQZeb>449^A`q;COSk+e^K=FRsS!F<>_+ zx(^%obvvK2En4;-;O6As$<3x&>#i-o_uk;Z%%Aow3#3Bw8aITiW<0000 - - - - - - - - - - - - True/PM - - - \ No newline at end of file diff --git a/src/gui/wxgui/AudioDebuggerWindow.cpp b/src/gui/wxgui/AudioDebuggerWindow.cpp deleted file mode 100644 index eb30974f..00000000 --- a/src/gui/wxgui/AudioDebuggerWindow.cpp +++ /dev/null @@ -1,305 +0,0 @@ -#include "wxgui.h" -#include "AudioDebuggerWindow.h" - -#include "Cafe/OS/libs/snd_core/ax.h" -#include "Cafe/OS/libs/snd_core/ax_internal.h" - -enum -{ - // options - REFRESH_ID, - CLOSE_ID, - VOICELIST_ID, - REFRESH_TIMER_ID, -}; - -wxBEGIN_EVENT_TABLE(AudioDebuggerWindow, wxFrame) -EVT_BUTTON(CLOSE_ID, AudioDebuggerWindow::OnCloseButton) -EVT_BUTTON(REFRESH_ID, AudioDebuggerWindow::OnRefreshButton) -EVT_TIMER(REFRESH_TIMER_ID, AudioDebuggerWindow::OnRefreshTimer) - -EVT_CLOSE(AudioDebuggerWindow::OnClose) -wxEND_EVENT_TABLE() - -AudioDebuggerWindow::AudioDebuggerWindow(wxFrame& parent) - : wxFrame(&parent, wxID_ANY, _("AX voice viewer"), wxDefaultPosition, wxSize(1126, 580), wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER) -{ - wxPanel* mainPane = new wxPanel(this); - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - - voiceListbox = new wxListCtrl(mainPane, VOICELIST_ID, wxPoint(0, 0), wxSize(1126, 570), wxLC_REPORT); - voiceListbox->SetFont(wxFont(8, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, "Courier New")); - // add columns - wxListItem col0; - col0.SetId(0); - col0.SetText("idx"); - col0.SetWidth(40); - voiceListbox->InsertColumn(0, col0); - wxListItem col1; - col1.SetId(1); - col1.SetText("state"); - col1.SetWidth(48); - voiceListbox->InsertColumn(1, col1); - //wxListItem col2; - // format - col1.SetId(2); - col1.SetText("fmt"); - col1.SetWidth(52); - voiceListbox->InsertColumn(2, col1); - // sample base addr - col1.SetId(3); - col1.SetText("base"); - col1.SetWidth(70); - voiceListbox->InsertColumn(3, col1); - // current offset - col1.SetId(4); - col1.SetText("current"); - col1.SetWidth(70); - voiceListbox->InsertColumn(4, col1); - // loop offset - col1.SetId(5); - col1.SetText("loop"); - col1.SetWidth(70); - voiceListbox->InsertColumn(5, col1); - // end offset - col1.SetId(6); - col1.SetText("end"); - col1.SetWidth(70); - voiceListbox->InsertColumn(6, col1); - // volume - col1.SetId(7); - col1.SetText("vol"); - col1.SetWidth(46); - voiceListbox->InsertColumn(7, col1); - // volume delta - col1.SetId(8); - col1.SetText("volD"); - col1.SetWidth(46); - voiceListbox->InsertColumn(8, col1); - // src - col1.SetId(9); - col1.SetText("src"); - col1.SetWidth(70); - voiceListbox->InsertColumn(9, col1); - // low-pass filter coef a0 - col1.SetId(10); - col1.SetText("lpa0"); - col1.SetWidth(46); - voiceListbox->InsertColumn(10, col1); - // low-pass filter coef b0 - col1.SetId(11); - col1.SetText("lpb0"); - col1.SetWidth(46); - voiceListbox->InsertColumn(11, col1); - // biquad filter coef b0 - col1.SetId(12); - col1.SetText("bqb0"); - col1.SetWidth(46); - voiceListbox->InsertColumn(12, col1); - // biquad filter coef b0 - col1.SetId(13); - col1.SetText("bqb1"); - col1.SetWidth(46); - voiceListbox->InsertColumn(13, col1); - // biquad filter coef b0 - col1.SetId(14); - col1.SetText("bqb2"); - col1.SetWidth(46); - voiceListbox->InsertColumn(14, col1); - // biquad filter coef a0 - col1.SetId(15); - col1.SetText("bqa1"); - col1.SetWidth(46); - voiceListbox->InsertColumn(15, col1); - // biquad filter coef a1 - col1.SetId(16); - col1.SetText("bqa2"); - col1.SetWidth(46); - voiceListbox->InsertColumn(16, col1); - // device mix - col1.SetId(17); - col1.SetText("deviceMix"); - col1.SetWidth(186); - voiceListbox->InsertColumn(17, col1); - - sizer->Add(voiceListbox, 1, wxEXPAND | wxBOTTOM, 0); - - voiceListbox->Bind(wxEVT_RIGHT_DOWN, &AudioDebuggerWindow::OnVoiceListRightClick, this); - - mainPane->SetSizer(sizer); - - // add empty entries - for (sint32 i = 0; i < snd_core::AX_MAX_VOICES; i++) - { - wxListItem item; - item.SetId(i); - item.SetText(wxString::Format("%d", snd_core::AX_MAX_VOICES-i-1)); - voiceListbox->InsertItem(item); - } - RefreshVoiceList(); - - // start refresh timer - static const int INTERVAL = 100; // milliseconds - refreshTimer = new wxTimer(this, REFRESH_TIMER_ID); - refreshTimer->Start(INTERVAL); -} - -void AudioDebuggerWindow::OnRefreshTimer(wxTimerEvent& event) -{ - RefreshVoiceList(); -} - -void AudioDebuggerWindow::OnCloseButton(wxCommandEvent& event) -{ - Close(); -} - -void AudioDebuggerWindow::OnRefreshButton(wxCommandEvent& event) -{ - RefreshVoiceList(); -} - -void AudioDebuggerWindow::OnClose(wxCloseEvent& event) -{ - Close(); -} - -#define _r(__idx) _swapEndianU32(threadItrBE->context.gpr[__idx]) - -void AudioDebuggerWindow::RefreshVoiceList_sndgeneric() -{ - if (snd_core::__AXVPBInternalVoiceArray == nullptr || snd_core::__AXVPBArrayPtr == nullptr) - return; - - snd_core::AXVPB tempVoiceArray[snd_core::AX_MAX_VOICES]; - memcpy(tempVoiceArray, snd_core::__AXVPBArrayPtr, sizeof(snd_core::AXVPB)*snd_core::AX_MAX_VOICES); - - voiceListbox->Freeze(); - - for (sint32 i = 0; i < snd_core::AX_MAX_VOICES; i++) - { - sint32 voiceIndex = snd_core::AX_MAX_VOICES - 1 - i; - snd_core::AXVPBInternal_t* internal = snd_core::__AXVPBInternalVoiceArray + voiceIndex; - // index - voiceListbox->SetItem(i, 0, wxString::Format("%d", (sint32)tempVoiceArray[voiceIndex].index)); - // state - uint16 playbackState = _swapEndianU16(internal->playbackState); - voiceListbox->SetItem(i, 1, playbackState ? "on" : "off"); - // if voice index is invalid then stop updating here to prevent crashes - if (voiceIndex < 0 || playbackState == 0) - { - voiceListbox->SetItem(i, 0, ""); - voiceListbox->SetItem(i, 1, ""); - voiceListbox->SetItem(i, 2, ""); - voiceListbox->SetItem(i, 3, ""); - voiceListbox->SetItem(i, 4, ""); - voiceListbox->SetItem(i, 5, ""); - voiceListbox->SetItem(i, 6, ""); - voiceListbox->SetItem(i, 7, ""); - voiceListbox->SetItem(i, 8, ""); - voiceListbox->SetItem(i, 9, ""); - voiceListbox->SetItem(i, 10, ""); - voiceListbox->SetItem(i, 11, ""); - voiceListbox->SetItem(i, 12, ""); - voiceListbox->SetItem(i, 13, ""); - voiceListbox->SetItem(i, 14, ""); - voiceListbox->SetItem(i, 15, ""); - voiceListbox->SetItem(i, 16, ""); - voiceListbox->SetItem(i, 17, ""); - continue; - } - // format - wxString formatLabel; - if (tempVoiceArray[voiceIndex].offsets.format == _swapEndianU16(snd_core::AX_FORMAT_ADPCM)) - formatLabel = "adpcm"; - else if (tempVoiceArray[voiceIndex].offsets.format == _swapEndianU16(snd_core::AX_FORMAT_PCM16)) - formatLabel = "pcm16"; - else if (tempVoiceArray[voiceIndex].offsets.format == _swapEndianU16(snd_core::AX_FORMAT_PCM8)) - formatLabel = "pcm8"; - else - formatLabel = "ukn"; - voiceListbox->SetItem(i, 2, formatLabel); - // update offsets - snd_core::AXPBOFFSET_t tempOffsets; - snd_core::AXGetVoiceOffsets(tempVoiceArray + voiceIndex, &tempOffsets); - // sample base - voiceListbox->SetItem(i, 3, wxString::Format("%08x", _swapEndianU32(tempOffsets.samples))); - // current offset - voiceListbox->SetItem(i, 4, wxString::Format("%08x", _swapEndianU32(tempOffsets.currentOffset))); - // loop offset - voiceListbox->SetItem(i, 5, tempOffsets.loopFlag ? wxString::Format("%08x", _swapEndianU32(tempOffsets.loopOffset)) : ""); - // end offset - voiceListbox->SetItem(i, 6, wxString::Format("%08x", _swapEndianU32(tempOffsets.endOffset))); - // volume - voiceListbox->SetItem(i, 7, wxString::Format("%04x", (uint16)internal->veVolume)); - // volume delta - voiceListbox->SetItem(i, 8, wxString::Format("%04x", (uint16)internal->veDelta)); - // src - voiceListbox->SetItem(i, 9, wxString::Format("%04x%04x", _swapEndianU16(internal->src.ratioHigh), _swapEndianU16(internal->src.ratioLow))); - // lpf - if (internal->lpf.on) - { - voiceListbox->SetItem(i, 10, wxString::Format("%04x", _swapEndianU16(internal->lpf.a0))); - voiceListbox->SetItem(i, 11, wxString::Format("%04x", _swapEndianU16(internal->lpf.b0))); - } - else - { - voiceListbox->SetItem(i, 10, ""); - voiceListbox->SetItem(i, 11, ""); - } - // biquad - if (internal->biquad.on) - { - voiceListbox->SetItem(i, 12, wxString::Format("%04x", _swapEndianU16(internal->biquad.b0))); - voiceListbox->SetItem(i, 13, wxString::Format("%04x", _swapEndianU16(internal->biquad.b1))); - voiceListbox->SetItem(i, 14, wxString::Format("%04x", _swapEndianU16(internal->biquad.b2))); - voiceListbox->SetItem(i, 15, wxString::Format("%04x", _swapEndianU16(internal->biquad.a1))); - voiceListbox->SetItem(i, 16, wxString::Format("%04x", _swapEndianU16(internal->biquad.a2))); - } - else - { - voiceListbox->SetItem(i, 12, ""); - voiceListbox->SetItem(i, 13, ""); - voiceListbox->SetItem(i, 14, ""); - voiceListbox->SetItem(i, 15, ""); - voiceListbox->SetItem(i, 16, ""); - } - wxString label; - // device mix - for (uint32 f = 0; f < snd_core::AX_TV_CHANNEL_COUNT*snd_core::AX_MAX_NUM_BUS; f++) - { - sint32 busIndex = f% snd_core::AX_MAX_NUM_BUS; - sint32 channelIndex = f / snd_core::AX_MAX_NUM_BUS; - - //debug_printf("DeviceMix TV Voice %08x b%02d/c%02d vol %04x delta %04x\n", hCPU->gpr[3], busIndex, channelIndex, _swapEndianU16(mixArrayBE[f].vol), _swapEndianU16(mixArrayBE[f].volDelta)); - uint32 mixVol = internal->deviceMixTV[channelIndex * 4 + busIndex].vol; - mixVol = (mixVol + 0x0FFF) >> (12); - label += wxString::Format("%x", mixVol); - //ax.voiceInternal[voiceIndex].deviceMixTVChannel[channelIndex].bus[busIndex].vol = _swapEndianU16(mixArrayBE[f].vol); - } - voiceListbox->SetItem(i, 17, label); - } - voiceListbox->Thaw(); -} - -void AudioDebuggerWindow::RefreshVoiceList() -{ - RefreshVoiceList_sndgeneric(); -} - -void AudioDebuggerWindow::OnVoiceListPopupClick(wxCommandEvent &evt) -{ - -} - -void AudioDebuggerWindow::OnVoiceListRightClick(wxMouseEvent& event) -{ - -} - -void AudioDebuggerWindow::Close() -{ - // this->MakeModal(false); - refreshTimer->Stop(); - this->Destroy(); -} diff --git a/src/gui/wxgui/AudioDebuggerWindow.h b/src/gui/wxgui/AudioDebuggerWindow.h deleted file mode 100644 index 896d4999..00000000 --- a/src/gui/wxgui/AudioDebuggerWindow.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include - -class wxListCtrl; - -class AudioDebuggerWindow : public wxFrame -{ -public: - AudioDebuggerWindow(wxFrame& parent); - - void OnCloseButton(wxCommandEvent& event); - void OnRefreshButton(wxCommandEvent& event); - void OnClose(wxCloseEvent& event); - void RefreshVoiceList_sndgeneric(); - void RefreshVoiceList(); - void OnRefreshTimer(wxTimerEvent& event); - void OnVoiceListPopupClick(wxCommandEvent &evt); - void OnVoiceListRightClick(wxMouseEvent& event); - - void Close(); - -private: - wxListCtrl* voiceListbox; - wxTimer* refreshTimer; - - wxDECLARE_EVENT_TABLE(); - - -}; diff --git a/src/gui/wxgui/CMakeLists.txt b/src/gui/wxgui/CMakeLists.txt deleted file mode 100644 index 079a78cd..00000000 --- a/src/gui/wxgui/CMakeLists.txt +++ /dev/null @@ -1,198 +0,0 @@ -add_library(CemuWxGui STATIC - canvas/IRenderCanvas.h - CemuApp.cpp - CemuApp.h - CemuUpdateWindow.cpp - CemuUpdateWindow.h - ChecksumTool.cpp - ChecksumTool.h - components/TextList.cpp - components/TextList.h - components/wxDownloadManagerList.cpp - components/wxDownloadManagerList.h - components/wxGameList.cpp - components/wxGameList.h - components/wxInputDraw.cpp - components/wxInputDraw.h - components/wxLogCtrl.cpp - components/wxLogCtrl.h - components/wxTitleManagerList.cpp - components/wxTitleManagerList.h - debugger/BreakpointWindow.cpp - debugger/BreakpointWindow.h - debugger/DebuggerWindow2.cpp - debugger/DebuggerWindow2.h - debugger/DisasmCtrl.cpp - debugger/DisasmCtrl.h - debugger/DumpCtrl.cpp - debugger/DumpCtrl.h - debugger/DumpWindow.cpp - debugger/DumpWindow.h - debugger/ModuleWindow.cpp - debugger/ModuleWindow.h - debugger/RegisterWindow.cpp - debugger/RegisterWindow.h - debugger/SymbolCtrl.cpp - debugger/SymbolCtrl.h - debugger/SymbolWindow.cpp - debugger/SymbolWindow.h - dialogs/CreateAccount/wxCreateAccountDialog.cpp - dialogs/CreateAccount/wxCreateAccountDialog.h - dialogs/SaveImport/SaveImportWindow.cpp - dialogs/SaveImport/SaveImportWindow.h - dialogs/SaveImport/SaveTransfer.cpp - dialogs/SaveImport/SaveTransfer.h - AudioDebuggerWindow.cpp - AudioDebuggerWindow.h - DownloadGraphicPacksWindow.cpp - DownloadGraphicPacksWindow.h - DownloadCustomGraphicPackWindow.cpp - DownloadCustomGraphicPackWindow.h - GameProfileWindow.cpp - GameProfileWindow.h - GameUpdateWindow.cpp - GameUpdateWindow.h - GeneralSettings2.cpp - GeneralSettings2.h - GettingStartedDialog.cpp - GettingStartedDialog.h - GraphicPacksWindow2.cpp - GraphicPacksWindow2.h - wxWindowSystem.cpp - helpers/wxControlObject.h - helpers/wxCustomData.h - helpers/wxCustomEvents.cpp - helpers/wxCustomEvents.h - helpers/wxHelpers.cpp - helpers/wxHelpers.h - helpers/wxLogEvent.h - helpers/wxWayland.cpp - helpers/wxWayland.h - input/HotkeySettings.cpp - input/HotkeySettings.h - input/InputAPIAddWindow.cpp - input/InputAPIAddWindow.h - input/InputSettings2.cpp - input/InputSettings2.h - input/panels/ClassicControllerInputPanel.cpp - input/panels/ClassicControllerInputPanel.h - input/panels/InputPanel.cpp - input/panels/InputPanel.h - input/panels/ProControllerInputPanel.cpp - input/panels/ProControllerInputPanel.h - input/panels/VPADInputPanel.cpp - input/panels/VPADInputPanel.h - input/panels/WiimoteInputPanel.cpp - input/panels/WiimoteInputPanel.h - input/settings/DefaultControllerSettings.cpp - input/settings/DefaultControllerSettings.h - input/settings/WiimoteControllerSettings.cpp - input/settings/WiimoteControllerSettings.h - LoggingWindow.cpp - LoggingWindow.h - MainWindow.cpp - MainWindow.h - MemorySearcherTool.cpp - MemorySearcherTool.h - PadViewFrame.cpp - PadViewFrame.h - TitleManager.cpp - TitleManager.h - wxCemuConfig.cpp - wxCemuConfig.h - EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp - EmulatedUSBDevices/EmulatedUSBDeviceFrame.h - windows/PPCThreadsViewer - windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp - windows/PPCThreadsViewer/DebugPPCThreadsWindow.h - windows/TextureRelationViewer - windows/TextureRelationViewer/TextureRelationWindow.cpp - windows/TextureRelationViewer/TextureRelationWindow.h - wxcomponents/checktree.cpp - wxcomponents/checktree.h - wxgui.h - wxHelper.h -) - -if (ENABLE_OPENGL) - target_sources(CemuWxGui PRIVATE - canvas/OpenGLCanvas.cpp - canvas/OpenGLCanvas.h - ) -endif() - -if (ENABLE_VULKAN) - target_sources(CemuWxGui PRIVATE - canvas/VulkanCanvas.cpp - canvas/VulkanCanvas.h - ) -endif() - -if (ENABLE_METAL) - target_sources(CemuWxGui PRIVATE - canvas/MetalCanvas.cpp - canvas/MetalCanvas.h - ) -endif() - -if (ENABLE_BLUEZ) - target_sources(CemuWxGui PRIVATE - input/PairingDialog.cpp - input/PairingDialog.h - ) -endif() - -set_property(TARGET CemuWxGui PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") - -cemu_use_precompiled_header(CemuWxGui) - - -target_include_directories(CemuWxGui PUBLIC "../") -# PUBLIC because rapidjson/document.h is included in ChecksumTool.h -target_include_directories(CemuWxGui PUBLIC ${RAPIDJSON_INCLUDE_DIRS}) - -target_link_libraries(CemuWxGui PRIVATE - CemuCommon - CemuResource - libzip::zip - ZArchive::zarchive - CemuComponents - pugixml::pugixml - CemuCafe - PUBLIC - CURL::libcurl -) - -if(ENABLE_WXWIDGETS AND UNIX AND NOT APPLE) - # PUBLIC because gdk/gdkkeysyms.h is included in guiWrapper.h - target_link_libraries(CemuWxGui PUBLIC GTK3::gtk) - if (ENABLE_WAYLAND) - target_link_libraries(CemuWxGui PRIVATE Wayland::Client CemuWaylandProtocols) - endif() -endif() - -if(ENABLE_CUBEB) - target_link_libraries(CemuWxGui PRIVATE cubeb::cubeb) -endif() - -if(ENABLE_SDL) - target_link_libraries(CemuWxGui PRIVATE SDL3::SDL3) -endif() - -if(UNIX AND NOT APPLE) - if(ENABLE_FERAL_GAMEMODE) - target_link_libraries(CemuWxGui PRIVATE gamemode) - endif() -endif() -if (ENABLE_WXWIDGETS) - # PUBLIC because wx/app.h is included in CemuApp.h - target_link_libraries(CemuWxGui PUBLIC wxWidgets::wxWidgets) -endif() - -if(WIN32) - target_link_libraries(CemuWxGui PRIVATE bthprops) -endif() - -if(ALLOW_PORTABLE) - target_compile_definitions(CemuWxGui PRIVATE CEMU_ALLOW_PORTABLE) -endif () diff --git a/src/gui/wxgui/CemuApp.cpp b/src/gui/wxgui/CemuApp.cpp deleted file mode 100644 index 98a3fc40..00000000 --- a/src/gui/wxgui/CemuApp.cpp +++ /dev/null @@ -1,678 +0,0 @@ -#include "wxgui/CemuApp.h" -#include "wxCemuConfig.h" -#include "wxgui/MainWindow.h" -#include "wxgui/wxgui.h" -#include "config/CemuConfig.h" -#ifdef ENABLE_VULKAN -#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" -#endif -#include "Cafe/HW/Latte/Core/LatteOverlay.h" -#include "config/ActiveSettings.h" -#include "config/LaunchSettings.h" -#include "wxgui/GettingStartedDialog.h" -#include "input/InputManager.h" -#include "input/api/SDL/SDLControllerProvider.h" -#include "wxgui/helpers/wxHelpers.h" -#include "Cemu/ncrypto/ncrypto.h" -#include "wxgui/input/HotkeySettings.h" -#include "wxgui/debugger/DebuggerWindow2.h" -#include - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND -#include "wxgui/helpers/wxWayland.h" -#endif -#if __WXGTK__ -#include -#endif - -#include -#include -#include -#include -#include -#include "wxHelper.h" - -#include "Cafe/TitleList/TitleList.h" -#include "Cafe/TitleList/SaveList.h" -#include "Cafe/CafeSystem.h" - -wxIMPLEMENT_APP_NO_MAIN(CemuApp); - -// defined in wxWindowSystem.cpp -extern WindowSystem::WindowInfo g_window_info; -extern std::shared_mutex g_mutex; - -// forward declarations from main.cpp -void UnitTests(); -void CemuCommonInit(); - -void HandlePostUpdate(); -// Translation strings to extract for gettext: -void unused_translation_dummy() -{ - void(_("Browse")); - void(_("Select a file")); - void(_("Select a directory")); - - void(_("Japanese")); - void(_("English")); - void(_("French")); - void(_("German")); - void(_("Italian")); - void(_("Spanish")); - void(_("Chinese")); - void(_("Korean")); - void(_("Dutch")); - void(_("Portugese")); - void(_("Russian")); - void(_("Taiwanese")); - void(_("unknown")); -} - -#if BOOST_OS_WINDOWS -#include -fs::path GetAppDataRoamingPath() -{ - PWSTR path = nullptr; - HRESULT result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path); - if (FAILED(result)) - { - CoTaskMemFree(path); - return {}; - } - std::string appDataPath = boost::nowide::narrow(path); - CoTaskMemFree(path); - return _utf8ToPath(appDataPath); -} -#endif - -#if BOOST_OS_WINDOWS -void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Windows -{ - std::error_code ec; - bool isPortable = false; - fs::path user_data_path, config_path, cache_path, data_path; - auto standardPaths = wxStandardPaths::Get(); - fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); - fs::path portablePath = exePath.parent_path() / "portable"; - data_path = exePath.parent_path(); // the data path is always the same as the exe path -#ifdef CEMU_ALLOW_PORTABLE - if (fs::is_directory(portablePath, ec)) - { - isPortable = true; - user_data_path = config_path = cache_path = portablePath; - } - else -#endif - { - fs::path roamingPath = GetAppDataRoamingPath() / "Cemu"; - user_data_path = config_path = cache_path = roamingPath; - } - // on Windows Cemu used to be portable by default prior to 2.0-89 - // to remain backwards compatible with old installations we check for settings.xml in the Cemu directory - // if it exists, we use the exe path as the portable directory - if(!isPortable) // lower priority than portable directory - { - if (fs::exists(exePath.parent_path() / "settings.xml", ec)) - { - isPortable = true; - user_data_path = config_path = cache_path = exePath.parent_path(); - } - } - ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); -} -#endif - -#if BOOST_OS_LINUX || BOOST_OS_BSD -void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for Linux -{ - std::error_code ec; - bool isPortable = false; - fs::path user_data_path, config_path, cache_path, data_path; - auto standardPaths = wxStandardPaths::Get(); - fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); - fs::path portablePath = exePath.parent_path() / "portable"; - // GetExecutablePath returns the AppImage's temporary mount location - wxString appImagePath; - if (wxGetEnv("APPIMAGE", &appImagePath)) - { - exePath = wxHelper::MakeFSPath(appImagePath); - portablePath = exePath.parent_path() / "portable"; - } -#ifdef CEMU_ALLOW_PORTABLE - if (fs::is_directory(portablePath, ec)) - { - isPortable = true; - user_data_path = config_path = cache_path = portablePath; - // in portable mode assume the data directories (resources, gameProfiles/default/) are next to the executable - data_path = exePath.parent_path(); - } - else -#endif - { - SetAppName("Cemu"); - wxString appName = GetAppName(); - standardPaths.SetFileLayout(wxStandardPaths::FileLayout::FileLayout_XDG); - auto getEnvDir = [&](const wxString& varName, const wxString& defaultValue) - { - wxString dir; - if (!wxGetEnv(varName, &dir) || dir.empty()) - return defaultValue; - return dir; - }; - wxString homeDir = wxFileName::GetHomeDir(); - user_data_path = (getEnvDir(wxS("XDG_DATA_HOME"), homeDir + wxS("/.local/share")) + "/" + appName).ToStdString(); - config_path = (getEnvDir(wxS("XDG_CONFIG_HOME"), homeDir + wxS("/.config")) + "/" + appName).ToStdString(); - data_path = standardPaths.GetDataDir().ToStdString(); - cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); - cache_path /= appName.ToStdString(); - } - ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); -} -#endif - -#if BOOST_OS_MACOS -void CemuApp::DeterminePaths(std::set& failedWriteAccess) // for MacOS -{ - std::error_code ec; - bool isPortable = false; - fs::path user_data_path, config_path, cache_path, data_path; - auto standardPaths = wxStandardPaths::Get(); - fs::path exePath(wxHelper::MakeFSPath(standardPaths.GetExecutablePath())); - // If run from an app bundle, use its parent directory - fs::path appPath = exePath.parent_path().parent_path().parent_path(); - fs::path portablePath = appPath.extension() == ".app" ? appPath.parent_path() / "portable" : exePath.parent_path() / "portable"; -#ifdef CEMU_ALLOW_PORTABLE - if (fs::is_directory(portablePath, ec)) - { - isPortable = true; - user_data_path = config_path = cache_path = portablePath; - data_path = exePath.parent_path(); - } - else -#endif - { - SetAppName("Cemu"); - wxString appName = GetAppName(); - user_data_path = config_path = standardPaths.GetUserDataDir().ToStdString(); - data_path = standardPaths.GetDataDir().ToStdString(); - cache_path = standardPaths.GetUserDir(wxStandardPaths::Dir::Dir_Cache).ToStdString(); - cache_path /= appName.ToStdString(); - } - ActiveSettings::SetPaths(isPortable, exePath, user_data_path, config_path, cache_path, data_path, failedWriteAccess); -} -#endif - -// create default MLC files or quit if it fails -void CemuApp::InitializeNewMLCOrFail(fs::path mlc) -{ - if( CemuApp::CreateDefaultMLCFiles(mlc) ) - return; // all good - cemu_assert_debug(!ActiveSettings::IsCustomMlcPath()); // should not be possible? - - if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) - { - // tell user that the custom path is not writable - wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); - } - wxMessageBox(formatWxString(_("Cemu failed to write to the mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); -} - -void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) -{ - if(CreateDefaultMLCFiles(mlc)) - return; // all good - // failed to write mlc files - if(ActiveSettings::IsCommandLineMlcPath() || ActiveSettings::IsCustomMlcPath()) - { - // tell user that the custom path is not writable - // if it's a command line path then just quit. Otherwise ask if user wants to reset the path - if(ActiveSettings::IsCommandLineMlcPath()) - { - wxMessageBox(formatWxString(_("Cemu failed to write to the custom mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); - } - // ask user if they want to reset the path - const wxString message = formatWxString(_("Cemu failed to write to the custom mlc directory.\n\nThe path is:\n{}\n\nCemu cannot start without a valid mlc path. Do you want to reset the path? You can later change it again in the General Settings."), - _pathToUtf8(mlc)); - wxMessageDialog dialog(nullptr, message, _("Error"), wxCENTRE | wxYES_NO | wxICON_WARNING); - dialog.SetYesNoLabels(_("Reset path"), _("Exit")); - const auto dialogResult = dialog.ShowModal(); - if (dialogResult == wxID_NO) - exit(0); - else // reset path - { - GetConfig().mlc_path = ""; - GetConfigHandle().Save(); - } - } - else - { - // default path is not writeable. Just let the user know and quit. Unsure if it would be a good idea to ask the user to choose an alternative path instead - wxMessageBox(formatWxString(_("Cemu failed to write to the default mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); - } -} - -std::string TranslationCallback(std::string_view msgId) -{ - return wxGetTranslation(wxString::FromUTF8(msgId)).utf8_string(); -} - -bool CemuApp::OnInit() -{ -#if __WXGTK__ - GTKSuppressDiagnostics(G_LOG_LEVEL_MASK & ~G_LOG_FLAG_FATAL); -#endif - std::set failedWriteAccess; - DeterminePaths(failedWriteAccess); - // make sure default cemu directories exist - CreateDefaultCemuFiles(); - - GetConfigHandle().SetFilename(ActiveSettings::GetConfigPath("settings.xml").generic_wstring()); - - auto& config = GetWxGUIConfig(); - - std::error_code ec; - bool isFirstStart = !fs::exists(ActiveSettings::GetConfigPath("settings.xml"), ec); - - NetworkConfig::LoadOnce(); - if (!isFirstStart) - { - GetConfigHandle().Load(); - sint32 language = config.language.GetValue(); - LocalizeUI(static_cast(language == wxLANGUAGE_DEFAULT ? wxLocale::GetSystemLanguage() : language)); - } - else - { - LocalizeUI(static_cast(wxLocale::GetSystemLanguage())); - } - - SetTranslationCallback(TranslationCallback); -#if __WXMSW__ - auto& wxGuiConfig = GetWxGUIConfig(); - if (wxGuiConfig.msw_theme.GetValue() == static_cast(MSWThemeOption::kAuto)) - { - MSWEnableDarkMode(DarkMode_Auto); - } - else if (wxGuiConfig.msw_theme.GetValue() == static_cast(MSWThemeOption::kDark)) - { - MSWEnableDarkMode(DarkMode_Always); - } - - // extend tooltip duration to the maximum possible value - wxToolTip::SetDelay(-1); - wxToolTip::SetAutoPop(MAKELPARAM(std::numeric_limits::max(),0)); -#endif - - for (auto&& path : failedWriteAccess) - { - wxMessageBox(formatWxString(_("Cemu can't write to {}!"), wxHelper::FromPath(path)), - _("Warning"), wxOK | wxCENTRE | wxICON_EXCLAMATION, nullptr); - } - - if (isFirstStart) - { - // show the getting started dialog - GettingStartedDialog dia(nullptr); - dia.ShowModal(); - // make sure config is created. Gfx pack UI and input UI may create it earlier already, but we still want to update it - GetConfigHandle().Save(); - // create mlc, on failure the user can quit here. So do this after the Getting Started dialog - InitializeNewMLCOrFail(ActiveSettings::GetMlcPath()); - } - else - { - // check if mlc is valid and recreate default files - InitializeExistingMLCOrFail(ActiveSettings::GetMlcPath()); - } - - ActiveSettings::Init(); // this is a bit of a misnomer, right now this call only loads certs for online play. In the future we should move the logic to a more appropriate place - HandlePostUpdate(); - - LatteOverlay_init(); - // run a couple of tests if in non-release mode -#ifdef CEMU_DEBUG_ASSERT - UnitTests(); -#endif - -#if BOOST_OS_MACOS - SDLControllerProvider::InitSDL(); -#endif - CemuCommonInit(); - -#if BOOST_OS_MACOS - m_sdlEventPumpTimer = new wxTimer(this); - Bind(wxEVT_TIMER, &CemuApp::OnSDLEventPumpTimer, this); - m_sdlEventPumpTimer->Start(5, wxTIMER_CONTINUOUS); -#endif - - wxInitAllImageHandlers(); - - // fill colour db - wxTheColourDatabase->AddColour("ERROR", wxColour(0xCC, 0, 0)); - wxTheColourDatabase->AddColour("SUCCESS", wxColour(0, 0xbb, 0)); - -#if BOOST_OS_WINDOWS - const auto parent_path = GetParentProcess(); - if(parent_path.has_filename()) - { - const auto filename = parent_path.filename().generic_string(); - if (boost::icontains(filename, "WiiU_USB_Helper")) - __fastfail(0); - } -#endif - #ifdef ENABLE_VULKAN - InitializeGlobalVulkan(); - #endif - - Bind(wxEVT_ACTIVATE_APP, &CemuApp::ActivateApp, this); - - m_mainFrame = new MainWindow(); - - std::unique_lock lock(g_mutex); - g_window_info.app_active = true; - - HotkeySettings::Init(m_mainFrame); - - SetTopWindow(m_mainFrame); - m_mainFrame->Show(); - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - if (wxWlIsWaylandWindow(m_mainFrame)) - wxWlSetAppId(m_mainFrame, "info.cemu.Cemu"); -#endif - - return true; -} - -int CemuApp::OnExit() -{ -#if BOOST_OS_MACOS - if (m_sdlEventPumpTimer) - { - m_sdlEventPumpTimer->Stop(); - Unbind(wxEVT_TIMER, &CemuApp::OnSDLEventPumpTimer, this); - delete m_sdlEventPumpTimer; - m_sdlEventPumpTimer = nullptr; - } -#endif - wxApp::OnExit(); - wxTheClipboard->Flush(); - InputManager::instance().Shutdown(); - int retValue = 0; - if (auto r = CafeSystem::GetForegroundTitleReturnStatus(); (LaunchSettings::GetLoadFile() || LaunchSettings::GetLoadTitleID()) && r) - retValue = *r; -#if BOOST_OS_MACOS - SDLControllerProvider::ShutdownSDL(); -#endif - // handle restart if requested - if (m_restartExecutable.has_value() && !m_restartExecutable->empty() && fs::exists(*m_restartExecutable)) - { - fs::path restartPath = *m_restartExecutable; -#if BOOST_OS_WINDOWS - PROCESS_INFORMATION pi{}; - STARTUPINFOW si{}; - si.cb = sizeof(si); - std::wstring cmdline; - cmdline = L"\"" + boost::nowide::widen(_pathToUtf8(restartPath)) + L"\""; - CreateProcessW(nullptr, (wchar_t*)cmdline.c_str(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi); -#elif BOOST_OS_LINUX - std::string appPath = _pathToUtf8(restartPath); - execlp(appPath.c_str(), appPath.c_str(), (char *)NULL); -#elif BOOST_OS_MACOS - std::string appPath = _pathToUtf8(restartPath); - execlp(appPath.c_str(), appPath.c_str(), (char *)NULL); -#endif - } -#if BOOST_OS_WINDOWS - ExitProcess(retValue); -#else - _Exit(retValue); -#endif -} - -#if BOOST_OS_MACOS -void CemuApp::OnSDLEventPumpTimer(wxTimerEvent& event) -{ - // this callback is only used on macOS where SDL event functions need to be called on the main thread - // on other platforms SDLControllerProvider creates a separate thread for SDL event polling - SDLControllerProvider::PumpSDLEvents(); -} -#endif - -#if BOOST_OS_WINDOWS -void DumpThreadStackTrace(); -#endif - -void CemuApp::OnAssertFailure(const wxChar* file, int line, const wxChar* func, const wxChar* cond, const wxChar* msg) -{ - cemuLog_createLogFile(false); - cemuLog_log(LogType::Force, "Encountered wxWidgets assert!"); - cemuLog_log(LogType::Force, "File: {0} Line: {1}", wxString(file).utf8_string(), line); - cemuLog_log(LogType::Force, "Func: {0} Cond: {1}", wxString(func).utf8_string(), wxString(cond).utf8_string()); - cemuLog_log(LogType::Force, "Message: {}", wxString(msg).utf8_string()); - -#if BOOST_OS_WINDOWS - DumpThreadStackTrace(); -#endif - cemu_assert_debug(false); -} - -int CemuApp::FilterEvent(wxEvent& event) -{ - if(event.GetEventType() == wxEVT_KEY_DOWN) - { - const auto& key_event = (wxKeyEvent&)event; - g_window_info.set_keystate(fix_raw_keycode(key_event.GetRawKeyCode(), key_event.GetRawKeyFlags()), true); - } - else if(event.GetEventType() == wxEVT_KEY_UP) - { - const auto& key_event = (wxKeyEvent&)event; - g_window_info.set_keystate(fix_raw_keycode(key_event.GetRawKeyCode(), key_event.GetRawKeyFlags()), false); - } - else if(event.GetEventType() == wxEVT_ACTIVATE_APP) - { - const auto& activate_event = (wxActivateEvent&)event; - if(!activate_event.GetActive()) - g_window_info.set_keystatesup(); - } - - // track if debugger window or its child windows are focused - if (s_debuggerWindow && (event.GetEventType() == wxEVT_SET_FOCUS || event.GetEventType() == wxEVT_ACTIVATE)) - { - wxWindow* target_window = wxDynamicCast(event.GetEventObject(), wxWindow); - - if (target_window && event.GetEventType() == wxEVT_ACTIVATE && !((wxActivateEvent&)event).GetActive()) - target_window = nullptr; - - if (target_window) - { - g_window_info.debugger_focused = false; - wxWindow* window_it = target_window; - while (window_it) - { - if (window_it == s_debuggerWindow) g_window_info.debugger_focused = true; - window_it = window_it->GetParent(); - } - } - } - else if (!s_debuggerWindow) - { - g_window_info.debugger_focused = false; - } - - return wxApp::FilterEvent(event); -} - -std::vector CemuApp::GetLanguages() const { - std::vector availableLanguages(m_availableTranslations); - availableLanguages.insert(availableLanguages.begin(), wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - return availableLanguages; -} - -void CemuApp::LocalizeUI(wxLanguage languageToUse) -{ - std::unique_ptr translationsMgr(new wxTranslations()); - m_availableTranslations = GetAvailableTranslationLanguages(translationsMgr.get()); - - bool isTranslationAvailable = std::any_of(m_availableTranslations.begin(), m_availableTranslations.end(), - [languageToUse](const wxLanguageInfo* info) { return info->Language == languageToUse; }); - if (languageToUse == wxLANGUAGE_DEFAULT || isTranslationAvailable) - { - translationsMgr->SetLanguage(static_cast(languageToUse)); - translationsMgr->AddCatalog("cemu"); - - if (translationsMgr->IsLoaded("cemu") && wxLocale::IsAvailable(languageToUse)) - { - m_locale.Init(languageToUse); - } - // This must be run after wxLocale::Init, as the latter sets up its own wxTranslations instance which we want to override - wxTranslations::Set(translationsMgr.release()); - } -} - -std::vector CemuApp::GetAvailableTranslationLanguages(wxTranslations* translationsMgr) -{ - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(wxHelper::FromPath(ActiveSettings::GetDataPath("resources"))); - std::vector languages; - for (const auto& langName : translationsMgr->GetAvailableTranslations("cemu")) - { - const auto* langInfo = wxLocale::FindLanguageInfo(langName); - if (langInfo) - languages.emplace_back(langInfo); - } - return languages; -} - -bool CemuApp::CheckMLCPath(const fs::path& mlc) -{ - std::error_code ec; - if (!fs::exists(mlc, ec)) - return false; - if (!fs::exists(mlc / "usr", ec) || !fs::exists(mlc / "sys", ec)) - return false; - return true; -} - -bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc) -{ - auto CreateDirectoriesIfNotExist = [](const fs::path& path) - { - std::error_code ec; - if (!fs::exists(path, ec)) - return fs::create_directories(path, ec); - return true; - }; - // list of directories to create - const fs::path directories[] = { - mlc, - mlc / "sys", - mlc / "usr", - mlc / "usr/title/00050000", // base - mlc / "usr/title/0005000c", // dlc - mlc / "usr/title/0005000e", // update - mlc / "usr/save/00050010/1004a000/user/common/db", // Mii Maker save folders {0x500101004A000, 0x500101004A100, 0x500101004A200} - mlc / "usr/save/00050010/1004a100/user/common/db", - mlc / "usr/save/00050010/1004a200/user/common/db", - mlc / "sys/title/0005001b/1005c000/content" // lang files - }; - for(auto& path : directories) - { - if(!CreateDirectoriesIfNotExist(path)) - return false; - } - // create sys/usr folder in mlc01 - try - { - const auto langDir = fs::path(mlc).append("sys/title/0005001b/1005c000/content"); - auto langFile = fs::path(langDir).append("language.txt"); - if (!fs::exists(langFile)) - { - std::ofstream file(langFile); - if (file.is_open()) - { - const char* langStrings[] = { "ja","en","fr","de","it","es","zh","ko","nl","pt","ru","zh" }; - for (const char* lang : langStrings) - file << fmt::format(R"("{}",)", lang) << std::endl; - - file.flush(); - file.close(); - } - } - - auto countryFile = fs::path(langDir).append("country.txt"); - if (!fs::exists(countryFile)) - { - std::ofstream file(countryFile); - for (sint32 i = 0; i < NCrypto::GetCountryCount(); i++) - { - const char* countryCode = NCrypto::GetCountryAsString(i); - if (boost::iequals(countryCode, "NN")) - file << "NULL," << std::endl; - else - file << fmt::format(R"("{}",)", countryCode) << std::endl; - } - file.flush(); - file.close(); - } - // create a dummy file in the mlc folder to check if it's writable - const auto dummyFile = fs::path(mlc).append("writetestdummy"); - std::ofstream file(dummyFile); - if (!file.is_open()) - return false; - file.close(); - fs::remove(dummyFile); - } - catch (const std::exception& ex) - { - return false; - } - return true; -} - -void CemuApp::CreateDefaultCemuFiles() -{ - // cemu directories - try - { - const auto controllerProfileFolder = ActiveSettings::GetConfigPath("controllerProfiles"); - if (!fs::exists(controllerProfileFolder)) - fs::create_directories(controllerProfileFolder); - - const auto memorySearcherFolder = ActiveSettings::GetUserDataPath("memorySearcher"); - if (!fs::exists(memorySearcherFolder)) - fs::create_directories(memorySearcherFolder); - } - catch (const std::exception& ex) - { - wxString errorMsg = formatWxString(_("Couldn't create a required cemu directory or file!\n\nError: {0}"), ex.what()); - -#if BOOST_OS_WINDOWS - const DWORD lastError = GetLastError(); - if (lastError != ERROR_SUCCESS) - errorMsg << fmt::format("\n\n{}", GetSystemErrorMessage(lastError)); -#endif - - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - exit(0); - } -} - -void CemuApp::ActivateApp(wxActivateEvent& event) -{ - g_window_info.app_active = event.GetActive(); - event.Skip(); -} - -void CemuApp::RequestRestart(fs::path executablePath) -{ - m_restartExecutable = executablePath; - CallAfter([this, executablePath = std::move(executablePath)]() mutable - { - if (m_mainFrame && !m_mainFrame->IsBeingDeleted()) - m_mainFrame->Close(true); - }); -} diff --git a/src/gui/wxgui/CemuApp.h b/src/gui/wxgui/CemuApp.h deleted file mode 100644 index 978123dd..00000000 --- a/src/gui/wxgui/CemuApp.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -class MainWindow; -class wxTimer; -class wxTimerEvent; - -class CemuApp : public wxApp -{ -public: - bool OnInit() override; - int OnExit() override; - - void OnAssertFailure(const wxChar* file, int line, const wxChar* func, const wxChar* cond, const wxChar* msg) override; - int FilterEvent(wxEvent& event) override; - - std::vector GetLanguages() const; - - static bool CheckMLCPath(const fs::path& mlc); - static bool CreateDefaultMLCFiles(const fs::path& mlc); - static void CreateDefaultCemuFiles(); - - static void InitializeNewMLCOrFail(fs::path mlc); - static void InitializeExistingMLCOrFail(fs::path mlc); - - void RequestRestart(fs::path executablePath); -private: - void LocalizeUI(wxLanguage languageToUse); - - void DeterminePaths(std::set& failedWriteAccess); - - void ActivateApp(wxActivateEvent& event); - static std::vector GetAvailableTranslationLanguages(wxTranslations* translationsMgr); - - MainWindow* m_mainFrame = nullptr; -#if BOOST_OS_MACOS - void OnSDLEventPumpTimer(wxTimerEvent& event); - wxTimer* m_sdlEventPumpTimer = nullptr; -#endif - - wxLocale m_locale; - std::vector m_availableTranslations; - std::optional m_restartExecutable; -}; - -wxDECLARE_APP(CemuApp); diff --git a/src/gui/wxgui/CemuUpdateWindow.cpp b/src/gui/wxgui/CemuUpdateWindow.cpp deleted file mode 100644 index 2b1183c0..00000000 --- a/src/gui/wxgui/CemuUpdateWindow.cpp +++ /dev/null @@ -1,861 +0,0 @@ -#include "wxgui/CemuUpdateWindow.h" - -#include "Common/version.h" -#include "util/helpers/helpers.h" -#include "util/helpers/SystemException.h" -#include "util/helpers/ZipArchive.h" -#include "config/ActiveSettings.h" -#include "Common/FileStream.h" -#include "wxCemuConfig.h" -#include "CemuApp.h" - -#include -#include -#include -#include -#include - -#ifndef BOOST_OS_WINDOWS -#include -#include -#endif - -#include -#include -#include - - -wxDECLARE_EVENT(wxEVT_RESULT, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_RESULT, wxCommandEvent); - -wxDECLARE_EVENT(wxEVT_PROGRESS, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_PROGRESS, wxCommandEvent); - -CemuUpdateWindow::CemuUpdateWindow(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Cemu update"), wxDefaultPosition, wxDefaultSize, - wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); - m_gauge->SetValue(0); - sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); - - auto* rows = new wxFlexGridSizer(0, 2, 0, 0); - rows->AddGrowableCol(1); - - m_text = new wxStaticText(this, wxID_ANY, _("Checking for latest version...")); - rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - { - auto* right_side = new wxBoxSizer(wxHORIZONTAL); - - m_updateButton = new wxButton(this, wxID_ANY, _("Update")); - m_updateButton->Bind(wxEVT_BUTTON, &CemuUpdateWindow::OnUpdateButton, this); - right_side->Add(m_updateButton, 0, wxALL, 5); - - m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &CemuUpdateWindow::OnCancelButton, this); - right_side->Add(m_cancelButton, 0, wxALL, 5); - - rows->Add(right_side, 1, wxALIGN_RIGHT, 5); - } - - m_changelog = new wxHyperlinkCtrl(this, wxID_ANY, _("Changelog"), wxEmptyString); - rows->Add(m_changelog, 0, wxLEFT | wxBOTTOM | wxRIGHT | wxEXPAND, 5); - - sizer->Add(rows, 0, wxALL | wxEXPAND, 5); - - SetSizerAndFit(sizer); - Centre(wxBOTH); - - Bind(wxEVT_CLOSE_WINDOW, &CemuUpdateWindow::OnClose, this); - Bind(wxEVT_RESULT, &CemuUpdateWindow::OnResult, this); - Bind(wxEVT_PROGRESS, &CemuUpdateWindow::OnGaugeUpdate, this); - m_thread = std::thread(&CemuUpdateWindow::WorkerThread, this); - - m_updateButton->Hide(); - m_changelog->Hide(); -} - -CemuUpdateWindow::~CemuUpdateWindow() -{ - m_order = WorkerOrder::Exit; - if (m_thread.joinable()) - m_thread.join(); -} - -size_t CemuUpdateWindow::WriteStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata) -{ - ((std::string*)userdata)->append(ptr, size * nmemb); - return size * nmemb; -}; - -static std::string CurlUrlEscape(CURL* curl, const std::string& input) -{ - char* escapedStr = curl_easy_escape(curl, input.c_str(), input.size()); - std::string r(escapedStr); - curl_free(escapedStr); - return r; -} - -static std::string CurlUrlUnescape(CURL* curl, std::string_view input) -{ - int decodedLen = 0; - char* decoded = curl_easy_unescape(curl, input.data(), input.size(), &decodedLen); - std::string r(decoded, decodedLen); - curl_free(decoded); - return r; -} - -static std::string GenerateSecureRandomString() -{ - std::array bytes{}; - if (RAND_bytes(bytes.data(), bytes.size()) != 1) - return ""; - std::string result; - result.reserve(21); - for (unsigned char byte : bytes) - { - result.push_back('a' + (byte % 26)); - } - return result; -} - -static bool HexToBytes(const std::string& hex, std::vector& dataOut) -{ - if (hex.size() % 2) - return false; - dataOut.clear(); - dataOut.reserve(hex.size() / 2); - for (size_t i = 0; i < hex.size(); i += 2) - { - unsigned int x; - auto [p, ec] = std::from_chars(hex.data() + i, hex.data() + i + 2, x, 16); - if (ec != std::errc{} || p != hex.data() + i + 2) - return false; - dataOut.push_back(static_cast(x)); - } - return true; -} - -static bool VerifyUpdateResponseSignature(std::string_view message, std::string signatureAsHex) -{ - std::vector signatureData; - if (!HexToBytes(signatureAsHex, signatureData)) - return false; - EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, (const unsigned char*)"\xa0\x73\xda\x45\x12\x11\xd6\xe0\x2d\x7b\x1a\xb3\x69\xfd\x6c\xc5\x99\xd3\xd3\x1e\xe7\x9d\x57\x89\x2a\xe6\xe8\x40\x54\x7c\xc5\xbd", 32); - if (!key) - return false; - EVP_MD_CTX* context = EVP_MD_CTX_new(); - if (!context) - { - EVP_PKEY_free(key); - return false; - } - bool valid = EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, key) == 1 && - EVP_DigestVerify(context,signatureData.data(),signatureData.size(), reinterpret_cast(message.data()), message.size()) == 1; - EVP_MD_CTX_free(context); - EVP_PKEY_free(key); - return valid; -} - -// returns empty string for platforms which cant auto-update or dont have support for it yet -static std::string GetPlatformUpdateIdentifier() -{ - std::string identifier; -#if BOOST_OS_LINUX - const char* appImagePath = std::getenv("APPIMAGE"); - if (appImagePath && *appImagePath) - identifier.append("linux_appimage"); - else - return ""; -#elif BOOST_OS_WINDOWS - identifier.append("windows"); -#elif BOOST_OS_MACOS - identifier.append("macos_bundle"); -#elif BOOST_OS_BSD - return ""; // BSD users must update from source -#else - return ""; -#endif -#if defined(__aarch64__) - identifier.append("_aarch64"); -#elif defined(ARCH_X86_64) - identifier.append("_x86_64"); -#else - return ""; -#endif - return identifier; -} - -// returns true if update is available and sets output parameters -bool CemuUpdateWindow::QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut, std::array& fileSha256Out) -{ - std::string rdString = GenerateSecureRandomString(); - if (rdString.empty()) - return false; - std::string buffer; - std::string urlStr("https://cemu.info/api2/version.php?v="); - auto* curl = curl_easy_init(); - urlStr.append(CurlUrlEscape(curl, BUILD_VERSION_STRING)); - - std::string platformIdentifier = GetPlatformUpdateIdentifier(); - if (platformIdentifier.empty()) - return false; - - urlStr.append("&platform="); - urlStr.append(platformIdentifier); - - const auto& config = GetWxGUIConfig(); - urlStr.append("&s="); - urlStr.append(rdString); - if(config.receive_untested_updates) - urlStr.append("&allowNewUpdates=1"); - - curl_easy_setopt(curl, CURLOPT_URL, urlStr.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteStringCallback); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - - bool result = false; - CURLcode cr = curl_easy_perform(curl); - if (cr == CURLE_OK) - { - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - if (http_code != 0 && http_code != 200) - { - cemuLog_log(LogType::Force, "Update check failed (http code: {})", http_code); - cemu_assert_debug(false); - return false; - } - - std::vector tokens; - const boost::char_separator sep{ "|" }; - for (const auto& token : boost::tokenizer(buffer, sep)) - tokens.emplace_back(token); - - if (tokens.size() >= 5 && tokens[0] == "UPDATE") - { - // first token: "UPDATE" - // second token: Download URL - // third token: Changelog URL - // fourth token: SHA256 - // fifth token: Signature - // we allow more tokens in case we ever want to add extra information for future releases - downloadUrlOut = CurlUrlUnescape(curl, tokens[1]); - changelogUrlOut = CurlUrlUnescape(curl, tokens[2]); - if (!downloadUrlOut.empty() && !changelogUrlOut.empty()) - result = true; - // check signature - std::string signedMessage = platformIdentifier.append("|").append(tokens[0]).append("|").append(tokens[1]).append("|").append(tokens[2]).append("|").append(tokens[3]).append("|").append(rdString); - bool isValidSignature = VerifyUpdateResponseSignature(signedMessage, tokens[4]); - if (!isValidSignature) - { - cemuLog_log(LogType::Force, "Failed server signature check"); - return false; - } - std::vector fileShaVec; - if (!HexToBytes(tokens[3], fileShaVec)) - return false; - if (fileShaVec.size() != 32) - return false; - std::memcpy(fileSha256Out.data(), fileShaVec.data(), 32); - } - } - else - { - cemuLog_log(LogType::Force, "Update check failed with CURL error {}", (int)cr); - cemu_assert_debug(false); - } - curl_easy_cleanup(curl); - return result; -} - -std::future CemuUpdateWindow::IsUpdateAvailableAsync() -{ - return std::async(std::launch::async, CheckVersion); -} - -bool CemuUpdateWindow::CheckVersion() -{ - std::string downloadUrl, changelogUrl; - std::array fileSha256; - return QueryUpdateInfo(downloadUrl, changelogUrl, fileSha256); -} - - -int CemuUpdateWindow::ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -{ - auto* thisptr = (CemuUpdateWindow*)clientp; - auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt((int)dlnow); - wxQueueEvent(thisptr, event); - return 0; -} - -void CemuUpdateWindow::SubmitWorkerResult(Result newWorkerState) -{ - auto* event = new wxCommandEvent(wxEVT_RESULT); - event->SetInt((int)newWorkerState); - wxQueueEvent(this, event); -} - -static std::atomic s_cemuUpdateCancelRequest{false}; - -bool CemuUpdateWindow::DownloadCemuUpdateFile(const std::string& url, const fs::path& filename) -{ - if (!fs::exists(filename.parent_path())) - fs::create_directories(filename.parent_path()); - auto* curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_NOBODY, 1); - curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - auto r = curl_easy_perform(curl); - if (r != CURLE_OK) - { - cemuLog_log(LogType::Force, "Cemu update download failed with error {}", r); - curl_easy_cleanup(curl); - return false; - } - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - if (http_code != 0 && http_code != 200) - { - cemuLog_log(LogType::Force, "Unable to download cemu update file from {} (http error: {})", url, http_code); - curl_easy_cleanup(curl); - return false; - } - - curl_off_t update_size; - if (curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &update_size) == CURLE_OK) - m_gaugeMaxValue = (int)update_size; - - std::vector fileBuffer; - - s_cemuUpdateCancelRequest = false; - auto _curlWriteData = +[](void* ptr, size_t size, size_t nmemb, void* ctx) -> size_t - { - if (s_cemuUpdateCancelRequest) - return 0; - std::vector* fileBufferVec = (std::vector*)ctx; - const size_t writeSize = size * nmemb; - fileBufferVec->insert(fileBufferVec->end(), (uint8*)ptr, (uint8*)ptr + writeSize); - return writeSize; - }; - - curl_easy_setopt(curl, CURLOPT_NOBODY, 0); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curlWriteData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &fileBuffer); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); - curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); - - auto curl_result = std::async(std::launch::async, [](CURL* curl, long* http_code) - { - const auto r = curl_easy_perform(curl); - return r; - }, curl, &http_code); - while (true) - { - auto r = curl_result.wait_for(std::chrono::milliseconds(100)); - if (r == std::future_status::ready) - break; - if (m_order == WorkerOrder::Exit && !s_cemuUpdateCancelRequest) - s_cemuUpdateCancelRequest = true; - } - curl_easy_cleanup(curl); - CURLcode curlResult = curl_result.get(); - if (curlResult == CURLE_ABORTED_BY_CALLBACK) - { - return false; - } - else if (curlResult != CURLE_OK) - { - return false; - } - // store file to temp location - std::error_code ec; - fs::remove(filename, ec); - if (!FileStream::WriteFileAtomic(filename, fileBuffer)) - { - cemuLog_log(LogType::Force, "Failed to store update file"); - return false; - } - // read back file and verify the hash - auto diskFileData = FileStream::LoadIntoMemory(filename); - if (!diskFileData) - { - cemuLog_log(LogType::Force, "Failed to read back update file"); - return false; - } - // verify the file hash - std::array hash; - EVP_Q_digest(nullptr, "SHA256", nullptr, diskFileData->data(), diskFileData->size(), hash.data(), nullptr); - if (hash != m_fileSHA256) - { - cemuLog_log(LogType::Force, "File hash of downloaded update does not match"); - fs::remove(filename, ec); - return false; - } - SubmitWorkerResult(Result::UpdateDownloaded); - return true; -} - -bool CemuUpdateWindow::ExtractZipUpdate(const fs::path& zipname, const fs::path& targetpath) -{ - ZipArchive archive(zipname); - if (!archive.IsValid()) - { - cemuLog_log(LogType::Force, "Cannot open zip file: {}", _pathToUtf8(zipname)); - return false; - } - const size_t count = archive.GetNumEntries(); - m_gaugeMaxValue = count; - // the update files are in a directory called "Cemu_". Find the name of the directory - std::string cemuPathPrefix; - for (size_t i=0; i fileData; - if (!archive.ExtractIntoMemoryByIndex(i, fileData)) - { - cemuLog_log(LogType::Force, "Failed to extract file {}", entry.fullPath); - return false; - } - // make sure the folder exists - const fs::path fname = targetpath / entryPath; - try - { - fs::create_directories(fname.parent_path()); - } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "can't create folder for update file \"{}\": {}", entry.fullPath, sys.what()); - return false; - } - } - } - // check if there is enough disk space (to keep this simple we assume 100MB, generally releases should be below that) - std::error_code ec; - const fs::space_info targetSpace = fs::space(ActiveSettings::GetExecutablePath().parent_path(), ec); - if (targetSpace.free <= 100 * 1024 * 1024) - { - cemuLog_log(LogType::Force, "Not enough disk space available for update"); - return false; - } - // actually extract and replace files - for (size_t i = 0; i < count; i++) - { - ZipArchive::ZipFileEntry entry; - if (archive.GetFileEntryByIndex(i, entry)) - { - if (!entry.fullPath.starts_with(cemuPathPrefxWithSlash)) - continue; - const fs::path entryPath = _utf8ToPath(entry.fullPath.substr(cemuPathPrefxWithSlash.length())); - // read into memory - std::vector fileData; - if (!archive.ExtractIntoMemoryByIndex(i, fileData)) - { - cemu_assert(false); // extraction tested earlier, it should not fail here - } - const fs::path fname = targetpath / entryPath; - if ( !FileStream::WriteFileAtomic(fname, fileData, true) ) - { - cemuLog_log(LogType::Force, "Failed to update {}", _pathToUtf8(fname)); - // continue regardless - } - if ((i / 10) * 10 == i) - { - auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt(static_cast(i)); - wxQueueEvent(this, event); - } - } - } - - auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt(m_gaugeMaxValue); - wxQueueEvent(this, event); - - return true; -} - -#if BOOST_OS_WINDOWS -bool CemuUpdateWindow::WorkerThread_Windows() -{ - const auto tmppath = fs::temp_directory_path() / L"cemu_update"; - std::error_code ec; - if (exists(tmppath)) - remove_all(tmppath, ec); - const auto updateSrcFile = tmppath / L"update.zip"; - if (!DownloadCemuUpdateFile(m_downloadUrl, updateSrcFile)) - return false; - if (m_order == WorkerOrder::Exit) - return false; - // extract and replace files - if (!ExtractZipUpdate(updateSrcFile, ActiveSettings::GetExecutablePath().parent_path())) - { - cemuLog_log(LogType::Force, "Extracting Cemu zip failed"); - return false; - } - SubmitWorkerResult(Result::ExtractSuccess); - // set relaunch path - fs::path newExePath = ActiveSettings::GetExecutablePath(); - newExePath = newExePath.parent_path().append("Cemu.exe"); - m_restartFile = newExePath; - return true; -} -#endif - -#if BOOST_OS_LINUX -bool CemuUpdateWindow::WorkerThread_AppImage() -{ - const auto tmppath = fs::temp_directory_path() / L"cemu_update"; - std::error_code ec; - if (exists(tmppath, ec)) - remove_all(tmppath, ec); - const auto updateSrcFile = tmppath / L"Cemu.AppImage"; - if (!DownloadCemuUpdateFile(m_downloadUrl, updateSrcFile)) - return false; - if (m_order == WorkerOrder::Exit) - return false; - const char* appimage_path = std::getenv("APPIMAGE"); - auto backupExecutable = fs::path(appimage_path); - backupExecutable.replace_extension( _utf8ToPath(_pathToUtf8(backupExecutable.extension()).append(".backup"))); - const char* filePath = updateSrcFile.c_str(); - mode_t permissions = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; - fs::rename(appimage_path, backupExecutable, ec); - if (ec) - { - cemuLog_log(LogType::Force, "Failed to rename current .AppImage for update replacement"); - return false; - } - m_restartFile = appimage_path; - chmod(filePath, permissions); - wxString wxAppPath = wxString::FromUTF8(appimage_path); - wxCopyFile (wxString::FromUTF8(_pathToUtf8(updateSrcFile)), wxAppPath); - return true; -} -#endif - -#if BOOST_OS_MACOS -bool CemuUpdateWindow::WorkerThread_MacBundle() -{ - const fs::path tempPath = fs::temp_directory_path() / "cemu_update"; - - std::error_code ec; - fs::remove_all(tempPath, ec); - fs::create_directories(tempPath, ec); - if (ec) - return false; - - const fs::path dmgPath = tempPath / "cemu.dmg"; - if (!DownloadCemuUpdateFile(m_downloadUrl, dmgPath)) - return false; - - if (m_order == WorkerOrder::Exit) - return false; - - // .../Cemu.app/Contents/MacOS/Cemu - const fs::path executable = ActiveSettings::GetExecutablePath(); - const fs::path appBundle = executable.parent_path().parent_path().parent_path(); - if (appBundle.extension() != ".app") - { - cemuLog_log(LogType::Force, "Cemu is not running from a macOS app bundle"); - return false; - } - - const fs::path scriptPath = tempPath / "update.sh"; - const fs::path mountPath = tempPath / "mount"; - - auto ShellQuote = [](std::string_view value) - { - std::string result = "'"; - for (const char c : value) - { - if (c == '\'') - result += "'\\''"; - else - result += c; - } - result += "'"; - return result; - }; - - std::string dmg = ShellQuote(_pathToUtf8(dmgPath)); - std::string app = ShellQuote(_pathToUtf8(appBundle)); - std::string mount = ShellQuote(_pathToUtf8(mountPath)); - - std::string script; - - script += "#!/bin/sh\n"; - script += "DMG_PATH=" + ShellQuote(_pathToUtf8(dmgPath)) + "\n"; - script += "APP_BUNDLE=" + ShellQuote(_pathToUtf8(appBundle)) + "\n"; - script += "MOUNT_PATH=" + ShellQuote(_pathToUtf8(mountPath)) + "\n"; - - script += R"SH( -set -eu - -STAGED_BUNDLE="${APP_BUNDLE}.update" -BACKUP_BUNDLE="${APP_BUNDLE}.backup" -MOUNTED=0 - -finish() -{ - STATUS=$? - trap - EXIT HUP INT TERM - - if [ "$MOUNTED" -eq 1 ]; then - /usr/bin/hdiutil detach "$MOUNT_PATH" >/dev/null 2>&1 || true - fi - - if [ -d "$APP_BUNDLE" ]; then - /usr/bin/open -n "$APP_BUNDLE" || true - elif [ -x "$BACKUP_BUNDLE/Contents/MacOS/Cemu" ]; then - "$BACKUP_BUNDLE/Contents/MacOS/Cemu" & - fi - - exit "$STATUS" -} - -trap finish EXIT HUP INT TERM - -/bin/rm -rf "$MOUNT_PATH" "$STAGED_BUNDLE" -/bin/mkdir -p "$MOUNT_PATH" - -/usr/bin/hdiutil attach \ - -nobrowse \ - -readonly \ - -mountpoint "$MOUNT_PATH" \ - "$DMG_PATH" - -MOUNTED=1 - -/usr/bin/ditto \ - "$MOUNT_PATH/Cemu.app" \ - "$STAGED_BUNDLE" - -/usr/bin/codesign \ - --verify \ - --strict \ - "$STAGED_BUNDLE/Contents/MacOS/Cemu" - -/usr/bin/hdiutil detach "$MOUNT_PATH" -MOUNTED=0 - -/bin/rm -rf "$BACKUP_BUNDLE" -/bin/mv "$APP_BUNDLE" "$BACKUP_BUNDLE" - -if ! /bin/mv "$STAGED_BUNDLE" "$APP_BUNDLE"; then - /bin/mv "$BACKUP_BUNDLE" "$APP_BUNDLE" || true - exit 1 -fi -)SH"; - - std::vector scriptData(script.begin(), script.end()); - if (!FileStream::WriteFileAtomic(scriptPath, scriptData)) - { - cemuLog_log(LogType::Force, "Failed to create macOS updater script"); - return false; - } - if (chmod(scriptPath.c_str(), S_IRUSR | S_IWUSR | S_IXUSR) != 0) - { - cemuLog_log(LogType::Force, "Failed to make updater script executable"); - return false; - } - // run the script on exit - m_restartFile = scriptPath; - return true; -} -#endif - -void CemuUpdateWindow::WorkerThread() -{ - // start writing log.txt if not created yet - cemuLog_createLogFile(false); - - while (true) - { - std::unique_lock lock(m_mutex); - WorkerOrder nextWorkOrder = WorkerOrder::Idle; - while (true) - { - nextWorkOrder = m_order.exchange(WorkerOrder::Idle); - if (nextWorkOrder != WorkerOrder::Idle) - break; - m_condition.wait_for(lock, std::chrono::milliseconds(125)); - } - if (nextWorkOrder == WorkerOrder::Exit) - break; - if (nextWorkOrder == WorkerOrder::CheckVersion) - { - auto* event = new wxCommandEvent(wxEVT_RESULT); - if (QueryUpdateInfo(m_downloadUrl, m_changelogUrl, m_fileSHA256)) - event->SetInt((int)Result::UpdateAvailable); - else - event->SetInt((int)Result::NoUpdateAvailable); - - wxQueueEvent(this, event); - } - else if (nextWorkOrder == WorkerOrder::UpdateVersion) - { - // download update - const std::string url = m_downloadUrl; - bool r = false; -#if BOOST_OS_WINDOWS - r = WorkerThread_Windows(); -#elif BOOST_OS_LINUX - r = WorkerThread_AppImage(); -#elif BOOST_OS_BSD - // dummy placeholder on BSD for now -#elif BOOST_OS_MACOS - r = WorkerThread_MacBundle(); -#endif - // update done - if (r) - { - // report success - auto* event = new wxCommandEvent(wxEVT_PROGRESS); - event->SetInt(m_gaugeMaxValue); - wxQueueEvent(this, event); - - auto* result_event = new wxCommandEvent(wxEVT_RESULT); - result_event->SetInt((int)Result::Success); - wxQueueEvent(this, result_event); - } - else if (m_order != WorkerOrder::Exit) - { - SubmitWorkerResult(Result::UpdateDownloadError); - } - } - } -} - -void CemuUpdateWindow::OnClose(wxCloseEvent& event) -{ - event.Skip(); - if (m_restartRequired) - { - wxGetApp().RequestRestart(m_restartFile); - } -} - - -void CemuUpdateWindow::OnResult(wxCommandEvent& event) -{ - switch ((Result)event.GetInt()) - { - case Result::NoUpdateAvailable: - m_cancelButton->SetLabel(_("Exit")); - m_text->SetLabel(_("No update available!")); - m_gauge->SetValue(100); - break; - case Result::UpdateAvailable: - { - if (!m_changelogUrl.empty()) - { - m_changelog->SetURL(m_changelogUrl); - m_changelog->Show(); - } - else - m_changelog->Hide(); - - m_updateButton->Show(); - - m_text->SetLabel(_("Update available!")); - m_cancelButton->SetLabel(_("Exit")); - break; - } - case Result::UpdateDownloaded: - m_text->SetLabel(_("Extracting update...")); - m_gauge->SetValue(0); - break; - case Result::UpdateDownloadError: - m_updateButton->Enable(); - m_cancelButton->Enable(); - m_text->SetLabel(_("Couldn't download the update!")); - break; - case Result::ExtractSuccess: - m_text->SetLabel(_("Applying update...")); - m_gauge->SetValue(0); - m_cancelButton->Disable(); - break; - case Result::ExtractError: - m_updateButton->Enable(); - m_cancelButton->Enable(); - m_text->SetLabel(_("Extracting failed!")); - break; - case Result::Success: - m_cancelButton->Enable(); - m_updateButton->Hide(); - - m_text->SetLabel(_("Success")); - m_cancelButton->SetLabel(_("Restart")); - m_restartRequired = true; - break; - default:; - } -} - -void CemuUpdateWindow::OnGaugeUpdate(wxCommandEvent& event) -{ - const int total_size = m_gaugeMaxValue > 0 ? m_gaugeMaxValue : 10000000; - m_gauge->SetValue((event.GetInt() * 100) / total_size); -} - -void CemuUpdateWindow::OnUpdateButton(const wxCommandEvent& event) -{ - std::unique_lock lock(m_mutex); - m_order = WorkerOrder::UpdateVersion; - - m_condition.notify_all(); - - m_updateButton->Disable(); - - m_text->SetLabel(_("Downloading update...")); -} - -void CemuUpdateWindow::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} diff --git a/src/gui/wxgui/CemuUpdateWindow.h b/src/gui/wxgui/CemuUpdateWindow.h deleted file mode 100644 index 4ab8c269..00000000 --- a/src/gui/wxgui/CemuUpdateWindow.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -class CemuUpdateWindow : public wxDialog -{ -public: - CemuUpdateWindow(wxWindow* parent); - ~CemuUpdateWindow(); - - static std::future IsUpdateAvailableAsync(); - -private: - wxStaticText* m_text; - wxGauge* m_gauge; - wxButton* m_cancelButton, *m_updateButton; - wxHyperlinkCtrl* m_changelog; - - void OnUpdateButton(const wxCommandEvent& event); - void OnCancelButton(const wxCommandEvent& event); - void OnClose(wxCloseEvent& event); - void OnResult(wxCommandEvent& event); - void OnGaugeUpdate(wxCommandEvent& event); - - static size_t WriteStringCallback(char* ptr, size_t size, size_t nmemb, void* userdata); - static bool QueryUpdateInfo(std::string& downloadUrlOut, std::string& changelogUrlOut, std::array& fileSha256Out); - static bool CheckVersion(); - - static int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); - bool DownloadCemuUpdateFile(const std::string& url, const fs::path& filename); - bool ExtractZipUpdate(const fs::path& zipname, const fs::path& targetpath); - - enum class WorkerOrder - { - Idle, - InProgress, // work command received - Exit, - CheckVersion, - UpdateVersion, - }; - enum class Result - { - NoUpdateAvailable, - UpdateAvailable, - UpdateDownloaded, - UpdateDownloadError, - ExtractSuccess, - ExtractError, - Success, - Error - }; - std::mutex m_mutex; - std::condition_variable m_condition; - std::atomic m_order = WorkerOrder::CheckVersion; - void WorkerThread(); - - void SubmitWorkerResult(Result newWorkerState); - -#if BOOST_OS_WINDOWS - bool WorkerThread_Windows(); -#endif -#if BOOST_OS_LINUX - bool WorkerThread_AppImage(); -#endif -#if BOOST_OS_MACOS - bool WorkerThread_MacBundle(); -#endif - - - std::string m_downloadUrl, m_changelogUrl; - std::array m_fileSHA256; - int m_gaugeMaxValue = 0; - - std::thread m_thread; - fs::path m_restartFile; - bool m_restartRequired = false; -}; diff --git a/src/gui/wxgui/ChecksumTool.cpp b/src/gui/wxgui/ChecksumTool.cpp deleted file mode 100644 index 8f9698a6..00000000 --- a/src/gui/wxgui/ChecksumTool.cpp +++ /dev/null @@ -1,792 +0,0 @@ -#include "wxgui/ChecksumTool.h" - -#include "Cafe/TitleList/GameInfo.h" -#include "wxgui/helpers/wxCustomEvents.h" -#include "util/helpers/helpers.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/wxHelper.h" -#include "Cafe/Filesystem/WUD/wud.h" - -#include -#include - -#include /* EVP_Digest */ -#include /* SHA256_DIGEST_LENGTH */ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "config/ActiveSettings.h" -#include "Cafe/TitleList/TitleList.h" - -const char kSchema[] = R"( -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "title_id": { - "type": "string" - }, - "region": { - "type": "integer" - }, - "version": { - "type": "integer" - }, - "wud_hash": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "hash": { - "type": "string" - } - }, - "required": [ - "file", - "hash" - ] - } - } - }, - "required": [ - "title_id", - "region", - "version", - "files" - ] -})"; - - -ChecksumTool::ChecksumTool(wxWindow* parent, wxTitleManagerList::TitleEntry& entry) - : wxDialog(parent, wxID_ANY, - formatWxString(_("Title checksum of {:08x}-{:08x}"), (uint32) (entry.title_id >> 32), (uint32) (entry.title_id & 0xFFFFFFFF)), - wxDefaultPosition, wxDefaultSize, wxCAPTION | wxFRAME_TOOL_WINDOW | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), m_entry(entry) -{ - - m_info = CafeTitleList::GetTitleInfoByUID(m_entry.location_uid); - if (!m_info.IsValid()) - throw std::runtime_error("Invalid title"); - - // only request online update once - static bool s_once = false; - if (!s_once) - { - s_once = true; - m_online_ready = std::async(std::launch::async, &ChecksumTool::LoadOnlineData, this); - } - else - m_enable_verify_button = 1; - - auto* sizer = new wxBoxSizer(wxVERTICAL); - { - auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Verifying integrity of game files...")), wxVERTICAL); - auto* box = box_sizer->GetStaticBox(); - - m_progress = new wxGauge(box, wxID_ANY, 100, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL | wxGA_SMOOTH); - m_progress->SetMinSize({ 400, -1 }); - m_progress->SetValue(0); - box_sizer->Add(m_progress, 0, wxALL | wxEXPAND, 5); - - m_status = new wxStaticText(box, wxID_ANY, wxEmptyString); - m_status->Wrap(-1); - box_sizer->Add(m_status, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 5); - - sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(this, wxID_ANY, _("Control")), wxHORIZONTAL); - auto* box = box_sizer->GetStaticBox(); - - m_verify_online = new wxButton(box, wxID_ANY, _("Verify online")); - m_verify_online->SetToolTip(_("Verifies the checksum online")); - m_verify_online->Disable(); - m_verify_online->Bind(wxEVT_BUTTON, &ChecksumTool::OnVerifyOnline, this); - m_verify_online->Bind(wxEVT_ENABLE, [this](wxCommandEvent&) - { - ++m_enable_verify_button; - if (m_enable_verify_button >= 2) - { - // only enable if we have a file for it - const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id); - const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion()); - - const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums/{}", default_file); - if (exists(checksum_path)) - m_verify_online->Enable(); - } - }); - box_sizer->Add(m_verify_online, 0, wxALL | wxEXPAND, 5); - - m_verify_local = new wxButton(box, wxID_ANY, _("Verify with local file")); - m_verify_local->SetToolTip(_("Verifies the checksum with a local JSON file you can select")); - m_verify_local->Disable(); - m_verify_local->Bind(wxEVT_BUTTON, &ChecksumTool::OnVerifyLocal, this); - box_sizer->Add(m_verify_local, 0, wxALL | wxEXPAND, 5); - - m_export_button = new wxButton(box, wxID_ANY, _("Export")); - m_export_button->SetToolTip(_("Export the title checksum data to a local JSON file")); - m_export_button->Disable(); - m_export_button->Bind(wxEVT_BUTTON, &ChecksumTool::OnExportChecksums, this); - box_sizer->Add(m_export_button, 0, wxALL | wxEXPAND, 5); - - sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - this->Bind(wxEVT_SET_GAUGE_VALUE, &ChecksumTool::OnSetGaugevalue, this); - - m_worker = std::thread(&ChecksumTool::DoWork, this); - - this->SetSizerAndFit(sizer); - this->Centre(wxBOTH); -} - -ChecksumTool::~ChecksumTool() -{ - m_running = false; - if (m_worker.joinable()) - m_worker.join(); -} - -std::size_t WriteCallback(const char* in, std::size_t size, std::size_t num, std::string* out) -{ - const std::size_t totalBytes(size * num); - out->append(in, totalBytes); - return totalBytes; -} - -void ChecksumTool::LoadOnlineData() const -{ - try - { - bool updated_required = true; - - std::string latest_commit; - - const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums"); - if (exists(checksum_path)) - { - std::string current_commit; - // check for current version - std::ifstream file(checksum_path / "commit.txt"); - if (file.is_open()) - { - std::getline(file, current_commit); - file.close(); - } - - // check latest version - /* - https://api.github.com/repos/cemu-project/title-checksums/branches/master - https://api.github.com/repos/cemu-project/title-checksums/commits?per_page=1 - */ - std::string data; - auto* curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/cemu-project/title-checksums/commits/master"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - - curl_easy_perform(curl); - - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_cleanup(curl); - if (http_code == 200 && !data.empty()) - { - rapidjson::Document doc; - doc.Parse(data.c_str(), data.size()); - if (!doc.HasParseError() && doc.HasMember("sha")) - { - latest_commit = doc["sha"].GetString(); - if (boost::iequals(current_commit, latest_commit)) - { - updated_required = false; - } - } - } - } - else - { - // create directory since not available yet - fs::create_directories(checksum_path); - } - - if (updated_required) - { - std::string data; - auto* curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, "https://github.com/cemu-project/title-checksums/archive/master.zip"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - - curl_easy_perform(curl); - - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - curl_easy_cleanup(curl); - if (http_code == 200 && !data.empty()) - { - // init zip source - zip_error_t error; - zip_error_init(&error); - zip_source_t* src; - if ((src = zip_source_buffer_create(data.data(), data.size(), 0, &error)) == nullptr) - { - zip_error_fini(&error); - return; - } - - auto* za = zip_open_from_source(src, ZIP_RDONLY, &error); - if (!za) - { - zip_source_free(src); - zip_error_fini(&error); - wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE)); - return; - } - - const auto numEntries = zip_get_num_entries(za, 0); - for (sint64 i = 0; i < numEntries; i++) - { - zip_stat_t sb = { 0 }; - if (zip_stat_index(za, i, 0, &sb) != 0) - continue; - - if (std::strstr(sb.name, "../") != nullptr || - std::strstr(sb.name, "..\\") != nullptr) - continue; // bad path - - if (boost::equals(sb.name, "title-checksums-master/")) - continue; - - // title-checksums-master/ - const auto path = checksum_path / &sb.name[sizeof("title-checksums-master")]; - - size_t sbNameLen = strlen(sb.name); - if (sbNameLen == 0) - continue; - - if (sb.name[sbNameLen - 1] == '/') - { - std::error_code ec; - fs::create_directories(path, ec); - continue; - } - - if (sb.size == 0) - continue; - - if (sb.size > (1024 * 1024 * 128)) - continue; // skip unusually huge files - - zip_file_t* zipFile = zip_fopen_index(za, i, 0); - if (zipFile == nullptr) - continue; - - std::vector buffer(sb.size); - if (zip_fread(zipFile, buffer.data(), sb.size) == sb.size) - { - std::ofstream file(path); - if (file.is_open()) - { - file.write(buffer.data(), sb.size); - file.flush(); - file.close(); - } - } - - zip_fclose(zipFile); - } - - zip_discard(za); - zip_error_fini(&error); - - std::ofstream file(checksum_path / "commit.txt"); - if (file.is_open()) - { - file << latest_commit; - file.close(); - } - } - } - } - catch(const std::exception& ex) - { - cemuLog_log(LogType::Force, "error on updating json checksum data: {}", ex.what()); - } - - wxQueueEvent(m_verify_online, new wxCommandEvent(wxEVT_ENABLE)); -} - -void ChecksumTool::OnSetGaugevalue(wxSetGaugeValue& event) -{ - event.GetGauge()->SetValue(event.GetValue()); - event.GetTextCtrl()->SetLabelText(event.GetText()); - - // no error - if(event.GetInt() == 0 && event.GetValue() == 100) - { - m_export_button->Enable(); - m_verify_local->Enable(); - wxPostEvent(m_verify_online, wxCommandEvent(wxEVT_ENABLE)); - } -} - -void ChecksumTool::OnExportChecksums(wxCommandEvent& event) -{ - // TODO: merge if json already exists - wxDirDialog dialog(this, _("Export checksum entry"), "", wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK || dialog.GetPath().IsEmpty()) - return; - - rapidjson::Document doc; - doc.SetObject(); - auto& a = doc.GetAllocator(); - /* - title_id - region - version - wud_hash - files: - [ - {file, hash} - ] - */ - - auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id); - doc.AddMember("title_id", rapidjson::StringRef(title_id_str.c_str(), title_id_str.size()), a); - doc.AddMember("region", (int)m_info.GetMetaRegion(), a); - doc.AddMember("version", m_info.GetAppTitleVersion(), a); - if (!m_json_entry.wud_hash.empty()) - doc.AddMember("wud_hash", rapidjson::StringRef(m_json_entry.wud_hash.c_str(), m_json_entry.wud_hash.size()), a); - - rapidjson::Value entry_array(rapidjson::kArrayType); - - rapidjson::Value file_array(rapidjson::kArrayType); - for(const auto& file : m_json_entry.file_hashes) - { - rapidjson::Value file_entry; - file_entry.SetObject(); - - file_entry.AddMember("file", rapidjson::StringRef(file.first.c_str(), file.first.size()), a); - file_entry.AddMember("hash", rapidjson::StringRef(file.second.c_str(), file.second.size()), a); - - file_array.PushBack(file_entry, a); - } - - doc.AddMember("files", file_array, a); - - std::filesystem::path target_file{ dialog.GetPath().c_str().AsInternal() }; - target_file /= fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion()); - - std::ofstream file(target_file); - if(file.is_open()) - { - rapidjson::OStreamWrapper osw(file); - rapidjson::PrettyWriter writer(osw); - //rapidjson::GenericSchemaValidator > validator(schema, writer); - doc.Accept(writer); - wxMessageBox(_("Export successful"), wxMessageBoxCaptionStr, wxOK | wxCENTRE, this); - } - else - { - wxMessageBox(formatWxString(_("Can't write to file: {}"), target_file.string()), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } -} - -void ChecksumTool::VerifyJsonEntry(const rapidjson::Document& doc) -{ - rapidjson::Document sdoc; - sdoc.Parse(kSchema, std::size(kSchema)); - wxASSERT(!sdoc.HasParseError()); - rapidjson::SchemaDocument schema(sdoc); - rapidjson::SchemaValidator validator(schema); - if (!doc.Accept(validator)) - { - //// validation error: - //rapidjson::StringBuffer sb; - //validator.GetInvalidSchemaPointer().StringifyUriFragment(sb); - //printf("Invalid schema: %s\n", sb.GetString()); - //printf("Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword()); - //sb.Clear(); - //validator.GetInvalidDocumentPointer().StringifyUriFragment(sb); - //printf("Invalid document: %s\n", sb.GetString()); - ///* - //Invalid schema: # - //Invalid keyword: required - //Invalid document: # - // */ - - wxMessageBox(_("JSON file doesn't satisfy needed schema"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - try - { - JsonEntry test_entry{}; - test_entry.title_id = ConvertString(doc["title_id"].GetString(), 16); - test_entry.region = (CafeConsoleRegion)doc["region"].GetInt(); - test_entry.version = doc["version"].GetInt(); - if (doc.HasMember("wud_hash")) - test_entry.wud_hash = doc["wud_hash"].GetString(); - - for (const auto& v : doc["files"].GetArray()) - { - std::filesystem::path genericFilePath(v["file"].GetString(), std::filesystem::path::generic_format); // convert path to generic form (forward slashes) - test_entry.file_hashes[genericFilePath.generic_string()] = v["hash"].GetString(); - } - - if (m_json_entry.title_id != test_entry.title_id) - { - wxMessageBox(formatWxString(_("The file you are comparing with is for a different title.")), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - if (m_json_entry.version != test_entry.version) - { - wxMessageBox(formatWxString(_("Wrong version: {}"), test_entry.version), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - if (m_json_entry.region != test_entry.region) - { - wxMessageBox(formatWxString(_("Wrong region: {}"), test_entry.region), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - if (!m_json_entry.wud_hash.empty()) - { - if (test_entry.wud_hash.empty()) - { - wxMessageBox(_("The verification data doesn't include a WUD hash!"), _("Error"), wxOK | wxCENTRE | wxICON_WARNING, this); - return; - } - if(!boost::iequals(test_entry.wud_hash, m_json_entry.wud_hash)) - { - wxMessageBox(formatWxString(_("Your game image is invalid!\n\nYour hash:\n{}\n\nExpected hash:\n{}"), m_json_entry.wud_hash, test_entry.wud_hash), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - } - else - { - std::map> invalid_hashes; - std::vector missing_files; - const auto writeMismatchInfoToLog = [this, &missing_files, &invalid_hashes]() - { - wxFileDialog dialog(this, _("Select a file to export the errors"), wxEmptyString, wxEmptyString, "Error list (*.txt)|*.txt", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (dialog.ShowModal() != wxID_OK || dialog.GetPath().IsEmpty()) - return; - - const std::string path = dialog.GetPath().utf8_string(); - std::ofstream file(path); - if (file.is_open()) - { - if (!missing_files.empty()) - { - file << "The following files are missing:\n"; - for (const auto& f : missing_files) - file << "\t" << f << "\n"; - - file << "\n"; - } - - if (!invalid_hashes.empty()) - { - file << "The following files have an invalid hash (name | current hash | expected hash):\n"; - for (const auto& f : invalid_hashes) - file << "\t" << f.first << " | " << f.second.first << " | " << f.second.second << "\n"; - } - file.flush(); - file.close(); - - wxLaunchDefaultBrowser(formatWxString("file:{}", path)); - } - else - wxMessageBox(_("Can't open file to write!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - }; - - for (const auto& f : test_entry.file_hashes) - { - const auto it = m_json_entry.file_hashes.find(f.first); - if (it == m_json_entry.file_hashes.cend()) - { - missing_files.emplace_back(f.first); - } - else if (!boost::iequals(f.second, it->second)) - { - invalid_hashes[f.first] = std::make_pair(it->second, f.second); - } - } - - // files are missing but rest is okay - if ((!missing_files.empty() || !invalid_hashes.empty()) && (missing_files.size() + invalid_hashes.size()) < 30) - { - // the list of missing + invalid hashes is short enough that we can print it to the message box - std::stringstream str; - if (missing_files.size() > 0) - { - str << _("The following files are missing:").ToUTF8().data() << "\n"; - for (const auto& v : missing_files) - str << v << "\n"; - if(invalid_hashes.size() > 0) - str << "\n"; - } - if (invalid_hashes.size() > 0) - { - str << _("The following files are damaged:").ToUTF8().data() << "\n"; - for (const auto& v : invalid_hashes) - str << v.first << "\n"; - } - - wxMessageBox(str.str(), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - else if (missing_files.empty() && !invalid_hashes.empty()) - { - const int result = wxMessageBox(formatWxString( - _("{} files have an invalid hash!\nDo you want to export a list of them to a file?"), - invalid_hashes.size()), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR, this); - if (result == wxYES) - { - writeMismatchInfoToLog(); - } - return; - } - else if (!missing_files.empty() && !invalid_hashes.empty()) - { - const int result = wxMessageBox(formatWxString( - _("Multiple issues with your game files have been found!\nDo you want to export them to a file?"), - invalid_hashes.size()), _("Error"), wxYES_NO | wxCENTRE | wxICON_ERROR, this); - if (result == wxYES) - { - writeMismatchInfoToLog(); - } - return; - } - } - wxMessageBox(_("Your game files are valid"), _("Success"), wxOK | wxCENTRE, this); - } - catch (const std::exception& ex) - { - wxMessageBox(formatWxString(_("JSON parse error: {}"), ex.what()), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } - -} - -void ChecksumTool::OnVerifyOnline(wxCommandEvent& event) -{ - const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id); - const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion()); - - const auto checksum_path = ActiveSettings::GetUserDataPath("resources/checksums/{}", default_file); - if(!exists(checksum_path)) - return; - - std::ifstream file(checksum_path); - if (!file.is_open()) - { - wxMessageBox(_("Can't open file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - rapidjson::IStreamWrapper str(file); - rapidjson::Document d; - d.ParseStream(str); - if (d.HasParseError()) - { - wxMessageBox(_("Can't parse JSON file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - VerifyJsonEntry(d); -} - -void ChecksumTool::OnVerifyLocal(wxCommandEvent& event) -{ - const auto title_id_str = fmt::format("{:016x}", m_json_entry.title_id); - const auto default_file = fmt::format("{}_v{}.json", title_id_str, m_info.GetAppTitleVersion()); - wxFileDialog file_dialog(this, _("Open checksum entry"), "", default_file.c_str(),"JSON files (*.json)|*.json", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (file_dialog.ShowModal() != wxID_OK || file_dialog.GetPath().IsEmpty()) - return; - - std::filesystem::path filename{ file_dialog.GetPath().c_str().AsInternal() }; - std::ifstream file(filename); - if(!file.is_open()) - { - wxMessageBox(_("Can't open file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - rapidjson::IStreamWrapper str(file); - rapidjson::Document d; - d.ParseStream(str); - if (d.HasParseError()) - { - wxMessageBox(_("Can't parse JSON file!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - VerifyJsonEntry(d); -} - -static void _fscGetAllFiles(std::set& allFilesOut, const std::string& fscBasePath, const std::string& relativePath) -{ - sint32 fscStatus; - FSCVirtualFile* fsc = fsc_openDirIterator((fscBasePath + relativePath).c_str(), &fscStatus); - cemu_assert(fsc); - FSCDirEntry dirEntry; - while (fsc_nextDir(fsc, &dirEntry)) - { - if (dirEntry.isDirectory) - { - _fscGetAllFiles(allFilesOut, fscBasePath, std::string(relativePath).append(dirEntry.GetPath()).append("/")); - } - else - { - allFilesOut.emplace(std::string(relativePath).append(dirEntry.GetPath())); - } - } - delete fsc; -} - -void ChecksumTool::DoWork() -{ - m_json_entry.title_id = m_info.GetAppTitleId(); - m_json_entry.region = m_info.GetMetaRegion(); - m_json_entry.version = m_info.GetAppTitleVersion(); - - static_assert(SHA256_DIGEST_LENGTH == 32); - - std::array checksum{}; - - switch (m_info.GetFormat()) - { - case TitleInfo::TitleDataFormat::WUD: - { - const auto path = m_entry.path.string(); - wxQueueEvent(this, new wxSetGaugeValue(1, m_progress, m_status, formatWxString(_("Reading game image: {}"), path))); - - wud_t* wud = wud_open(m_info.GetPath()); - if (!wud) - throw std::runtime_error("can't open game image"); - - const auto wud_size = wud_getWUDSize(wud); - std::vector buffer(1024 * 1024 * 8); - - EVP_MD_CTX *sha256 = EVP_MD_CTX_new(); - EVP_DigestInit(sha256, EVP_sha256()); - - uint32 read = 0; - size_t offset = 0; - auto size = wud_size; - do - { - if (!m_running.load(std::memory_order_relaxed)) - { - wud_close(wud); - return; - } - - read = wud_readData(wud, buffer.data(), std::min(buffer.size(), (size_t)wud_size - offset), offset); - offset += read; - size -= read; - - EVP_DigestUpdate(sha256, buffer.data(), read); - - wxQueueEvent(this, new wxSetGaugeValue((int)((offset * 90) / wud_size), m_progress, m_status, formatWxString(_("Reading game image: {0}/{1} kB"), offset / 1024, wud_size / 1024))); - } while (read != 0 && size > 0); - wud_close(wud); - - wxQueueEvent(this, new wxSetGaugeValue(90, m_progress, m_status, formatWxString(_("Generating checksum of game image: {}"), path))); - - if (!m_running.load(std::memory_order_relaxed)) - return; - - EVP_DigestFinal_ex(sha256, checksum.data(), NULL); - EVP_MD_CTX_free(sha256); - - std::stringstream str; - for (const auto& b : checksum) - { - str << fmt::format("{:02X}", b); - } - - m_json_entry.wud_hash = str.str(); - - wxQueueEvent(this, new wxSetGaugeValue(100, m_progress, m_status, formatWxString(_("Generated checksum of game image: {}"), path))); - break; - } - default: - // we hash the individual files for all formats except WUD/WUX - std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath(); - m_info.Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE); - wxQueueEvent(this, new wxSetGaugeValue(1, m_progress, m_status, _("Grabbing game files"))); - - // get list of all files - std::set files; - _fscGetAllFiles(files, temporaryMountPath, ""); - - const size_t file_count = files.size(); - size_t counter = 0; - for (const auto& filename : files) - { - auto fileData = fsc_extractFile((temporaryMountPath + "/" + filename).c_str()); - if (!fileData) - { - cemuLog_log(LogType::Force, "Failed to open {}", filename); - continue; - } - - SHA256(fileData->data(), fileData->size(), checksum.data()); - - std::stringstream str; - for (const auto& b : checksum) - { - str << fmt::format("{:02X}", b); - } - - // store relative path and hash - m_json_entry.file_hashes[filename] = str.str(); - - ++counter; - wxQueueEvent(this, new wxSetGaugeValue((int)((counter * 100) / file_count), m_progress, m_status, formatWxString(_("Hashing game file: {}/{}"), counter, file_count))); - - if (!m_running.load(std::memory_order_relaxed)) - { - m_info.Unmount(temporaryMountPath.c_str()); - return; - } - } - m_info.Unmount(temporaryMountPath.c_str()); - - wxQueueEvent(this, new wxSetGaugeValue(100, m_progress, m_status, formatWxString(_("Generated checksum of {} game files"), file_count))); - break; - } -} diff --git a/src/gui/wxgui/ChecksumTool.h b/src/gui/wxgui/ChecksumTool.h deleted file mode 100644 index ae0ea760..00000000 --- a/src/gui/wxgui/ChecksumTool.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include -#include "wxgui/components/wxTitleManagerList.h" -#include "Cafe/TitleList/TitleInfo.h" -#include - -class wxSetGaugeValue; - -class ChecksumTool : public wxDialog -{ -public: - ChecksumTool(wxWindow* parent, wxTitleManagerList::TitleEntry& entry); - ~ChecksumTool(); - -private: - std::future m_online_ready; - void LoadOnlineData() const; - void VerifyJsonEntry(const rapidjson::Document& doc); - - void OnSetGaugevalue(wxSetGaugeValue& event); - void OnExportChecksums(wxCommandEvent& event); - void OnVerifyOnline(wxCommandEvent& event); - void OnVerifyLocal(wxCommandEvent& event); - - void DoWork(); - std::atomic_bool m_running = true; - std::thread m_worker; - - class wxGauge* m_progress; - class wxStaticText* m_status; - class wxButton *m_verify_online, *m_verify_local, *m_export_button; - int m_enable_verify_button = 0; - - TitleInfo m_info; - wxTitleManagerList::TitleEntry m_entry; - wxColour m_default_color; - - struct JsonEntry - { - uint64 title_id; - uint32 version; - CafeConsoleRegion region; - - std::string wud_hash; - std::map file_hashes; - }; - JsonEntry m_json_entry; - inline static std::mutex s_mutex{}; - inline static std::vector s_entries{}; - -}; diff --git a/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp b/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp deleted file mode 100644 index f261061d..00000000 --- a/src/gui/wxgui/DownloadCustomGraphicPackWindow.cpp +++ /dev/null @@ -1,418 +0,0 @@ -#include "wxgui/DownloadCustomGraphicPackWindow.h" -#include "Cafe/CafeSystem.h" -#include "config/ActiveSettings.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -static size_t curlDownloadFile_writeData(void *ptr, size_t size, size_t nmemb, DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState) -{ - const size_t writeSize = size * nmemb; - const size_t currentSize = downloadState->fileData.size(); - const size_t newSize = currentSize + writeSize; - auto* bytePtr = static_cast(ptr); - downloadState->fileData.insert(downloadState->fileData.end(), bytePtr, bytePtr + writeSize); - return writeSize; -} - -static int progress_callback(DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -{ - if (downloadState->isCanceled) - return 1; - - if (dltotal > 1.0) - downloadState->progress = dlnow / dltotal; - else - downloadState->progress = 0.0; - return 0; -} - -static bool curlDownloadFile(const char *url, DownloadCustomGraphicPackWindow::curlDownloadFileState_t* downloadState) -{ - CURL* curl = curl_easy_init(); - if (curl == nullptr) - return false; - - downloadState->progress = 0.0; - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlDownloadFile_writeData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, downloadState); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); - curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, downloadState); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); - curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 30L); - curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 15L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - downloadState->fileData.resize(0); - const CURLcode res = curl_easy_perform(curl); - curl_easy_cleanup(curl); - return res == CURLE_OK; -} - -DownloadCustomGraphicPackWindow::DownloadCustomGraphicPackWindow(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Download Graphic Pack from URL"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), - m_stage(StageDone), m_currentStage(StageDone) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_urlField = new wxTextCtrl(this, wxID_ANY, wxEmptyString); - m_urlField->SetHint(_("Enter download URL...")); - sizer->Add(m_urlField, 0, wxALL | wxEXPAND, 5); - - m_processBar = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); - m_processBar->SetValue(0); - m_processBar->SetRange(100); - sizer->Add(m_processBar, 0, wxALL | wxEXPAND, 5); - - auto* buttonSizer = new wxBoxSizer(wxHORIZONTAL); - - m_statusText = new wxStaticText(this, wxID_ANY, _("Ready...")); - buttonSizer->Add(m_statusText, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - buttonSizer->AddStretchSpacer(1); - - auto* m_closeButton = new wxButton(this, wxID_ANY, _("Close")); - m_closeButton->Bind(wxEVT_BUTTON, &DownloadCustomGraphicPackWindow::OnCancelButton, this); - buttonSizer->Add(m_closeButton, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_downloadButton = new wxButton(this, wxID_ANY, _("Download")); - m_downloadButton->Bind(wxEVT_BUTTON, &DownloadCustomGraphicPackWindow::OnDownloadButton, this); - buttonSizer->Add(m_downloadButton, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - sizer->Add(buttonSizer, 0, wxEXPAND | wxALL, 5); - - this->SetSizer(sizer); - this->Centre(wxBOTH); - - wxWindowBase::Layout(); - wxWindowBase::Fit(); - - m_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &DownloadCustomGraphicPackWindow::OnUpdate, this); - this->Bind(wxEVT_CLOSE_WINDOW, &DownloadCustomGraphicPackWindow::OnClose, this); - m_timer->Start(100); - - m_downloadState = std::make_unique(); -} - -DownloadCustomGraphicPackWindow::~DownloadCustomGraphicPackWindow() -{ - if (m_downloadState) - m_downloadState->isCanceled = true; - - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); -} - -int DownloadCustomGraphicPackWindow::ShowModal() -{ - wxDialog::ShowModal(); - return wxID_OK; -} - -void DownloadCustomGraphicPackWindow::OnClose(wxCloseEvent& event) -{ - if (m_downloadState) - { - m_downloadState->isCanceled = true; - } - - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); - - event.Skip(); -} - -void DownloadCustomGraphicPackWindow::OnUpdate(const wxTimerEvent& event) -{ - if (m_currentStage >= StageDone) - { - m_downloadButton->Enable(); - m_urlField->Enable(); - } - else - { - m_downloadButton->Disable(); - m_urlField->Disable(); - } - - if (m_currentStage == StageDownloading) - { - const sint32 processedSize = (sint32)(m_downloadState->progress * 100.0f); - if (m_processBar->GetValue() != processedSize) - m_processBar->SetValue(processedSize); - } - else if (m_currentStage == StageExtracting) - { - const sint32 processedSize = (sint32)(m_extractionProgress * 100.0f); - if (m_processBar->GetValue() != processedSize) - m_processBar->SetValue(processedSize); - } - - if (m_currentStage != m_stage) - { - wxString status = "..."; - const wxColour* colour = wxWHITE; - - switch (m_stage) - { - case StageDownloading: - status = "Downloading..."; - colour = wxWHITE; - break; - case StageVerifying: - status = "Verifying..."; - colour = wxWHITE; - break; - case StageExtracting: - status = "Extracting..."; - colour = wxWHITE; - break; - case StageDone: - status = "Done!"; - colour = wxGREEN; - m_processBar->SetValue(100.0); - break; - case StageErrConnectFailed: - if (m_urlField->GetValue().empty()) - { - status = "Please enter a valid URL."; - colour = wxWHITE; - } - else - { - status = "ERROR: Connection failed."; - colour = wxRED; - } - m_processBar->SetValue(0.0); - break; - case StageErrInvalidPack: - status = "ERROR: Invalid pack."; - colour = wxRED; - m_processBar->SetValue(0.0); - break; - case StageErrSourceFailed: - status = "ERROR: Failed to create ZIP source."; - colour = wxRED; - m_processBar->SetValue(0.0); - break; - case StageErrOpenFailed: - status = "ERROR: Failed to open downloaded ZIP."; - colour = wxRED; - m_processBar->SetValue(0.0); - break; - case StageErrConflict: - status = "ERROR: File conflict. Pack already installed?"; - colour = wxRED; - m_processBar->SetValue(0.0); - break; - } - - m_currentStage = m_stage; - m_statusText->SetLabel(status); - m_statusText->SetForegroundColour(*colour); - } -} - -void DownloadCustomGraphicPackWindow::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} - -void DownloadCustomGraphicPackWindow::OnDownloadButton(const wxCommandEvent& event) -{ - m_downloadButton->Disable(); - m_urlField->Disable(); - - if (m_thread.joinable()) - m_thread.join(); - - m_thread = std::thread(&DownloadCustomGraphicPackWindow::UpdateThread, this); -} - -void DownloadCustomGraphicPackWindow::UpdateThread() -{ - m_stage = StageDownloading; - if (curlDownloadFile(m_urlField->GetValue(), m_downloadState.get()) == false) - { - m_stage = StageErrConnectFailed; - return; - } - - m_extractionProgress = 0.0; - m_stage = StageExtracting; - - zip_source_t *src; - zip_t *za; - zip_error_t error; - - // init zip source - zip_error_init(&error); - if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 0, &error)) == NULL) - { - zip_error_fini(&error); - m_stage = StageErrSourceFailed; - return; - } - - // open zip from source - if ((za = zip_open_from_source(src, 0, &error)) == NULL) - { - zip_source_free(src); - zip_error_fini(&error); - m_stage = StageErrOpenFailed; - return; - } - - auto path = ActiveSettings::GetUserDataPath("graphicPacks/customGraphicPacks"); - fs::create_directories(path); - - // check if zip root directly contains a rules.txt - zip_stat_t zs; - zip_stat_init(&zs); - bool hasRootRulesTxt = (zip_stat(za, "rules.txt", 0, &zs) == 0); - - fs::path extractionRoot = path; - - if (hasRootRulesTxt) - { - // make a folder and extract into that - wxString urlStr = m_urlField->GetValue(); - std::string folderName = "NewCustomPack"; // fallback - - int lastSlash = urlStr.Find('/', true); - wxString fileNameBase = (lastSlash != wxNOT_FOUND) ? urlStr.Mid(lastSlash + 1) : urlStr; - - int lastDot = fileNameBase.Find('.', true); - if (lastDot != wxNOT_FOUND) - { - fileNameBase = fileNameBase.Left(lastDot); - } - - fileNameBase.Trim(true).Trim(false); - if (!fileNameBase.IsEmpty()) - { - folderName = fileNameBase.ToStdString(); - } - - extractionRoot = path / folderName; - fs::create_directories(extractionRoot); - } - else - { - // not a gfxpack - m_stage = StageErrInvalidPack; - zip_close(za); - return; - } - - // extract - bool err = false; - zip_int64_t numEntries = zip_get_num_entries(za, 0); - for (zip_int64_t i = 0; i < numEntries; i++) - { - m_extractionProgress = (double)i / (double)numEntries; - - if (m_downloadState->isCanceled) - { - err = true; - } - - if (err) - { - break; - } - - zip_stat_t sb = { 0 }; - zip_stat_init(&sb); - if (zip_stat_index(za, i, 0, &sb) != 0) - { - assert_dbg(); - continue; - } - - std::string fileName = sb.name; - if (std::strstr(sb.name, "../") != nullptr || - std::strstr(sb.name, "..\\") != nullptr || - (!fileName.empty() && (fileName[0] == '/' || fileName[0] == '\\'))) - { - // bad path, CommunityGraphicPacks silently continues - // but this is a low-trust environment, so we halt - m_stage = StageErrInvalidPack; - err = true; - break; - } - - fs::path targetDest = extractionRoot / fileName; - - if (!fileName.empty() && fileName.back() == '/') - { - fs::create_directories(targetDest); - continue; - } - - if (fs::exists(targetDest) && fs::is_regular_file(targetDest)) - { - m_stage = StageErrConflict; - err = true; - break; - } - - fs::create_directories(targetDest.parent_path()); - - zip_file_t* zf = zip_fopen_index(za, i, 0); - if (!zf) - { - continue; - } - - if (sb.size > 1000000000) // 1GB suspicious limit per file - { - m_stage = StageErrInvalidPack; - err = true; - break; - } - - std::vector fileBuffer(sb.size); - - if (zip_fread(zf, fileBuffer.data(), sb.size) == sb.size) - { - std::unique_ptr fs(FileStream::createFile2(targetDest)); - if (fs) - { - fs->writeData(fileBuffer.data(), sb.size); - } - } - else - { - err = true; - m_stage = StageErrInvalidPack; - } - - zip_fclose(zf); - } - - zip_discard(za); - zip_error_fini(&error); - - if (!err) - { - m_stage = StageDone; - } -} diff --git a/src/gui/wxgui/DownloadCustomGraphicPackWindow.h b/src/gui/wxgui/DownloadCustomGraphicPackWindow.h deleted file mode 100644 index 586a8644..00000000 --- a/src/gui/wxgui/DownloadCustomGraphicPackWindow.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -struct curlDownloadFileState_t; - -class DownloadCustomGraphicPackWindow : public wxDialog -{ -public: - DownloadCustomGraphicPackWindow(wxWindow* parent); - ~DownloadCustomGraphicPackWindow() override; - - int ShowModal() override; - void OnClose(wxCloseEvent& event); - void OnUpdate(const wxTimerEvent& event); - void OnCancelButton(const wxCommandEvent& event); - void OnDownloadButton(const wxCommandEvent& event); - -public: - struct curlDownloadFileState_t - { - std::vector fileData; - std::atomic progress = 0.0; - std::atomic isCanceled = false; - }; - -private: - void UpdateThread(); - - enum DownloadStage_t - { - StageDownloading, - StageExtracting, - StageVerifying, - StageDone, - - // Error stages - StageErrConnectFailed, - StageErrSourceFailed, - StageErrInvalidPack, - StageErrOpenFailed, - StageErrConflict, - }; - - std::unique_ptr m_downloadState; - std::atomic m_stage; - DownloadStage_t m_currentStage; - std::atomic m_extractionProgress; - - wxTimer* m_timer; - wxTextCtrl* m_urlField; - wxGauge* m_processBar; - wxStaticText* m_statusText; - wxButton* m_downloadButton; - std::thread m_thread; -}; diff --git a/src/gui/wxgui/DownloadGraphicPacksWindow.cpp b/src/gui/wxgui/DownloadGraphicPacksWindow.cpp deleted file mode 100644 index b2f8b849..00000000 --- a/src/gui/wxgui/DownloadGraphicPacksWindow.cpp +++ /dev/null @@ -1,404 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/DownloadGraphicPacksWindow.h" - -#include -#include -#include -#include -#include - -#include "config/ActiveSettings.h" -#include "Common/FileStream.h" - -#include "Cafe/CafeSystem.h" - -struct DownloadGraphicPacksWindow::curlDownloadFileState_t -{ - std::vector fileData; - double progress; -}; - -size_t DownloadGraphicPacksWindow::curlDownloadFile_writeData(void *ptr, size_t size, size_t nmemb, curlDownloadFileState_t* downloadState) -{ - const size_t writeSize = size * nmemb; - const size_t currentSize = downloadState->fileData.size(); - const size_t newSize = currentSize + writeSize; - downloadState->fileData.resize(newSize); - memcpy(downloadState->fileData.data() + currentSize, ptr, writeSize); - return writeSize; -} - -int DownloadGraphicPacksWindow::progress_callback(curlDownloadFileState_t* downloadState, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -{ - if (dltotal > 1.0) - downloadState->progress = dlnow / dltotal; - else - downloadState->progress = 0.0; - return 0; -} - -bool DownloadGraphicPacksWindow::curlDownloadFile(const char *url, curlDownloadFileState_t* downloadState) -{ - CURL* curl = curl_easy_init(); - if (curl == nullptr) - return false; - - downloadState->progress = 0.0; - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlDownloadFile_writeData); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, downloadState); - curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); - curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, downloadState); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1); - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); - - curl_easy_setopt(curl, CURLOPT_USERAGENT, BUILD_VERSION_WITH_NAME_STRING); - downloadState->fileData.resize(0); - const CURLcode res = curl_easy_perform(curl); - curl_easy_cleanup(curl); - return res == CURLE_OK; -} - -// returns true if the version matches -bool checkGraphicPackDownloadedVersion(const char* nameVersion, bool& hasVersionFile) -{ - hasVersionFile = false; - const auto path = ActiveSettings::GetUserDataPath("graphicPacks/downloadedGraphicPacks/version.txt"); - std::unique_ptr file(FileStream::openFile2(path)); - - std::string versionInFile; - if (file && file->readLine(versionInFile)) - { - return boost::iequals(versionInFile, nameVersion); - } - return false; -} - -void createGraphicPackDownloadedVersionFile(const char* nameVersion) -{ - const auto path = ActiveSettings::GetUserDataPath("graphicPacks/downloadedGraphicPacks/version.txt"); - std::unique_ptr file(FileStream::createFile2(path)); - if (file) - file->writeString(nameVersion); - else - { - cemuLog_log(LogType::Force, "Failed to write graphic pack version.txt"); - } -} - -void deleteDownloadedGraphicPacks() -{ - const auto path = ActiveSettings::GetUserDataPath("graphicPacks/downloadedGraphicPacks"); - std::error_code er; - if (!fs::exists(path, er)) - return; - try - { - for (auto& p : fs::directory_iterator(path)) - { - fs::remove_all(p.path(), er); - } - } - catch (std::filesystem::filesystem_error& e) - { - cemuLog_log(LogType::Force, "Error in deleteDownloadedGraphicPacks():"); - cemuLog_log(LogType::Force, e.what()); - } -} - -void DownloadGraphicPacksWindow::UpdateThread() -{ - // get github url - std::string githubAPIUrl; - curlDownloadFileState_t tempDownloadState; - std::string queryUrl("https://cemu.info/api2/query_graphicpack_url.php?"); - char temp[64]; - sprintf(temp, "version=%d.%d.%d", EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_MINOR, EMULATOR_VERSION_PATCH); - queryUrl.append(temp); - queryUrl.append("&"); - sprintf(temp, "t=%u", (uint32)std::chrono::seconds(std::time(NULL)).count()); // add a dynamic part to the url to bypass overly aggressive caching (like some proxies do) - queryUrl.append(temp); - if (curlDownloadFile(queryUrl.c_str(), &tempDownloadState)) - { - // convert downloaded data to url string - githubAPIUrl.assign(tempDownloadState.fileData.cbegin(), tempDownloadState.fileData.cend()); - } - if (githubAPIUrl.empty()) - { - // cemu api request failed, use hardcoded github url - cemuLog_log(LogType::Force, "Graphic pack update request failed or returned invalid URL. Using default repository URL instead"); - githubAPIUrl = "https://api.github.com/repos/cemu-project/cemu_graphic_packs/releases/latest"; - } - // github API request - if (curlDownloadFile(githubAPIUrl.c_str(), &tempDownloadState) == false) - { - wxMessageBox( _("Error"), _(L"Failed to connect to server"), wxOK | wxCENTRE | wxICON_ERROR, this); - m_threadState = ThreadError; - return; - } - // parse json result - rapidjson::Document d; - d.Parse((const char*)tempDownloadState.fileData.data(), tempDownloadState.fileData.size()); - if (d.HasParseError()) - { - m_threadState = ThreadError; - return; - } - auto& jsonName = d["name"]; - if (jsonName.IsString() == false) - { - m_threadState = ThreadError; - return; - } - const char* assetName = jsonName.GetString(); // name includes version - if( d.IsObject() == false) - { - m_threadState = ThreadError; - return; - } - auto& jsonAssets = d["assets"]; - if (jsonAssets.IsArray() == false || jsonAssets.GetArray().Size() == 0) - { - m_threadState = ThreadError; - return; - } - auto& jsonAsset0 = jsonAssets.GetArray()[0]; - if (jsonAsset0.IsObject() == false) - { - m_threadState = ThreadError; - return; - } - auto& jsonDownloadUrl = jsonAsset0["browser_download_url"]; - if (jsonDownloadUrl.IsString() == false) - { - m_threadState = ThreadError; - return; - } - const char* browserDownloadUrl = jsonDownloadUrl.GetString(); - // check version - bool hasVersionFile = false; - if (checkGraphicPackDownloadedVersion(assetName, hasVersionFile)) - { - // already up to date - wxMessageBox(_("No updates available."), _("Graphic packs"), wxOK | wxCENTRE, this); - m_threadState = ThreadFinished; - return; - } - if (hasVersionFile) - { - // if a version file already exists (and graphic packs are installed) ask the user if he really wants to update - if (wxMessageBox(_("Updated graphic packs are available. Do you want to download and install them?"), _("Graphic packs"), wxYES_NO, this) != wxYES) - { - // cancel update - m_threadState = ThreadFinished; - return; - } - } - // download zip - m_stage = StageDownloading; - if (curlDownloadFile(browserDownloadUrl, m_downloadState.get()) == false) - { - wxMessageBox(_("Error"), _(L"Failed to connect to server"), wxOK | wxCENTRE | wxICON_ERROR, this); - m_threadState = ThreadError; - return; - } - - m_extractionProgress = 0.0; - m_stage = StageExtracting; - - zip_source_t *src; - zip_t *za; - zip_error_t error; - - // init zip source - zip_error_init(&error); - if ((src = zip_source_buffer_create(m_downloadState->fileData.data(), m_downloadState->fileData.size(), 0, &error)) == NULL) - { - zip_error_fini(&error); - m_threadState = ThreadError; - return; - } - - // open zip from source - if ((za = zip_open_from_source(src, 0, &error)) == NULL) - { - zip_source_free(src); - zip_error_fini(&error); - m_threadState = ThreadError; - return; - } - - auto path = ActiveSettings::GetUserDataPath("graphicPacks/downloadedGraphicPacks"); - std::error_code er; - //fs::remove_all(path, er); -> Don't delete the whole folder and recreate it immediately afterwards because sometimes it just fails - deleteDownloadedGraphicPacks(); - fs::create_directories(path, er); // make sure downloadedGraphicPacks folder exists - - sint32 numEntries = zip_get_num_entries(za, 0); - for (sint32 i = 0; i < numEntries; i++) - { - m_extractionProgress = (double)i / (double)numEntries; - zip_stat_t sb = { 0 }; - if (zip_stat_index(za, i, 0, &sb) != 0) - { - assert_dbg(); - } - - if(std::strstr(sb.name, "../") != nullptr || - std::strstr(sb.name, "..\\") != nullptr) - continue; // bad path - - path = ActiveSettings::GetUserDataPath("graphicPacks/downloadedGraphicPacks/{}", sb.name); - - size_t sbNameLen = strlen(sb.name); - if(sbNameLen == 0) - continue; - if (sb.name[sbNameLen - 1] == '/') - { - fs::create_directories(path, er); - continue; - } - if(sb.size == 0) - continue; - if (sb.size > (1024 * 1024 * 128)) - continue; // skip unusually huge files - - zip_file_t* zipFile = zip_fopen_index(za, i, 0); - if (zipFile == nullptr) - continue; - - uint8* fileBuffer = new uint8[sb.size]; - - if (zip_fread(zipFile, fileBuffer, sb.size) == sb.size) - { - FileStream* fs = FileStream::createFile2(path); - if (fs) - { - fs->writeData(fileBuffer, sb.size); - delete fs; - } - } - - delete [] fileBuffer; - zip_fclose(zipFile); - } - - zip_discard(za); - zip_error_fini(&error); - createGraphicPackDownloadedVersionFile(assetName); - m_threadState = ThreadFinished; -} - -DownloadGraphicPacksWindow::DownloadGraphicPacksWindow(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Checking version..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), - m_threadState(ThreadRunning), m_stage(StageCheckVersion), m_currentStage(StageCheckVersion) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_processBar = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); - m_processBar->SetValue(0); - m_processBar->SetRange(100); - sizer->Add(m_processBar, 0, wxALL | wxEXPAND, 5); - - auto* m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &DownloadGraphicPacksWindow::OnCancelButton, this); - sizer->Add(m_cancelButton, 0, wxALIGN_RIGHT | wxALL, 5); - - this->SetSizer(sizer); - this->Centre(wxBOTH); - - wxWindowBase::Layout(); - wxWindowBase::Fit(); - - m_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &DownloadGraphicPacksWindow::OnUpdate, this); - this->Bind(wxEVT_CLOSE_WINDOW, &DownloadGraphicPacksWindow::OnClose, this); - m_timer->Start(250); - - - m_downloadState = std::make_unique(); -} - -DownloadGraphicPacksWindow::~DownloadGraphicPacksWindow() -{ - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); -} - -const std::string& DownloadGraphicPacksWindow::GetException() const -{ - return m_threadException; -} - -int DownloadGraphicPacksWindow::ShowModal() -{ - if(CafeSystem::IsTitleRunning()) - { - wxMessageBox(_("Graphic packs cannot be updated while a game is running."), _("Graphic packs"), 5, this); - return wxID_CANCEL; - } - m_thread = std::thread(&DownloadGraphicPacksWindow::UpdateThread, this); - wxDialog::ShowModal(); - return m_threadState == ThreadCanceled ? wxID_CANCEL : wxID_OK; -} - -void DownloadGraphicPacksWindow::OnClose(wxCloseEvent& event) -{ - if (m_threadState == ThreadRunning) - { - //wxMessageDialog dialog(this, _("Do you really want to cancel the update process?\n\nCanceling the process will delete the applied update."), _("Info"), wxCENTRE | wxYES_NO); - //if (dialog.ShowModal() != wxID_YES) - // return; - - m_threadState = ThreadCanceled; - } - - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); - - event.Skip(); -} - -void DownloadGraphicPacksWindow::OnUpdate(const wxTimerEvent& event) -{ - if (m_threadState != ThreadRunning) - { - Close(); - return; - } - if (m_currentStage != m_stage) - { - if (m_stage == StageDownloading) - { - this->SetTitle(_("Downloading graphic packs...")); - } - else if (m_stage == StageExtracting) - { - this->SetTitle(_("Extracting...")); - } - m_currentStage = m_stage; - } - - if (m_currentStage == StageDownloading) - { - const sint32 processedSize = (sint32)(m_downloadState->progress * 100.0f); - if (m_processBar->GetValue() != processedSize) - m_processBar->SetValue(processedSize); - } - else if (m_currentStage == StageExtracting) - { - const sint32 processedSize = (sint32)(m_extractionProgress * 100.0f); - if (m_processBar->GetValue() != processedSize) - m_processBar->SetValue(processedSize); - } -} - -void DownloadGraphicPacksWindow::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} diff --git a/src/gui/wxgui/DownloadGraphicPacksWindow.h b/src/gui/wxgui/DownloadGraphicPacksWindow.h deleted file mode 100644 index 73101f98..00000000 --- a/src/gui/wxgui/DownloadGraphicPacksWindow.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include - - -class DownloadGraphicPacksWindow : public wxDialog -{ -public: - DownloadGraphicPacksWindow(wxWindow* parent); - ~DownloadGraphicPacksWindow(); - - const std::string& GetException() const; - - int ShowModal() override; - void OnClose(wxCloseEvent& event); - - void OnUpdate(const wxTimerEvent& event); - void OnCancelButton(const wxCommandEvent& event); - -private: - void UpdateThread(); - - enum ThreadState_t - { - ThreadRunning, - ThreadCanceled, - ThreadError, - ThreadFinished, - }; - - enum DownloadStage_t - { - StageCheckVersion, - StageDownloading, - StageExtracting - }; - - std::atomic m_threadState; - std::atomic m_stage; - std::atomic m_extractionProgress; - std::string m_threadException; - std::thread m_thread; - - DownloadStage_t m_currentStage; - wxGauge* m_processBar; - wxTimer* m_timer; - - struct curlDownloadFileState_t; - std::unique_ptr m_downloadState; - - static size_t curlDownloadFile_writeData(void* ptr, size_t size, size_t nmemb, curlDownloadFileState_t* downloadState); - static int progress_callback(curlDownloadFileState_t* downloadState, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); - static bool curlDownloadFile(const char* url, curlDownloadFileState_t* downloadState); -}; diff --git a/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp deleted file mode 100644 index c1d4b81b..00000000 --- a/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ /dev/null @@ -1,842 +0,0 @@ -#include "EmulatedUSBDeviceFrame.h" - -#include - -#include "config/CemuConfig.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/wxHelper.h" -#include "util/helpers/helpers.h" - -#include "Cafe/OS/libs/nsyshid/nsyshid.h" -#include "Cafe/OS/libs/nsyshid/Dimensions.h" - -#include "Common/FileStream.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "resource/embedded/resources.h" - -EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) - : wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition, - wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) -{ - SetIcon(wxICON(X_BOX)); - - auto& config = GetConfig(); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - auto* notebook = new wxNotebook(this, wxID_ANY); - - notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); - notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base")); - notebook->AddPage(AddDimensionsPage(notebook), _("Dimensions Toypad")); - - sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); - - SetSizerAndFit(sizer); - Layout(); - Centre(wxBOTH); -} - -EmulatedUSBDeviceFrame::~EmulatedUSBDeviceFrame() {} - -wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook); - auto* panelSizer = new wxBoxSizer(wxVERTICAL); - auto* box = new wxStaticBox(panel, wxID_ANY, _("Skylanders Manager")); - auto* boxSizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* row = new wxBoxSizer(wxHORIZONTAL); - - m_emulatePortal = - new wxCheckBox(box, wxID_ANY, _("Emulate Skylander Portal")); - m_emulatePortal->SetValue( - GetConfig().emulated_usb_devices.emulate_skylander_portal); - m_emulatePortal->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { - GetConfig().emulated_usb_devices.emulate_skylander_portal = - m_emulatePortal->IsChecked(); - GetConfigHandle().Save(); - }); - row->Add(m_emulatePortal, 1, wxEXPAND | wxALL, 2); - boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); - for (int i = 0; i < nsyshid::MAX_SKYLANDERS; i++) - { - boxSizer->Add(AddSkylanderRow(i, box), 1, wxEXPAND | wxALL, 2); - } - panelSizer->Add(boxSizer, 1, wxEXPAND | wxALL, 2); - panel->SetSizerAndFit(panelSizer); - - return panel; -} - -wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook); - auto* panelSizer = new wxBoxSizer(wxVERTICAL); - auto* box = new wxStaticBox(panel, wxID_ANY, _("Infinity Manager")); - auto* boxSizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* row = new wxBoxSizer(wxHORIZONTAL); - - m_emulateBase = - new wxCheckBox(box, wxID_ANY, _("Emulate Infinity Base")); - m_emulateBase->SetValue( - GetConfig().emulated_usb_devices.emulate_infinity_base); - m_emulateBase->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { - GetConfig().emulated_usb_devices.emulate_infinity_base = - m_emulateBase->IsChecked(); - GetConfigHandle().Save(); - }); - row->Add(m_emulateBase, 1, wxEXPAND | wxALL, 2); - boxSizer->Add(row, 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Play Set/Power Disc", 0, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Power Disc Two", 1, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Power Disc Three", 2, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player One", 3, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player One Ability One", 4, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player One Ability Two", 5, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player Two", 6, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player Two Ability One", 7, box), 1, wxEXPAND | wxALL, 2); - boxSizer->Add(AddInfinityRow("Player Two Ability Two", 8, box), 1, wxEXPAND | wxALL, 2); - - panelSizer->Add(boxSizer, 1, wxEXPAND | wxALL, 2); - panel->SetSizerAndFit(panelSizer); - - return panel; -} - -wxPanel* EmulatedUSBDeviceFrame::AddDimensionsPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook); - auto* panel_sizer = new wxBoxSizer(wxVERTICAL); - auto* box = new wxStaticBox(panel, wxID_ANY, _("Dimensions Manager")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* row = new wxBoxSizer(wxHORIZONTAL); - - m_emulateToypad = - new wxCheckBox(box, wxID_ANY, _("Emulate Dimensions Toypad")); - m_emulateToypad->SetValue( - GetConfig().emulated_usb_devices.emulate_dimensions_toypad); - m_emulateToypad->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { - GetConfig().emulated_usb_devices.emulate_dimensions_toypad = - m_emulateToypad->IsChecked(); - GetConfigHandle().Save(); - }); - row->Add(m_emulateToypad, 1, wxEXPAND | wxALL, 2); - box_sizer->Add(row, 1, wxEXPAND | wxALL, 2); - auto* top_row = new wxBoxSizer(wxHORIZONTAL); - auto* bottom_row = new wxBoxSizer(wxHORIZONTAL); - - top_row->Add(AddDimensionPanel(2, 0, box), 1, wxEXPAND | wxALL, 2); - top_row->Add(0, 0, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); - top_row->Add(AddDimensionPanel(1, 1, box), 1, wxEXPAND | wxALL, 2); - top_row->Add(0, 0, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); - top_row->Add(AddDimensionPanel(3, 2, box), 1, wxEXPAND | wxALL, 2); - - bottom_row->Add(AddDimensionPanel(2, 3, box), 1, wxEXPAND | wxALL, 2); - bottom_row->Add(AddDimensionPanel(2, 4, box), 1, wxEXPAND | wxALL, 2); - bottom_row->Add(0, 0, 1, wxEXPAND | wxLEFT | wxRIGHT, 0); - bottom_row->Add(AddDimensionPanel(3, 5, box), 1, wxEXPAND | wxALL, 2); - bottom_row->Add(AddDimensionPanel(3, 6, box), 1, wxEXPAND | wxALL, 2); - - box_sizer->Add(top_row, 1, wxEXPAND | wxALL, 2); - box_sizer->Add(bottom_row, 1, wxEXPAND | wxALL, 2); - panel_sizer->Add(box_sizer, 1, wxEXPAND | wxALL, 2); - panel->SetSizerAndFit(panel_sizer); - - return panel; -} - -wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box) -{ - auto* row = new wxBoxSizer(wxHORIZONTAL); - - row->Add(new wxStaticText(box, wxID_ANY, - fmt::format("{} {}", _("Skylander").ToStdString(), - rowNumber + 1)), - 1, wxEXPAND | wxALL, 2); - m_skylanderSlots[rowNumber] = - new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, - wxTE_READONLY); - m_skylanderSlots[rowNumber]->SetMinSize(wxSize(150, -1)); - m_skylanderSlots[rowNumber]->Disable(); - row->Add(m_skylanderSlots[rowNumber], 1, wxEXPAND | wxALL, 2); - auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); - loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - LoadSkylander(rowNumber); - }); - auto* createButton = new wxButton(box, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - CreateSkylander(rowNumber); - }); - auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); - clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - ClearSkylander(rowNumber); - }); - row->Add(loadButton, 1, wxEXPAND | wxALL, 2); - row->Add(createButton, 1, wxEXPAND | wxALL, 2); - row->Add(clearButton, 1, wxEXPAND | wxALL, 2); - - return row; -} - -wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumber, wxStaticBox* box) -{ - auto* row = new wxBoxSizer(wxHORIZONTAL); - - row->Add(new wxStaticText(box, wxID_ANY, name), 1, wxEXPAND | wxALL, 2); - m_infinitySlots[rowNumber] = - new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, - wxTE_READONLY); - m_infinitySlots[rowNumber]->SetMinSize(wxSize(150, -1)); - m_infinitySlots[rowNumber]->Disable(); - row->Add(m_infinitySlots[rowNumber], 1, wxALL | wxEXPAND, 5); - auto* loadButton = new wxButton(box, wxID_ANY, _("Load")); - loadButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - LoadFigure(rowNumber); - }); - auto* createButton = new wxButton(box, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - CreateFigure(rowNumber); - }); - auto* clearButton = new wxButton(box, wxID_ANY, _("Clear")); - clearButton->Bind(wxEVT_BUTTON, [rowNumber, this](wxCommandEvent&) { - ClearFigure(rowNumber); - }); - row->Add(loadButton, 1, wxEXPAND | wxALL, 2); - row->Add(createButton, 1, wxEXPAND | wxALL, 2); - row->Add(clearButton, 1, wxEXPAND | wxALL, 2); - - return row; -} - -wxBoxSizer* EmulatedUSBDeviceFrame::AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box) -{ - auto* panel = new wxBoxSizer(wxVERTICAL); - - auto* combo_row = new wxBoxSizer(wxHORIZONTAL); - m_dimensionSlots[index] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, - wxTE_READONLY); - combo_row->Add(m_dimensionSlots[index], 1, wxEXPAND | wxALL, 2); - auto* move_button = new wxButton(box, wxID_ANY, _("Move")); - move_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { - MoveMinifig(pad, index); - }); - - combo_row->Add(move_button, 1, wxEXPAND | wxALL, 2); - - auto* button_row = new wxBoxSizer(wxHORIZONTAL); - auto* load_button = new wxButton(box, wxID_ANY, _("Load")); - load_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { - LoadMinifig(pad, index); - }); - auto* clear_button = new wxButton(box, wxID_ANY, _("Clear")); - clear_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { - ClearMinifig(pad, index); - }); - auto* create_button = new wxButton(box, wxID_ANY, _("Create")); - create_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { - CreateMinifig(pad, index); - }); - button_row->Add(clear_button, 1, wxEXPAND | wxALL, 2); - button_row->Add(create_button, 1, wxEXPAND | wxALL, 2); - button_row->Add(load_button, 1, wxEXPAND | wxALL, 2); - - panel->Add(combo_row, 1, wxEXPAND | wxALL, 2); - panel->Add(button_row, 1, wxEXPAND | wxALL, 2); - - return panel; -} - -void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot) -{ - wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "", - "Skylander files (*.sky;*.bin;*.dump;*.dmp)|*.sky;*.bin;*.dump;*.dmp", - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) - return; - - LoadSkylanderPath(slot, openFileDialog.GetPath()); -} - -void EmulatedUSBDeviceFrame::LoadSkylanderPath(uint8 slot, wxString path) -{ - std::unique_ptr skyFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); - if (!skyFile) - { - wxMessageDialog open_error(this, "Error Opening File: " + path); - open_error.ShowModal(); - return; - } - - std::array fileData; - if (skyFile->readData(fileData.data(), fileData.size()) != fileData.size()) - { - wxMessageDialog open_error(this, "Failed to read file! File was too small"); - open_error.ShowModal(); - return; - } - ClearSkylander(slot); - - uint16 skyId = uint16(fileData[0x11]) << 8 | uint16(fileData[0x10]); - uint16 skyVar = uint16(fileData[0x1D]) << 8 | uint16(fileData[0x1C]); - - uint8 portalSlot = nsyshid::g_skyportal.LoadSkylander(fileData.data(), - std::move(skyFile)); - m_skySlots[slot] = std::tuple(portalSlot, skyId, skyVar); - UpdateSkylanderEdits(); -} - -void EmulatedUSBDeviceFrame::CreateSkylander(uint8 slot) -{ - CreateSkylanderDialog create_dlg(this, slot); - create_dlg.ShowModal(); - if (create_dlg.GetReturnCode() == 1) - { - LoadSkylanderPath(slot, create_dlg.GetFilePath()); - } -} - -void EmulatedUSBDeviceFrame::ClearSkylander(uint8 slot) -{ - if (auto slotInfos = m_skySlots[slot]) - { - auto [curSlot, id, var] = slotInfos.value(); - nsyshid::g_skyportal.RemoveSkylander(curSlot); - m_skySlots[slot] = {}; - UpdateSkylanderEdits(); - } -} - -CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) - : wxDialog(parent, wxID_ANY, _("Skylander Figure Creator"), wxDefaultPosition, wxSize(500, 150)) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - auto* comboRow = new wxBoxSizer(wxHORIZONTAL); - - auto* comboBox = new wxComboBox(this, wxID_ANY); - comboBox->Append("---Select---", reinterpret_cast(0xFFFFFFFF)); - wxArrayString filterlist; - for (const auto& it : nsyshid::g_skyportal.GetListSkylanders()) - { - const uint32 variant = uint32(uint32(it.first.first) << 16) | uint32(it.first.second); - comboBox->Append(it.second, reinterpret_cast(variant)); - filterlist.Add(it.second); - } - comboBox->SetSelection(0); - bool enabled = comboBox->AutoComplete(filterlist); - comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); - - auto* idVarRow = new wxBoxSizer(wxHORIZONTAL); - - wxIntegerValidator validator; - - auto* labelId = new wxStaticText(this, wxID_ANY, "ID:"); - auto* labelVar = new wxStaticText(this, wxID_ANY, "Variant:"); - auto* editId = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); - auto* editVar = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); - - idVarRow->Add(labelId, 1, wxALL, 5); - idVarRow->Add(editId, 1, wxALL, 5); - idVarRow->Add(labelVar, 1, wxALL, 5); - idVarRow->Add(editVar, 1, wxALL, 5); - - auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); - - auto* createButton = new wxButton(this, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [editId, editVar, this](wxCommandEvent&) { - long longSkyId; - if (!editId->GetValue().ToLong(&longSkyId) || longSkyId > 0xFFFF) - { - wxMessageDialog idError(this, "Error Converting ID!", "ID Entered is Invalid"); - idError.ShowModal(); - return; - } - long longSkyVar; - if (!editVar->GetValue().ToLong(&longSkyVar) || longSkyVar > 0xFFFF) - { - wxMessageDialog idError(this, "Error Converting Variant!", "Variant Entered is Invalid"); - idError.ShowModal(); - return; - } - uint16 skyId = longSkyId & 0xFFFF; - uint16 skyVar = longSkyVar & 0xFFFF; - wxString predefName = nsyshid::g_skyportal.FindSkylander(skyId, skyVar) + ".sky"; - wxFileDialog - saveFileDialog(this, _("Create Skylander file"), "", predefName, - "SKY files (*.sky)|*.sky", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (saveFileDialog.ShowModal() == wxID_CANCEL) - return; - - m_filePath = saveFileDialog.GetPath(); - - if (!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) - { - wxMessageDialog errorMessage(this, "Failed to create file"); - errorMessage.ShowModal(); - this->EndModal(0); - return; - } - - this->EndModal(1); - }); - auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - this->EndModal(0); - }); - - comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editId, editVar, this](wxCommandEvent&) { - const uint64 sky_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); - if (sky_info != 0xFFFFFFFF) - { - const uint16 skyId = sky_info >> 16; - const uint16 skyVar = sky_info & 0xFFFF; - - editId->SetValue(wxString::Format(wxT("%i"), skyId)); - editVar->SetValue(wxString::Format(wxT("%i"), skyVar)); - } - }); - - buttonRow->Add(createButton, 1, wxALL, 5); - buttonRow->Add(cancelButton, 1, wxALL, 5); - - sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(idVarRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); - - this->SetSizer(sizer); - this->Centre(wxBOTH); -} - -wxString CreateSkylanderDialog::GetFilePath() const -{ - return m_filePath; -} - -void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() -{ - for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) - { - std::string displayString; - if (auto sd = m_skySlots[i]) - { - auto [portalSlot, skyId, skyVar] = sd.value(); - displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); - } - else - { - displayString = "None"; - } - - m_skylanderSlots[i]->ChangeValue(displayString); - } -} - -void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) -{ - wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", - "BIN files (*.bin)|*.bin", - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) - { - wxMessageDialog errorMessage(this, "File Okay Error"); - errorMessage.ShowModal(); - return; - } - - LoadFigurePath(slot, openFileDialog.GetPath()); -} - -void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) -{ - std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); - if (!infFile) - { - wxMessageDialog errorMessage(this, "File Open Error"); - errorMessage.ShowModal(); - return; - } - - std::array fileData; - if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) - { - wxMessageDialog open_error(this, "Failed to read file! File was too small"); - open_error.ShowModal(); - return; - } - ClearFigure(slot); - - uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); - m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); -} - -void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) -{ - cemuLog_log(LogType::Force, "Create Figure: {}", slot); - CreateInfinityFigureDialog create_dlg(this, slot); - create_dlg.ShowModal(); - if (create_dlg.GetReturnCode() == 1) - { - LoadFigurePath(slot, create_dlg.GetFilePath()); - } -} - -void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) -{ - m_infinitySlots[slot]->ChangeValue("None"); - nsyshid::g_infinitybase.RemoveFigure(slot); -} - -CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot) - : wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150)) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - auto* comboRow = new wxBoxSizer(wxHORIZONTAL); - - auto* comboBox = new wxComboBox(this, wxID_ANY); - comboBox->Append("---Select---", reinterpret_cast(0xFFFFFF)); - wxArrayString filterlist; - for (const auto& it : nsyshid::g_infinitybase.GetFigureList()) - { - const uint32 figure = it.first; - if ((slot == 0 && - ((figure > 0x1E8480 && figure < 0x2DC6BF) || (figure > 0x3D0900 && figure < 0x4C4B3F))) || - ((slot == 1 || slot == 2) && (figure > 0x3D0900 && figure < 0x4C4B3F)) || - ((slot == 3 || slot == 6) && figure < 0x1E847F) || - ((slot == 4 || slot == 5 || slot == 7 || slot == 8) && - (figure > 0x2DC6C0 && figure < 0x3D08FF))) - { - comboBox->Append(it.second.second, reinterpret_cast(figure)); - filterlist.Add(it.second.second); - } - } - comboBox->SetSelection(0); - bool enabled = comboBox->AutoComplete(filterlist); - comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); - - auto* figNumRow = new wxBoxSizer(wxHORIZONTAL); - - wxIntegerValidator validator; - - auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:"); - auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); - - figNumRow->Add(labelFigNum, 1, wxALL, 5); - figNumRow->Add(editFigNum, 1, wxALL, 5); - - auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); - - auto* createButton = new wxButton(this, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) { - long longFigNum; - if (!editFigNum->GetValue().ToLong(&longFigNum)) - { - wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); - idError.ShowModal(); - this->EndModal(0); - return; - } - uint32 figNum = longFigNum & 0xFFFFFFFF; - auto figure = nsyshid::g_infinitybase.FindFigure(figNum); - wxString predefName = figure.second + ".bin"; - wxFileDialog - saveFileDialog(this, _("Create Infinity Figure file"), "", predefName, - "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (saveFileDialog.ShowModal() == wxID_CANCEL) - { - this->EndModal(0); - return; - } - - m_filePath = saveFileDialog.GetPath(); - - nsyshid::g_infinitybase.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum, figure.first); - - this->EndModal(1); - }); - auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - this->EndModal(0); - }); - - comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) { - const uint64 fig_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); - if (fig_info != 0xFFFFFF) - { - const uint32 figNum = fig_info & 0xFFFFFFFF; - - editFigNum->SetValue(wxString::Format(wxT("%i"), figNum)); - } - }); - - buttonRow->Add(createButton, 1, wxALL, 5); - buttonRow->Add(cancelButton, 1, wxALL, 5); - - sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); - - this->SetSizer(sizer); - this->Centre(wxBOTH); -} - -wxString CreateInfinityFigureDialog::GetFilePath() const -{ - return m_filePath; -} - -void EmulatedUSBDeviceFrame::LoadMinifig(uint8 pad, uint8 index) -{ - wxFileDialog openFileDialog(this, _("Load Dimensions Figure"), "", "", - "Dimensions files (*.bin)|*.bin", - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) - return; - - LoadMinifigPath(openFileDialog.GetPath(), pad, index); -} - -void EmulatedUSBDeviceFrame::LoadMinifigPath(wxString path_name, uint8 pad, uint8 index) -{ - std::unique_ptr dim_file(FileStream::openFile2(_utf8ToPath(path_name.utf8_string()), true)); - if (!dim_file) - { - wxMessageDialog errorMessage(this, "Failed to open minifig file"); - errorMessage.ShowModal(); - return; - } - - std::array file_data; - - if (dim_file->readData(file_data.data(), file_data.size()) != file_data.size()) - { - wxMessageDialog errorMessage(this, "Failed to read minifig file data"); - errorMessage.ShowModal(); - return; - } - - ClearMinifig(pad, index); - - uint32 id = nsyshid::g_dimensionstoypad.LoadFigure(file_data, std::move(dim_file), pad, index); - m_dimensionSlots[index]->ChangeValue(nsyshid::g_dimensionstoypad.FindFigure(id)); - m_dimSlots[index] = id; -} - -void EmulatedUSBDeviceFrame::ClearMinifig(uint8 pad, uint8 index) -{ - nsyshid::g_dimensionstoypad.RemoveFigure(pad, index, true); - m_dimensionSlots[index]->ChangeValue("None"); - m_dimSlots[index] = std::nullopt; -} - -void EmulatedUSBDeviceFrame::CreateMinifig(uint8 pad, uint8 index) -{ - CreateDimensionFigureDialog create_dlg(this); - create_dlg.ShowModal(); - if (create_dlg.GetReturnCode() == 1) - { - LoadMinifigPath(create_dlg.GetFilePath(), pad, index); - } -} - -void EmulatedUSBDeviceFrame::MoveMinifig(uint8 pad, uint8 index) -{ - if (!m_dimSlots[index]) - return; - - MoveDimensionFigureDialog move_dlg(this, index); - nsyshid::g_dimensionstoypad.TempRemove(index); - move_dlg.ShowModal(); - if (move_dlg.GetReturnCode() == 1) - { - nsyshid::g_dimensionstoypad.MoveFigure(move_dlg.GetNewPad(), move_dlg.GetNewIndex(), pad, index); - if (index != move_dlg.GetNewIndex()) - { - m_dimSlots[move_dlg.GetNewIndex()] = m_dimSlots[index]; - m_dimensionSlots[move_dlg.GetNewIndex()]->ChangeValue(m_dimensionSlots[index]->GetValue()); - m_dimSlots[index] = std::nullopt; - m_dimensionSlots[index]->ChangeValue("None"); - } - } - else - { - nsyshid::g_dimensionstoypad.CancelRemove(index); - } -} - -CreateDimensionFigureDialog::CreateDimensionFigureDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Dimensions Figure Creator"), wxDefaultPosition, wxSize(500, 200)) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - auto* comboRow = new wxBoxSizer(wxHORIZONTAL); - - auto* comboBox = new wxComboBox(this, wxID_ANY); - comboBox->Append("---Select---", reinterpret_cast(0xFFFFFFFF)); - wxArrayString filterlist; - for (const auto& it : nsyshid::g_dimensionstoypad.GetListMinifigs()) - { - const uint32 figure = it.first; - comboBox->Append(it.second, reinterpret_cast(figure)); - filterlist.Add(it.second); - } - comboBox->SetSelection(0); - bool enabled = comboBox->AutoComplete(filterlist); - comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); - - auto* figNumRow = new wxBoxSizer(wxHORIZONTAL); - - wxIntegerValidator validator; - - auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:"); - auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); - - figNumRow->Add(labelFigNum, 1, wxALL, 5); - figNumRow->Add(editFigNum, 1, wxALL, 5); - - auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); - - auto* createButton = new wxButton(this, wxID_ANY, _("Create")); - createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) { - long longFigNum; - if (!editFigNum->GetValue().ToLong(&longFigNum) || longFigNum > 0xFFFF) - { - wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); - idError.ShowModal(); - this->EndModal(0); - return; - } - uint16 figNum = longFigNum & 0xFFFF; - auto figure = nsyshid::g_dimensionstoypad.FindFigure(figNum); - wxString predefName = figure + ".bin"; - wxFileDialog - saveFileDialog(this, _("Create Dimensions Figure file"), "", predefName, - "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - - if (saveFileDialog.ShowModal() == wxID_CANCEL) - { - this->EndModal(0); - return; - } - - m_filePath = saveFileDialog.GetPath(); - - nsyshid::g_dimensionstoypad.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum); - - this->EndModal(1); - }); - auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - this->EndModal(0); - }); - - comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) { - const uint64 fig_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); - if (fig_info != 0xFFFF) - { - const uint16 figNum = fig_info & 0xFFFF; - - editFigNum->SetValue(wxString::Format(wxT("%i"), figNum)); - } - }); - - buttonRow->Add(createButton, 1, wxALL, 5); - buttonRow->Add(cancelButton, 1, wxALL, 5); - - sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2); - sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); - - this->SetSizer(sizer); - this->Centre(wxBOTH); -} - -wxString CreateDimensionFigureDialog::GetFilePath() const -{ - return m_filePath; -} - -MoveDimensionFigureDialog::MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex) - : wxDialog(parent, wxID_ANY, _("Dimensions Figure Mover"), wxDefaultPosition, wxSize(700, 300)) -{ - auto* sizer = new wxGridSizer(2, 5, 10, 10); - - std::array, 7> ids = parent->GetCurrentMinifigs(); - - sizer->Add(AddMinifigSlot(2, 0, currentIndex, ids[0]), 1, wxALL, 5); - sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); - sizer->Add(AddMinifigSlot(1, 1, currentIndex, ids[1]), 1, wxALL, 5); - sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); - sizer->Add(AddMinifigSlot(3, 2, currentIndex, ids[2]), 1, wxALL, 5); - - sizer->Add(AddMinifigSlot(2, 3, currentIndex, ids[3]), 1, wxALL, 5); - sizer->Add(AddMinifigSlot(2, 4, currentIndex, ids[4]), 1, wxALL, 5); - sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); - sizer->Add(AddMinifigSlot(3, 5, currentIndex, ids[5]), 1, wxALL, 5); - sizer->Add(AddMinifigSlot(3, 6, currentIndex, ids[6]), 1, wxALL, 5); - - this->SetSizer(sizer); - this->Centre(wxBOTH); -} - -wxBoxSizer* MoveDimensionFigureDialog::AddMinifigSlot(uint8 pad, uint8 index, uint8 currentIndex, std::optional currentId) -{ - auto* panel = new wxBoxSizer(wxVERTICAL); - - auto* label = new wxStaticText(this, wxID_ANY, "None"); - if (currentId) - label->SetLabel(nsyshid::g_dimensionstoypad.FindFigure(currentId.value())); - - auto* moveButton = new wxButton(this, wxID_ANY, _("Move Here")); - if (index == currentIndex) - moveButton->SetLabelText("Pick up and Place"); - - moveButton->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { - m_newPad = pad; - m_newIndex = index; - this->EndModal(1); - }); - - panel->Add(label, 1, wxALL, 5); - panel->Add(moveButton, 1, wxALL, 5); - - return panel; -} - -uint8 MoveDimensionFigureDialog::GetNewPad() const -{ - return m_newPad; -} - -uint8 MoveDimensionFigureDialog::GetNewIndex() const -{ - return m_newIndex; -} - -std::array, 7> EmulatedUSBDeviceFrame::GetCurrentMinifigs() -{ - return m_dimSlots; -} diff --git a/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h deleted file mode 100644 index 78c70a4a..00000000 --- a/src/gui/wxgui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include "Cafe/OS/libs/nsyshid/Infinity.h" -#include "Cafe/OS/libs/nsyshid/Skylander.h" - -class wxBoxSizer; -class wxCheckBox; -class wxFlexGridSizer; -class wxNotebook; -class wxPanel; -class wxStaticBox; -class wxString; -class wxTextCtrl; - -class EmulatedUSBDeviceFrame : public wxFrame -{ - public: - EmulatedUSBDeviceFrame(wxWindow* parent); - ~EmulatedUSBDeviceFrame(); - std::array, 7> GetCurrentMinifigs(); - - private: - wxCheckBox* m_emulatePortal; - wxCheckBox* m_emulateBase; - wxCheckBox* m_emulateToypad; - std::array m_skylanderSlots; - std::array m_infinitySlots; - std::array m_dimensionSlots; - std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; - std::array, 7> m_dimSlots; - - wxPanel* AddSkylanderPage(wxNotebook* notebook); - wxPanel* AddInfinityPage(wxNotebook* notebook); - wxPanel* AddDimensionsPage(wxNotebook* notebook); - wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); - wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box); - wxBoxSizer* AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box); - void LoadSkylander(uint8 slot); - void LoadSkylanderPath(uint8 slot, wxString path); - void CreateSkylander(uint8 slot); - void ClearSkylander(uint8 slot); - void UpdateSkylanderEdits(); - void LoadFigure(uint8 slot); - void LoadFigurePath(uint8 slot, wxString path); - void CreateFigure(uint8 slot); - void ClearFigure(uint8 slot); - void LoadMinifig(uint8 pad, uint8 index); - void LoadMinifigPath(wxString path_name, uint8 pad, uint8 index); - void CreateMinifig(uint8 pad, uint8 index); - void ClearMinifig(uint8 pad, uint8 index); - void MoveMinifig(uint8 pad, uint8 index); -}; - -class CreateSkylanderDialog : public wxDialog -{ - public: - explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); - wxString GetFilePath() const; - - protected: - wxString m_filePath; -}; - -class CreateInfinityFigureDialog : public wxDialog -{ - public: - explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot); - wxString GetFilePath() const; - - protected: - wxString m_filePath; -}; - -class CreateDimensionFigureDialog : public wxDialog -{ - public: - explicit CreateDimensionFigureDialog(wxWindow* parent); - wxString GetFilePath() const; - - protected: - wxString m_filePath; -}; - -class MoveDimensionFigureDialog : public wxDialog -{ - public: - explicit MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex); - uint8 GetNewPad() const; - uint8 GetNewIndex() const; - - protected: - uint8 m_newIndex = 0; - uint8 m_newPad = 0; - - private: - wxBoxSizer* AddMinifigSlot(uint8 pad, uint8 index, uint8 oldIndex, std::optional currentId); -}; \ No newline at end of file diff --git a/src/gui/wxgui/GameProfileWindow.cpp b/src/gui/wxgui/GameProfileWindow.cpp deleted file mode 100644 index 5962133e..00000000 --- a/src/gui/wxgui/GameProfileWindow.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include "wxgui/GameProfileWindow.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "wxgui/helpers/wxHelpers.h" -#include "input/InputManager.h" - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif - -GameProfileWindow::GameProfileWindow(wxWindow* parent, uint64_t title_id) - : wxFrame(parent, wxID_ANY, _("Edit game profile"), wxDefaultPosition, wxSize{ 390, 350 }, wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER | wxTAB_TRAVERSAL | wxSYSTEM_MENU), m_title_id(title_id) -{ - SetIcon(wxICON(X_GAME_PROFILE)); - - m_game_profile.Reset(); - m_game_profile.Load(title_id); - - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - auto* main_sizer = new wxBoxSizer(wxVERTICAL); - - auto* m_notebook = new wxNotebook(this, wxID_ANY); - // general - { - auto* panel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* sizer = new wxBoxSizer(wxVERTICAL); - { - auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(panel, wxID_ANY, _("General")), wxVERTICAL); - auto* box = box_sizer->GetStaticBox(); - - m_load_libs = new wxCheckBox(box, wxID_ANY, _("Load shared libraries")); - m_load_libs->SetToolTip(_("EXPERT OPTION\nThis option will load libraries from the cafeLibs directory")); - box_sizer->Add(m_load_libs, 0, wxALL, 5); - - m_start_with_padview = new wxCheckBox(box, wxID_ANY, _("Launch with gamepad view")); - m_start_with_padview->SetToolTip(_("Games will be launched with gamepad view toggled as default. The view can be toggled with CTRL + TAB")); - box_sizer->Add(m_start_with_padview, 0, wxALL, 5); - - sizer->Add(box_sizer, 0, wxEXPAND, 5); - } - // cpu - { - auto* box_sizer = new wxStaticBoxSizer(new wxStaticBox(panel, wxID_ANY, _("CPU")), wxVERTICAL); - auto* box = box_sizer->GetStaticBox(); - - auto* first_row = new wxFlexGridSizer(0, 2, 0, 0); - first_row->SetFlexibleDirection(wxBOTH); - first_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - first_row->Add(new wxStaticText(box, wxID_ANY, _("Mode")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString cpu_modes[] = { _("Single-core interpreter"), _("Single-core recompiler"), _("Multi-core recompiler"), _("Auto (recommended)") }; - const sint32 m_cpu_modeNChoices = std::size(cpu_modes); - m_cpu_mode = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, m_cpu_modeNChoices, cpu_modes, 0); - m_cpu_mode->SetToolTip(_("Set the CPU emulation mode")); - first_row->Add(m_cpu_mode, 0, wxALL, 5); - - first_row->Add(new wxStaticText(box, wxID_ANY, _("Thread quantum")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* quantum_sizer = new wxFlexGridSizer(0, 2, 0, 0); - quantum_sizer->SetFlexibleDirection(wxBOTH); - quantum_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - wxString quantum_values[] = { "20000", "45000", "60000", "80000" ,"100000" }; - m_thread_quantum = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(quantum_values), quantum_values); - m_thread_quantum->SetMinSize(wxSize(85, -1)); - m_thread_quantum->SetToolTip(_("EXPERT OPTION\nSet the maximum thread slice runtime (in virtual cycles)")); - quantum_sizer->Add(m_thread_quantum, 0, wxALL, 5); - - quantum_sizer->Add(new wxStaticText(box, wxID_ANY, _("cycles")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - first_row->Add(quantum_sizer, 0, wxEXPAND, 5); - - box_sizer->Add(first_row, 0, wxEXPAND, 5); - - - sizer->Add(box_sizer, 0, wxEXPAND, 5); - } - - panel->SetSizer(sizer); - panel->Layout(); - sizer->Fit(panel); - m_notebook->AddPage(panel, _("General"), true); - } - - // graphic - { - auto* panel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* sizer = new wxBoxSizer(wxVERTICAL); - - //m_extended_texture_readback = new wxCheckBox(panel, wxID_ANY, _("Extended texture readback")); - //m_extended_texture_readback->SetToolTip(_("Improves emulation accuracy of CPU to GPU memory access at the cost of performance. Required for some games.")); - //sizer->Add(m_extended_texture_readback, 0, wxALL, 5); - - auto* first_row = new wxFlexGridSizer(0, 2, 0, 0); - first_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - /*first_row->Add(new wxStaticText(panel, wxID_ANY, _("Precompiled shaders")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - wxString precompiled_modes[] = { _("auto"), _("enable"), _("disable") }; - const sint32 precompiled_count = std::size(precompiled_modes); - m_precompiled = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, precompiled_count, precompiled_modes, 0); - m_precompiled->SetToolTip(_("Precompiled shaders can speed up the load time on the shader loading screen.\nAuto will enable it for AMD/Intel but disable it for NVIDIA GPUs as a workaround for a driver bug.\n\nRecommended: Auto")); - first_row->Add(m_precompiled, 0, wxALL, 5);*/ - - first_row->Add(new wxStaticText(panel, wxID_ANY, _("Graphics API")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString gapi_values[] = { "", "OpenGL", "Vulkan", -#ifdef ENABLE_METAL - "Metal" -#endif - }; - m_graphic_api = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(gapi_values), gapi_values); - first_row->Add(m_graphic_api, 0, wxALL, 5); - - first_row->Add(new wxStaticText(panel, wxID_ANY, _("Shader multiplication accuracy")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString mul_values[] = { _("false"), _("true")}; - m_shader_mul_accuracy = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(mul_values), mul_values); - m_shader_mul_accuracy->SetToolTip(_("EXPERT OPTION\nControls the accuracy of floating point multiplication in shaders.\n\nRecommended: true")); - first_row->Add(m_shader_mul_accuracy, 0, wxALL, 5); - -#ifdef ENABLE_METAL - first_row->Add(new wxStaticText(panel, wxID_ANY, _("Shader fast math")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString math_values[] = { _("false"), _("true") }; - m_shader_fast_math = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(math_values), math_values); - m_shader_fast_math->SetToolTip(_("EXPERT OPTION\nEnables fast math for all shaders. May (rarely) cause graphical bugs.\n\nMetal only\n\nRecommended: true")); - first_row->Add(m_shader_fast_math, 0, wxALL, 5); - - first_row->Add(new wxStaticText(panel, wxID_ANY, _("Metal buffer cache mode")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString cache_values[] = { _("auto"), _("device private"), _("device shared"), _("host") }; - m_metal_buffer_cache_mode = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(cache_values), cache_values); - m_metal_buffer_cache_mode->SetToolTip(_("EXPERT OPTION\nDecides how the buffer cache memory will be managed.\n\nMetal only\n\nRecommended: auto")); - first_row->Add(m_metal_buffer_cache_mode, 0, wxALL, 5); - - first_row->Add(new wxStaticText(panel, wxID_ANY, _("Position invariance")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString pos_values[] = { _("auto"), _("false"), _("true") }; - m_position_invariance = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(pos_values), pos_values); - m_position_invariance->SetToolTip(_("EXPERT OPTION\nDisables most optimizations for vertex positions. May fix polygon cutouts or flickering in some games.\n\nMetal only\n\nRecommended: auto")); - first_row->Add(m_position_invariance, 0, wxALL, 5); -#endif - - /*first_row->Add(new wxStaticText(panel, wxID_ANY, _("GPU buffer cache accuracy")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - wxString accuarcy_values[] = { _("high"), _("medium"), _("low") }; - m_cache_accuracy = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (int)std::size(accuarcy_values), accuarcy_values); - m_cache_accuracy->SetToolTip(_("Lower value results in higher performance, but may cause graphical issues")); - first_row->Add(m_cache_accuracy, 0, wxALL, 5);*/ - - sizer->Add(first_row, 0, wxEXPAND, 5); - - - panel->SetSizer(sizer); - panel->Layout(); - sizer->Fit(panel); - m_notebook->AddPage(panel, _("Graphic"), false); - } - - //// audio - //{ - // auto panel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - // wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - - // m_disable_audio = new wxCheckBox(panel, wxID_ANY, _("Disable Audio"), wxDefaultPosition, wxDefaultSize, wxCHK_3STATE | wxCHK_ALLOW_3RD_STATE_FOR_USER); - // sizer->Add(m_disable_audio, 0, wxALL, 5); - - - // panel->SetSizer(sizer); - // panel->Layout(); - // sizer->Fit(panel); - // m_notebook->AddPage(panel, _("Audio"), false); - //} - - // controller - { - auto panel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* sizer; - sizer = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer* profile_sizer; - profile_sizer = new wxFlexGridSizer(0, 2, 0, 0); - profile_sizer->SetFlexibleDirection(wxBOTH); - profile_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - for (int i = 0; i < 8; ++i) - { - profile_sizer->Add(new wxStaticText(panel, wxID_ANY, formatWxString(_("Controller {}"), i + 1)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_controller_profile[i] = new wxComboBox(panel, wxID_ANY,"", wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_DROPDOWN| wxCB_READONLY); - m_controller_profile[i]->SetMinSize(wxSize(250, -1)); - m_controller_profile[i]->Bind(wxEVT_COMBOBOX_DROPDOWN, &GameProfileWindow::OnControllerProfileDropdown, this); - m_controller_profile[i]->SetToolTip(_("Forces a given controller profile")); - profile_sizer->Add(m_controller_profile[i], 0, wxALL, 5); - } - - sizer->Add(profile_sizer, 0, wxEXPAND, 5); - - panel->SetSizer(sizer); - panel->Layout(); - sizer->Fit(panel); - m_notebook->AddPage(panel, _("Controller"), false); - } - - main_sizer->Add(m_notebook, 1, wxEXPAND | wxALL, 5); - - - this->SetSizer(main_sizer); - this->Layout(); - - this->Centre(wxBOTH); - - ApplyProfile(); -} - -GameProfileWindow::~GameProfileWindow() -{ - SaveProfile(); -} - -void GameProfileWindow::OnStreamoutSizeChange(wxCommandEvent& event) -{ - wxSlider* slider = wxDynamicCast(event.GetEventObject(), wxSlider); - wxASSERT(slider); - wxStaticText* text = wxDynamicCast(slider->GetClientData(), wxStaticText); - wxASSERT(text); - text->SetLabelText(fmt::format("{} MB", slider->GetValue())); - event.Skip(); -} - -void GameProfileWindow::OnControllerProfileDropdown(wxCommandEvent& event) -{ - wxComboBox* cb = wxDynamicCast(event.GetEventObject(), wxComboBox); - wxASSERT(cb); - - wxWindowUpdateLocker lock(cb); - - const auto selected_value = cb->GetStringSelection(); - cb->Clear(); - cb->Append(wxEmptyString); - - auto profiles = InputManager::get_profiles(); - for (const auto& profile : profiles) - { - cb->Append(wxString::FromUTF8(profile)); - } - - cb->SetStringSelection(selected_value); -} - -void GameProfileWindow::SetProfileInt(gameProfileIntegerOption_t& option, wxCheckBox* checkbox, sint32 value) const -{ - const auto state = checkbox->GetValue(); - if (state) - { - option.isPresent = true; - option.value = value; - } - else - option.isPresent = false; -} - -void GameProfileWindow::ApplyProfile() -{ - if(m_game_profile.m_gameName) - this->SetTitle(_("Edit game profile") + " - " + m_game_profile.m_gameName.value()); - - // general - m_load_libs->SetValue(m_game_profile.m_loadSharedLibraries.value()); - m_start_with_padview->SetValue(m_game_profile.m_startWithPadView); - - // cpu - // wxString cpu_modes[] = { _("Singlecore-Interpreter"), _("Singlecore-Recompiler"), _("Triplecore-Recompiler"), _("Auto (recommended)") }; - switch(m_game_profile.m_cpuMode.value()) - { - case CPUMode::SinglecoreInterpreter: m_cpu_mode->SetSelection(0); break; - case CPUMode::SinglecoreRecompiler: m_cpu_mode->SetSelection(1); break; - case CPUMode::DualcoreRecompiler: m_cpu_mode->SetSelection(2); break; - case CPUMode::MulticoreRecompiler: m_cpu_mode->SetSelection(2); break; - default: m_cpu_mode->SetSelection(3); - } - - m_thread_quantum->SetStringSelection(fmt::format("{}", m_game_profile.m_threadQuantum)); - - // gpu - if (!m_game_profile.m_graphics_api.has_value()) - m_graphic_api->SetSelection(0); // selecting "" - else - m_graphic_api->SetSelection(1 + m_game_profile.m_graphics_api.value()); // "", OpenGL, Vulkan, Metal - m_shader_mul_accuracy->SetSelection((int)m_game_profile.m_accurateShaderMul); -#ifdef ENABLE_METAL - m_shader_fast_math->SetSelection((int)m_game_profile.m_shaderFastMath); - m_metal_buffer_cache_mode->SetSelection((int)m_game_profile.m_metalBufferCacheMode); - m_position_invariance->SetSelection((int)m_game_profile.m_positionInvariance); -#endif - - //// audio - //m_disable_audio->Set3StateValue(GetCheckboxState(m_game_profile.disableAudio)); - - // controller - auto profiles = InputManager::get_profiles(); - - for (const auto& cb : m_controller_profile) - { - cb->Clear(); - for (const auto& profile : profiles) - { - cb->Append(wxString::FromUTF8(profile)); - } - } - - for (int i = 0; i < InputManager::kMaxController; ++i) - { - const bool has_value = m_game_profile.m_controllerProfile[i].has_value(); - if (has_value) - { - const auto& v = m_game_profile.m_controllerProfile[i].value(); - m_controller_profile[i]->SetStringSelection(wxString::FromUTF8(v)); - } - - else - m_controller_profile[i]->SetSelection(wxNOT_FOUND); - } -} - -void GameProfileWindow::SaveProfile() -{ - // update game profile struct - m_game_profile.Reset(); - // general - m_game_profile.m_loadSharedLibraries = m_load_libs->GetValue(); - m_game_profile.m_startWithPadView = m_start_with_padview->GetValue(); - - // cpu - switch(m_cpu_mode->GetSelection()) - { - case 0: m_game_profile.m_cpuMode = CPUMode::SinglecoreInterpreter; break; - case 1: m_game_profile.m_cpuMode = CPUMode::SinglecoreRecompiler; break; - case 2: m_game_profile.m_cpuMode = CPUMode::MulticoreRecompiler; break; - default: - m_game_profile.m_cpuMode = CPUMode::Auto; - } - - - const wxString thread_quantum = m_thread_quantum->GetStringSelection(); - if (!thread_quantum.empty()) - { - m_game_profile.m_threadQuantum = ConvertString(thread_quantum.ToStdString()); - m_game_profile.m_threadQuantum = std::min(m_game_profile.m_threadQuantum, 536870912); - m_game_profile.m_threadQuantum = std::max(m_game_profile.m_threadQuantum, 5000); - } - - // gpu - m_game_profile.m_accurateShaderMul = (AccurateShaderMulOption)m_shader_mul_accuracy->GetSelection(); - if (m_game_profile.m_accurateShaderMul != AccurateShaderMulOption::False && m_game_profile.m_accurateShaderMul != AccurateShaderMulOption::True) - m_game_profile.m_accurateShaderMul = AccurateShaderMulOption::True; // force a legal value -#ifdef ENABLE_METAL - m_game_profile.m_shaderFastMath = (bool)m_shader_fast_math->GetSelection(); - m_game_profile.m_metalBufferCacheMode = (MetalBufferCacheMode)m_metal_buffer_cache_mode->GetSelection(); - m_game_profile.m_positionInvariance = (PositionInvariance)m_position_invariance->GetSelection(); -#endif - - if (m_graphic_api->GetSelection() == 0) - m_game_profile.m_graphics_api = {}; - else - m_game_profile.m_graphics_api = (GraphicAPI)(m_graphic_api->GetSelection() - 1); // "", OpenGL, Vulkan, Metal - - // controller - for (int i = 0; i < 8; ++i) - { - if(m_controller_profile[i]->GetSelection() == wxNOT_FOUND) - { - m_game_profile.m_controllerProfile[i].reset(); - continue; - } - - const wxString profile_name = m_controller_profile[i]->GetStringSelection(); - if (profile_name.empty()) - m_game_profile.m_controllerProfile[i].reset(); - else - m_game_profile.m_controllerProfile[i] = profile_name.ToUTF8(); - } - - // update game profile file - m_game_profile.Save(m_title_id); -} - -void GameProfileWindow::SetSliderValue(wxSlider* slider, sint32 new_value) const -{ - wxASSERT(slider); - slider->SetValue(new_value); - - wxCommandEvent slider_event(wxEVT_SLIDER, slider->GetId()); - slider_event.SetEventObject(slider); - slider_event.SetClientData((void*)IsFrozen()); - wxPostEvent(slider->GetEventHandler(), slider_event); -} diff --git a/src/gui/wxgui/GameProfileWindow.h b/src/gui/wxgui/GameProfileWindow.h deleted file mode 100644 index fe6150dc..00000000 --- a/src/gui/wxgui/GameProfileWindow.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "Cafe/GameProfile/GameProfile.h" - -class GameProfileWindow : public wxFrame -{ -public: - GameProfileWindow(wxWindow* parent, uint64_t title_id); - ~GameProfileWindow(); - -private: - uint64_t m_title_id; - GameProfile m_game_profile; - - void OnStreamoutSizeChange(wxCommandEvent& event); - void OnControllerProfileDropdown(wxCommandEvent& event); - - void SetSliderValue(wxSlider* slider, sint32 new_value) const; - void SetProfileInt(gameProfileIntegerOption_t& option, wxCheckBox* checkbox, sint32 value) const; - - void ApplyProfile(); - void SaveProfile(); - - // general - wxCheckBox* m_load_libs, *m_start_with_padview; - - // cpu - wxChoice *m_cpu_mode; - wxChoice* m_thread_quantum; - - // gpu - //wxCheckBox* m_extended_texture_readback; - //wxChoice* m_precompiled; - wxChoice* m_graphic_api; - - wxChoice* m_shader_mul_accuracy; -#ifdef ENABLE_METAL - wxChoice* m_shader_fast_math; - wxChoice* m_metal_buffer_cache_mode; - wxChoice* m_position_invariance; -#endif - //wxChoice* m_cache_accuracy; - - // audio - //wxCheckBox* m_disable_audio; - - // controller - wxComboBox* m_controller_profile[8]; -}; diff --git a/src/gui/wxgui/GameUpdateWindow.cpp b/src/gui/wxgui/GameUpdateWindow.cpp deleted file mode 100644 index 253a7610..00000000 --- a/src/gui/wxgui/GameUpdateWindow.cpp +++ /dev/null @@ -1,339 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/GameUpdateWindow.h" -#include "util/helpers/helpers.h" -#include "util/helpers/SystemException.h" -#include "Cafe/TitleList/GameInfo.h" -#include "config/ActiveSettings.h" -#include "wxgui/helpers/wxHelpers.h" - -wxString _GetTitleIdTypeStr(TitleId titleId) -{ - TitleIdParser tip(titleId); - switch (tip.GetType()) - { - case TitleIdParser::TITLE_TYPE::AOC: - return _("DLC"); - case TitleIdParser::TITLE_TYPE::BASE_TITLE: - return _("Base game"); - case TitleIdParser::TITLE_TYPE::BASE_TITLE_DEMO: - return _("Demo"); - case TitleIdParser::TITLE_TYPE::SYSTEM_TITLE: - case TitleIdParser::TITLE_TYPE::SYSTEM_OVERLAY_TITLE: - return _("System title"); - case TitleIdParser::TITLE_TYPE::SYSTEM_DATA: - return _("System data title"); - case TitleIdParser::TITLE_TYPE::BASE_TITLE_UPDATE: - return _("Update"); - default: - break; - } - return "Unknown"; -} - -bool GameUpdateWindow::ParseUpdate(const fs::path& metaPath) -{ - m_title_info = TitleInfo(metaPath); - if (!m_title_info.IsValid()) - return false; - fs::path target_location = ActiveSettings::GetMlcPath(m_title_info.GetInstallPath()); - std::error_code ec; - if (fs::exists(target_location, ec)) - { - try - { - const TitleInfo tmp(target_location); - if (!tmp.IsValid()) - { - // does not exist / is not valid. We allow to overwrite it - } - else - { - TitleIdParser tip(m_title_info.GetAppTitleId()); - TitleIdParser tipOther(tmp.GetAppTitleId()); - - if (tip.GetType() != tipOther.GetType()) - { - auto typeStrToInstall = _GetTitleIdTypeStr(m_title_info.GetAppTitleId()); - auto typeStrCurrentlyInstalled = _GetTitleIdTypeStr(tmp.GetAppTitleId()); - - auto wxMsg = _("It seems that there is already a title installed at the target location but it has a different type.\nCurrently installed: \'{}\' Installing: \'{}\'\n\nThis can happen for titles which were installed with very old Cemu versions.\nDo you still want to continue with the installation? It will replace the currently installed title."); - wxMessageDialog dialog(this, formatWxString(wxMsg, typeStrCurrentlyInstalled, typeStrToInstall), _("Warning"), wxCENTRE | wxYES_NO | wxICON_EXCLAMATION); - if (dialog.ShowModal() != wxID_YES) - return false; - } - else if (tmp.GetAppTitleVersion() == m_title_info.GetAppTitleVersion()) - { - wxMessageDialog dialog(this, _("It seems that the selected title is already installed, do you want to reinstall it?"), _("Warning"), wxCENTRE | wxYES_NO); - if (dialog.ShowModal() != wxID_YES) - return false; - } - else if (tmp.GetAppTitleVersion() > m_title_info.GetAppTitleVersion()) - { - wxMessageDialog dialog(this, _("It seems that a newer version is already installed, do you still want to install the older version?"), _("Warning"), wxCENTRE | wxYES_NO); - if (dialog.ShowModal() != wxID_YES) - return false; - } - } - - // temp rename until done - m_backup_folder = target_location; - m_backup_folder.replace_extension(".backup"); - - std::error_code ec; - while (fs::exists(m_backup_folder, ec) || ec) - { - fs::remove_all(m_backup_folder, ec); - - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to move former title installation:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return false; - } - - // wait so filesystem doesnt - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - fs::rename(target_location, m_backup_folder); - } - catch (const std::exception& ex) - { - cemuLog_log(LogType::Force, "GameUpdateWindow::ParseUpdate exist-error: {} at {}", ex.what(), _pathToUtf8(target_location)); - } - } - - m_target_path = target_location; - - fs::path source(metaPath); - - m_source_paths = - { - fs::path(source).append("content"), - fs::path(source).append("code"), - fs::path(source).append("meta") - }; - - m_required_size = 0; - for (auto& path : m_source_paths) - { - for (const fs::directory_entry& f : fs::recursive_directory_iterator(path)) - { - if (is_regular_file(f.path())) - m_required_size += file_size(f.path()); - } - } - - const fs::space_info targetSpace = fs::space(ActiveSettings::GetMlcPath()); - if (targetSpace.free <= m_required_size) - { - auto string = formatWxString(_("Not enough space available.\nRequired: {0} MB\nAvailable: {1} MB"), (m_required_size / 1024 / 1024), (targetSpace.free / 1024 / 1024)); - throw std::runtime_error(string.utf8_string()); - } - - return true; -} - -GameUpdateWindow::GameUpdateWindow(wxWindow& parent, const fs::path& filePath) - : wxDialog(&parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), - m_thread_state(ThreadRunning) -{ - try - { - #if BOOST_OS_WINDOWS - SetLastError(0); - #endif - if(!ParseUpdate(filePath)) - throw AbortException(); - } - catch (const std::runtime_error& ex) - { - throw SystemException(ex); - } - - auto sizer = new wxBoxSizer(wxVERTICAL); - - TitleIdParser tip(GetTitleId()); - - if (tip.GetType() == TitleIdParser::TITLE_TYPE::AOC) - SetTitle(_("Installing DLC...")); - else if (tip.GetType() == TitleIdParser::TITLE_TYPE::BASE_TITLE_UPDATE) - SetTitle(_("Installing update...")); - else if (tip.IsSystemTitle()) - SetTitle(_("Installing system title...")); - else - SetTitle(_("Installing title...")); - - m_processBar = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(500, 20), wxGA_HORIZONTAL); - m_processBar->SetValue(0); - m_processBar->SetRange((sint32)(m_required_size / 1000)); - sizer->Add(m_processBar, 0, wxALL | wxEXPAND, 5); - - wxButton* m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &GameUpdateWindow::OnCancelButton, this); - sizer->Add(m_cancelButton, 0, wxALIGN_RIGHT | wxALL, 5); - - this->SetSizer(sizer); - this->Centre(wxBOTH); - - wxWindowBase::Layout(); - wxWindowBase::Fit(); - - m_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &GameUpdateWindow::OnUpdate, this); - this->Bind(wxEVT_CLOSE_WINDOW, &GameUpdateWindow::OnClose, this); - m_timer->Start(250); - - m_thread_state = ThreadRunning; - m_thread = std::thread(&GameUpdateWindow::ThreadWork, this); -} - -void GameUpdateWindow::ThreadWork() -{ - fs::directory_entry currentDirEntry; - try - { - // create base directories - for (auto& path : m_source_paths) - { - if (!path.has_stem()) - continue; - - fs::path targetDir = fs::path(m_target_path) / path.stem(); - create_directories(targetDir); - } - - for (auto& path : m_source_paths) - { - if (m_thread_state == ThreadCanceled) - break; - - if (!path.has_parent_path()) - continue; - - const auto len = path.parent_path().string().size() + 1; - for (const fs::directory_entry& f : fs::recursive_directory_iterator {path}) - { - if (m_thread_state == ThreadCanceled) - break; - - currentDirEntry = f; - fs::path relative(f.path().string().substr(len)); - fs::path target = fs::path(m_target_path) / relative; - if (is_directory(f)) - { - create_directories(target); - continue; - } - - copy(f, target, fs::copy_options::overwrite_existing); - if (is_regular_file(f.path())) - { - m_processed_size += file_size(f.path()); - } - } - } - } - catch (const std::exception& ex) - { - std::stringstream error_msg; - error_msg << GetSystemErrorMessage(ex); - - if(currentDirEntry != fs::directory_entry{}) - error_msg << fmt::format("\n{}\n{}",_("Current file:").utf8_string(), _pathToUtf8(currentDirEntry.path())); - - m_thread_exception = error_msg.str(); - m_thread_state = ThreadCanceled; - } - - if (m_thread_state == ThreadCanceled) - { - if(fs::exists(m_target_path)) - fs::remove_all(m_target_path); - } - else - m_thread_state = ThreadFinished; -} - -GameUpdateWindow::~GameUpdateWindow() -{ - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); -} - -int GameUpdateWindow::ShowModal() -{ - wxDialog::ShowModal(); - return m_thread_state == ThreadCanceled ? wxID_CANCEL : wxID_OK; -} - -void GameUpdateWindow::OnClose(wxCloseEvent& event) -{ - if (m_thread_state == ThreadRunning) - { - wxMessageDialog dialog(this, _("Do you really want to cancel the installation process?\n\nCanceling the process will delete the applied files."), _("Info"), wxCENTRE | wxYES_NO); - if (dialog.ShowModal() != wxID_YES) - return; - - m_thread_state = ThreadCanceled; - } - - m_timer->Stop(); - if (m_thread.joinable()) - m_thread.join(); - - if(!m_backup_folder.empty()) - { - if(m_thread_state == ThreadCanceled) - { - // restore backup - try - { - if(fs::exists(m_target_path)) - fs::remove_all(m_target_path); - - fs::rename(m_backup_folder, m_target_path); - } - catch (const std::exception& ex) - { - cemuLog_logDebug(LogType::Force, "can't restore update backup: {}",ex.what()); - } - } - else - { - // delete backup - try - { - if(fs::exists(m_backup_folder)) - fs::remove_all(m_backup_folder); - } - catch (const std::exception& ex) - { - cemuLog_logDebug(LogType::Force, "can't delete update backup: {}",ex.what()); - } - } - - m_backup_folder.clear(); - } - - event.Skip(); -} - -void GameUpdateWindow::OnUpdate(const wxTimerEvent& event) -{ - if (m_thread_state != ThreadRunning) - { - Close(); - return; - } - - const auto processedSize = (sint32)(m_processed_size / 1000); - if (m_processBar->GetValue() != processedSize) - m_processBar->SetValue(processedSize); -} - -void GameUpdateWindow::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} diff --git a/src/gui/wxgui/GameUpdateWindow.h b/src/gui/wxgui/GameUpdateWindow.h deleted file mode 100644 index 508e6465..00000000 --- a/src/gui/wxgui/GameUpdateWindow.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "Cafe/TitleList/GameInfo.h" - -#include -#include -#include - -#include -#include -#include -#include - -// thrown if users doesn't wish to reinstall update/dlc -class AbortException : public std::exception {}; - -class GameUpdateWindow : public wxDialog -{ -public: - - GameUpdateWindow(wxWindow& parent, const fs::path& metaPath); - ~GameUpdateWindow(); - - uint64 GetTitleId() const { return m_title_info.GetAppTitleId(); } - bool HasException() const { return !m_thread_exception.empty(); } - //bool IsDLC() const { return m_game_info->IsDLC(); } - //bool IsUpdate() const { return m_game_info->IsUpdate(); } - const std::string& GetExceptionMessage() const { return m_thread_exception; } - const std::string GetGameName() const { return m_title_info.GetMetaTitleName(); } - uint32 GetTargetVersion() const { return m_title_info.GetAppTitleVersion(); } - fs::path GetTargetPath() const { return fs::path(m_target_path); } - - int ShowModal() override; - void OnClose(wxCloseEvent& event); - - void OnUpdate(const wxTimerEvent& event); - void OnCancelButton(const wxCommandEvent& event); - - //uint64 GetUpdateTitleId() const { return m_title_info->GetUpdateTitleId(); } - //uint64 GetDLCTitleId() const { return m_game_info->GetDLCTitleId(); } - -private: - //std::unique_ptr m_game_info; - TitleInfo m_title_info; - enum ThreadState_t - { - ThreadRunning, - ThreadCanceled, - ThreadFinished, - }; - - uint64_t m_required_size; - fs::path m_target_path; - std::array m_source_paths; - bool ParseUpdate(const fs::path& metaPath); - - std::atomic m_processed_size = 0; - std::atomic m_thread_state; - std::string m_thread_exception; - std::thread m_thread; - void ThreadWork(); - - fs::path m_backup_folder; // for prev update data - - wxGauge* m_processBar; - wxTimer* m_timer; -}; diff --git a/src/gui/wxgui/GeneralSettings2.cpp b/src/gui/wxgui/GeneralSettings2.cpp deleted file mode 100644 index 0dd8c768..00000000 --- a/src/gui/wxgui/GeneralSettings2.cpp +++ /dev/null @@ -1,2509 +0,0 @@ -#include "wxCemuConfig.h" -#include "wxgui/wxgui.h" -#include "wxgui/GeneralSettings2.h" -#include "wxgui/CemuApp.h" -#include "wxgui/helpers/wxControlObject.h" - -#include "util/helpers/helpers.h" - -#include "Cafe/OS/libs/snd_core/ax.h" - -#include -#include -#include -#include -#include -#include - -#include "config/CemuConfig.h" -#include "config/NetworkSettings.h" - -#include "audio/IAudioAPI.h" -#if HAS_DIRECTX_AUDIO -#include "audio/DirectSoundAPI.h" -#include "audio/XAudio27API.h" -#endif -#include "audio/CubebAPI.h" - -#include "audio/IAudioInputAPI.h" - -#ifdef ENABLE_VULKAN -#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" -#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h" -#endif -#ifdef ENABLE_METAL -#include "Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h" -#endif -#include "Cafe/Account/Account.h" - -#include -#include "util/helpers/SystemException.h" -#include "wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h" - -#if BOOST_OS_WINDOWS -#include -#endif - -#include "config/LaunchSettings.h" -#include "config/ActiveSettings.h" -#include "wxgui/helpers/wxHelpers.h" - -#include "resource/embedded/resources.h" - -#include "Cafe/CafeSystem.h" -#include "Cemu/ncrypto/ncrypto.h" -#include "Cafe/TitleList/TitleList.h" -#include "wxHelper.h" - -#include "util/ScreenSaver/ScreenSaver.h" - -const wxString kDirectSound("DirectSound"); -const wxString kXAudio27("XAudio2.7"); -const wxString kXAudio2("XAudio2"); -const wxString kCubeb("Cubeb"); - -const wxString kPropertyPersistentId("PersistentId"); -const wxString kPropertyMiiName("MiiName"); -const wxString kPropertyBirthday("Birthday"); -const wxString kPropertyGender("Gender"); -const wxString kPropertyEmail("Email"); -const wxString kPropertyCountry("Country"); - -wxDEFINE_EVENT(wxEVT_ACCOUNTLIST_REFRESH, wxCommandEvent); - -class wxDeviceDescription : public wxClientData -{ -public: - wxDeviceDescription(const IAudioAPI::DeviceDescriptionPtr& description) : m_description(description) {} - const IAudioAPI::DeviceDescriptionPtr& GetDescription() const { return m_description; } -private: - IAudioAPI::DeviceDescriptionPtr m_description; -}; - -class wxInputDeviceDescription : public wxClientData -{ -public: - wxInputDeviceDescription(const IAudioInputAPI::DeviceDescriptionPtr& description) : m_description(description) {} - const IAudioInputAPI::DeviceDescriptionPtr& GetDescription() const { return m_description; } -private: - IAudioInputAPI::DeviceDescriptionPtr m_description; -}; - -#ifdef ENABLE_VULKAN -class wxVulkanUUID : public wxClientData -{ -public: - wxVulkanUUID(const VulkanRenderer::DeviceInfo& info) - : m_device_info(info) {} - const VulkanRenderer::DeviceInfo& GetDeviceInfo() const { return m_device_info; } - -private: - VulkanRenderer::DeviceInfo m_device_info; -}; -#endif - -#ifdef ENABLE_METAL -class wxMetalUUID : public wxClientData -{ -public: - wxMetalUUID(const MetalRenderer::DeviceInfo& info) - : m_device_info(info) {} - const MetalRenderer::DeviceInfo& GetDeviceInfo() const { return m_device_info; } - -private: - MetalRenderer::DeviceInfo m_device_info; -}; -#endif - -class wxAccountData : public wxClientData -{ -public: - wxAccountData(const Account& account) - : m_account(account) {} - - Account& GetAccount() { return m_account; } - const Account& GetAccount() const { return m_account; } - -private: - Account m_account; -}; - -wxPanel* GeneralSettings2::AddGeneralPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook); - auto* general_panel_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* box = new wxStaticBox(panel, wxID_ANY, _("Interface")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - { - auto* first_row = new wxFlexGridSizer(0, 2, 0, 0); - first_row->SetFlexibleDirection(wxBOTH); - first_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - first_row->Add(new wxStaticText(box, wxID_ANY, _("Language")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxString language_choices[] = { _("Default") }; - m_language = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(language_choices), language_choices); - m_language->SetSelection(0); - m_language->SetToolTip(_("Changes the interface language of Cemu\nAvailable languages are stored in the translation directory\nA restart will be required after changing the language")); - for (const auto& language : wxGetApp().GetLanguages()) - { - m_language->Append(language->DescriptionNative); - } - - first_row->Add(m_language, 0, wxALL | wxEXPAND, 5); - - box_sizer->Add(first_row, 1, wxEXPAND, 5); - } - -#if BOOST_OS_WINDOWS - { - auto* second_row = new wxFlexGridSizer(0, 2, 0, 0); - second_row->SetFlexibleDirection(wxBOTH); - second_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - second_row->Add(new wxStaticText(box, wxID_ANY, _("Theme")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_msw_theme = new wxChoice(box, wxID_ANY); - m_msw_theme->SetToolTip(_("Changes the Windows theme used by Cemu\nThis only works on Windows 10 and later\nA restart will be required for any changes to take effect")); - - m_msw_theme->AppendString(_("Follow Windows theme")); - m_msw_theme->AppendString(_("Light Theme")); - m_msw_theme->AppendString(_("Dark Theme")); - m_msw_theme->SetSelection(0); - - second_row->Add(m_msw_theme, 0, wxALL, 5); - - box_sizer->Add(second_row, 0, wxEXPAND, 5); - } -#endif - - { - auto* third_row = new wxFlexGridSizer(0, 3, 0, 0); - third_row->SetFlexibleDirection(wxBOTH); - third_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - sint32 checkboxCount = 0; - auto CountRowElement = [&]() - { - checkboxCount++; - if(checkboxCount != 2) - return; - third_row->AddSpacer(10); - checkboxCount = 0; - }; - - auto InsertEmptyRow = [&]() - { - while(checkboxCount != 0) - CountRowElement(); - third_row->AddSpacer(10); - third_row->AddSpacer(10); - third_row->AddSpacer(10); - }; - - const int topflag = wxALIGN_CENTER_VERTICAL | wxALL; - m_save_window_position_size = new wxCheckBox(box, wxID_ANY, _("Remember main window position")); - m_save_window_position_size->SetToolTip(_("Restores the last known window position and size when starting Cemu")); - third_row->Add(m_save_window_position_size, 0, topflag, 5); - CountRowElement(); - //third_row->AddSpacer(10); - m_save_padwindow_position_size = new wxCheckBox(box, wxID_ANY, _("Remember pad window position")); - m_save_padwindow_position_size->SetToolTip(_("Restores the last known pad window position and size when opening it")); - third_row->Add(m_save_padwindow_position_size, 0, topflag, 5); - CountRowElement(); - - const int botflag = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT | wxBOTTOM; - m_discord_presence = new wxCheckBox(box, wxID_ANY, _("Discord Presence")); - m_discord_presence->SetToolTip(_("Enables the Discord Rich Presence feature\nYou will also need to enable it in the Discord settings itself!")); - third_row->Add(m_discord_presence, 0, botflag, 5); - CountRowElement(); -#ifndef ENABLE_DISCORD_RPC - m_discord_presence->Disable(); -#endif - // third_row->AddSpacer(10); - m_fullscreen_menubar = new wxCheckBox(box, wxID_ANY, _("Fullscreen menu bar")); - m_fullscreen_menubar->SetToolTip(_("Displays the menu bar when Cemu is running in fullscreen mode and the mouse cursor is moved to the top")); - third_row->Add(m_fullscreen_menubar, 0, botflag, 5); - CountRowElement(); - - m_save_screenshot = new wxCheckBox(box, wxID_ANY, _("Save screenshot")); - m_save_screenshot->SetToolTip(_("Pressing the screenshot key will save a screenshot directly to the screenshots folder instead of to the clipboard")); - third_row->Add(m_save_screenshot, 0, botflag, 5); - CountRowElement(); - - m_disable_screensaver = new wxCheckBox(box, wxID_ANY, _("Disable screen saver")); - m_disable_screensaver->SetToolTip(_("Prevents the system from activating the screen saver or going to sleep while running a game.")); - third_row->Add(m_disable_screensaver, 0, botflag, 5); - CountRowElement(); - - // enable/disable feral interactive gamemode -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - m_feral_gamemode = new wxCheckBox(box, wxID_ANY, _("Enable Feral GameMode")); - m_feral_gamemode->SetToolTip(_("Use FeralInteractive GameMode if installed.")); - third_row->Add(m_feral_gamemode, 0, botflag, 5); - CountRowElement(); -#endif - - // temporary workaround because feature crashes on macOS -#if BOOST_OS_MACOS - m_disable_screensaver->Enable(false); -#endif - m_play_boot_sound = new wxCheckBox(box, wxID_ANY, _("Enable intro sound")); - m_play_boot_sound->SetToolTip(_("Play bootSound file while compiling shaders/pipelines.")); - third_row->Add(m_play_boot_sound, 0, botflag, 5); - CountRowElement(); - - m_auto_update = new wxCheckBox(box, wxID_ANY, _("Automatically check for updates")); - m_auto_update->SetToolTip(_("Automatically checks for new cemu versions on startup")); - third_row->Add(m_auto_update, 0, botflag, 5); - CountRowElement(); - - m_receive_untested_releases = new wxCheckBox(box, wxID_ANY, _("Receive untested updates")); - m_receive_untested_releases->SetToolTip(_("When checking for updates, include brand new and untested releases. These may contain bugs!")); - third_row->Add(m_receive_untested_releases, 0, botflag, 5); -#if BOOST_OS_LINUX - if (!std::getenv("APPIMAGE")) { - m_auto_update->Disable(); - } -#elif BOOST_OS_BSD // BSD users must update from source so disable auto updates - m_auto_update->Disable(); -#endif - - box_sizer->Add(third_row, 0, wxEXPAND, 5); - } - - general_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto* outerMlcBox = new wxStaticBox(panel, wxID_ANY, _("Custom MLC path")); - - auto* box_sizer_mlc = new wxStaticBoxSizer(outerMlcBox, wxVERTICAL); - box_sizer_mlc->Add(new wxStaticText(box_sizer_mlc->GetStaticBox(), wxID_ANY, _("You can configure a custom path for the emulated internal Wii U storage (MLC).\nThis is where Cemu stores saves, accounts and other Wii U system files.")), 0, wxALL, 5); - - auto* mlcPathLineSizer = new wxBoxSizer(wxHORIZONTAL); - - m_mlc_path = new wxTextCtrl(outerMlcBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY); - m_mlc_path->SetMinSize(wxSize(150, -1)); - m_mlc_path->SetToolTip(_("The mlc directory contains your save games and installed game update/dlc data")); - - mlcPathLineSizer->Add(m_mlc_path, 1, wxALL | wxEXPAND, 5); - - auto* changePath = new wxButton(outerMlcBox, wxID_ANY, "Change"); - changePath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathSelect, this); - mlcPathLineSizer->Add(changePath, 0, wxALL, 5); - if (LaunchSettings::GetMLCPath().has_value()) - changePath->Disable(); - - auto* clearPath = new wxButton(outerMlcBox, wxID_ANY, "Clear custom path"); - clearPath->Bind(wxEVT_BUTTON, &GeneralSettings2::OnMLCPathClear, this); - mlcPathLineSizer->Add(clearPath, 0, wxALL, 5); - if (LaunchSettings::GetMLCPath().has_value() || !ActiveSettings::IsCustomMlcPath()) - clearPath->Disable(); - - box_sizer_mlc->Add(mlcPathLineSizer, 0, wxEXPAND, 5); - general_panel_sizer->Add(box_sizer_mlc, 0, wxEXPAND | wxALL, 5); - } - - { - auto* general_gamepath_box = new wxStaticBox(panel, wxID_ANY, _("Game Paths")); - auto* general_gamepath_sizer = new wxStaticBoxSizer(general_gamepath_box, wxVERTICAL); - - m_game_paths = new wxListBox(general_gamepath_box, wxID_ANY); - m_game_paths->SetMinSize(wxSize(150, 70)); - m_game_paths->SetToolTip(_("Add the root directory of your game(s). It will scan all directories in it for games")); - general_gamepath_sizer->Add(m_game_paths, 1, wxALL | wxEXPAND, 5); - - auto* general_gamepath_buttons = new wxFlexGridSizer(0, 2, 0, 0); - general_gamepath_buttons->SetFlexibleDirection(wxBOTH); - general_gamepath_buttons->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - auto* general_gamepath_add_button = new wxButton(general_gamepath_box, wxID_ANY, _("Add")); - general_gamepath_add_button->Bind(wxEVT_BUTTON, &GeneralSettings2::OnAddPathClicked, this); - general_gamepath_add_button->SetToolTip(_("Adds a game path to scan for games displayed in the game list\nIf you have unpacked games, make sure to select the root folder of a game")); - general_gamepath_buttons->Add(general_gamepath_add_button, 0, wxALL, 5); - - auto* general_gamepath_remove_button = new wxButton(general_gamepath_box, wxID_ANY, _("Remove")); - general_gamepath_remove_button->Bind(wxEVT_BUTTON, &GeneralSettings2::OnRemovePathClicked, this); - general_gamepath_remove_button->SetToolTip(_("Removes the currently selected game path from the game list")); - general_gamepath_buttons->Add(general_gamepath_remove_button, 0, wxALL, 5); - - general_gamepath_sizer->Add(general_gamepath_buttons, 0, wxEXPAND, 5); - general_panel_sizer->Add(general_gamepath_sizer, 1, wxEXPAND | wxALL, 5); - } - - panel->SetSizerAndFit(general_panel_sizer); - - return panel; -} - -wxPanel* GeneralSettings2::AddGraphicsPage(wxNotebook* notebook) -{ - // Graphics page - auto graphics_panel = new wxPanel(notebook); - auto graphics_panel_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto box = new wxStaticBox(graphics_panel, wxID_ANY, _("General")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto row = new wxFlexGridSizer(0, 2, 0, 0); - row->SetFlexibleDirection(wxBOTH); - row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - row->Add(new wxStaticText(box, wxID_ANY, _("Graphics API")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - sint32 api_size = 0; - wxString choices[size_t(GraphicAPI::COUNT)]; - -#ifdef ENABLE_OPENGL - choices[api_size++] = "OpenGL"; - m_api_map.push_back(GraphicAPI::kOpenGL); -#endif -#ifdef ENABLE_VULKAN - if (g_vulkan_available) - { - choices[api_size++] = "Vulkan"; - m_api_map.push_back(GraphicAPI::kVulkan); - } -#endif -#ifdef ENABLE_METAL - choices[api_size++] = "Metal"; - m_api_map.push_back(GraphicAPI::kMetal); -#endif - wxASSERT(api_size > 0); - - m_graphic_api = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, api_size, choices); - m_graphic_api->SetSelection(0); - if (api_size > 1) - m_graphic_api->SetToolTip(_("Select one of the available graphic back ends")); - row->Add(m_graphic_api, 0, wxALL, 5); - m_graphic_api->Bind(wxEVT_CHOICE, &GeneralSettings2::OnGraphicAPISelected, this); - - row->Add(new wxStaticText(box, wxID_ANY, _("Graphics Device")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_graphic_device = new wxChoice(box, wxID_ANY, wxDefaultPosition, { 230, -1 }, api_size, choices); - m_graphic_device->SetSelection(0); - m_graphic_device->SetToolTip(_("Select the used graphic device")); - row->Add(m_graphic_device, 0, wxALL, 5); - - row->Add(new wxStaticText(box, wxID_ANY, _("VSync")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_vsync = new wxChoice(box, wxID_ANY, wxDefaultPosition, { 230, -1 }); - m_vsync->SetToolTip(_("Controls the vsync state")); - m_vsync->Bind(wxEVT_CHOICE, [](wxCommandEvent& event) { - GetConfig().vsync = event.GetSelection(); - }); - row->Add(m_vsync, 0, wxALL, 5); - - box_sizer->Add(row, 0, wxEXPAND, 5); - - auto* graphic_misc_row = new wxFlexGridSizer(0, 2, 0, 0); - - m_async_compile = new wxCheckBox(box, wxID_ANY, _("Async shader compile")); - m_async_compile->SetToolTip(_("Enables async shader and pipeline compilation. Reduces stutter at the cost of objects not rendering for a short time.\nVulkan only")); - graphic_misc_row->Add(m_async_compile, 0, wxALL, 5); - - m_gx2drawdone_sync = new wxCheckBox(box, wxID_ANY, _("Full sync at GX2DrawDone()")); - m_gx2drawdone_sync->SetToolTip(_("If synchronization is requested by the game, the emulated CPU will wait for the GPU to finish all operations.\nThis is more accurate behavior, but may cause lower performance")); - graphic_misc_row->Add(m_gx2drawdone_sync, 0, wxALL, 5); - -#ifdef ENABLE_METAL - m_force_mesh_shaders = new wxCheckBox(box, wxID_ANY, _("Force mesh shaders")); - m_force_mesh_shaders->SetToolTip(_("Force mesh shaders on all GPUs that support them. Mesh shaders are disabled by default on Intel GPUs due to potential stability issues.\nMetal only")); - graphic_misc_row->Add(m_force_mesh_shaders, 0, wxALL, 5); -#endif - - box_sizer->Add(graphic_misc_row, 1, wxEXPAND, 5); - graphics_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto box = new wxStaticBox(graphics_panel, wxID_ANY, _("Gamma settings")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - auto row = new wxFlexGridSizer(0, 2, 0, 0); - row->SetFlexibleDirection(wxBOTH); - row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - auto targetGammaLabel = new wxStaticText(box, wxID_ANY, _("Target Gamma")); - row->Add(targetGammaLabel, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_overrideGammaValue = new wxSpinCtrlDouble(box, wxID_ANY, "2.2f", wxDefaultPosition, {230, -1}, wxSP_ARROW_KEYS, 0.1f, 4.0f, 2.2f, 0.1f); - row->Add(m_overrideGammaValue, 0, wxALL, 5); - auto targetGammaTooltip = _("The display gamma to reproduce\nIf you are unsure, set this to 2.2"); - targetGammaLabel->SetToolTip(targetGammaTooltip); - m_overrideGammaValue->SetToolTip(targetGammaTooltip); - m_overrideGammaValue->Bind(wxEVT_SPINCTRLDOUBLE, [](wxSpinDoubleEvent& event) { - GetConfig().overrideGammaValue = event.GetValue(); - }); - - - auto displayGammaLabel = new wxStaticText(box, wxID_ANY, _("Display Gamma")); - row->Add(displayGammaLabel, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - wxBoxSizer* srgbCheckBoxSizer = new wxBoxSizer(wxHORIZONTAL); - row->Add(srgbCheckBoxSizer); - m_userDisplayGamma = new wxSpinCtrlDouble(box, wxID_ANY, "2.2f", wxDefaultPosition, {230, -1}, wxSP_ARROW_KEYS, 0.1f, 4.0f, 2.2f, 0.1f); - - auto displayGammaTooltip = _("The gamma of your monitor\nIf you are unsure, set this to 2.2"); - m_userDisplayGamma->SetToolTip(displayGammaTooltip); - displayGammaLabel->SetToolTip(displayGammaTooltip); - m_userDisplayGamma->Bind(wxEVT_SPINCTRLDOUBLE, [](wxSpinDoubleEvent& event) { - GetConfig().userDisplayGamma = event.GetValue(); - }); - - m_userDisplayisSRGB = new wxCheckBox(box, wxID_ANY, "sRGB"); - m_userDisplayisSRGB->SetToolTip(_("Select this if Cemu is being displayed using a piecewise sRGB gamma curve.\n" - "This is typically not the case so you can probably leave this unchecked.\n" - "Exceptions include HDR displays (with HDR enabled), calibrated SDR displays with Windows 11's Auto Color Management enabled, " - "or when using a display profile with a VCGT tag that targets piecewise sRGB.\n" - "When this box is selected Cemu will compensate for the piecewise curve to approximate the pure gamma curve of a TV.\n" - "Colors will be more accurate, especially in dark scenes, but this may result in banding or crushed shadows, " - "so it is best if you display Cemu with pure gamma and do not use this setting.")); - m_userDisplayisSRGB->Bind(wxEVT_CHECKBOX, &GeneralSettings2::OnUserDisplaySRGBSelected, this); - - srgbCheckBoxSizer->Add(m_userDisplayGamma, 0, wxALL, 5); - srgbCheckBoxSizer->Add(m_userDisplayisSRGB, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - row->Add(new wxStaticText(box, wxID_ANY, _("Override Gamma")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_overrideGamma = new wxCheckBox(box, wxID_ANY, "", wxDefaultPosition, {230, -1}); - m_overrideGamma->SetToolTip(_("Ignore title's gamma preference")); - m_overrideGamma->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { - GetConfig().overrideAppGammaPreference = event.IsChecked(); - }); - row->Add(m_overrideGamma, 0, wxALL, 5); - - box_sizer->Add(row, 0, wxEXPAND, 5); - graphics_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - wxString choices[] = { _("Bilinear"), _("Bicubic"), _("Hermite"), _("Nearest Neighbor") }; - m_upscale_filter = new wxRadioBox(graphics_panel, wxID_ANY, _("Upscale filter"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 5, wxRA_SPECIFY_COLS); - m_upscale_filter->SetToolTip(_("Upscaling filters are used when the game resolution is smaller than the window size")); - m_upscale_filter->Bind(wxEVT_RADIOBOX, [](wxCommandEvent& event) { - GetConfig().upscale_filter = event.GetSelection(); - }); - graphics_panel_sizer->Add(m_upscale_filter, 0, wxALL | wxEXPAND, 5); - - m_downscale_filter = new wxRadioBox(graphics_panel, wxID_ANY, _("Downscale filter"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 5, wxRA_SPECIFY_COLS); - m_downscale_filter->SetToolTip(_("Downscaling filters are used when the game resolution is bigger than the window size")); - m_downscale_filter->Bind(wxEVT_RADIOBOX, [](wxCommandEvent& event) { - GetConfig().downscale_filter = event.GetSelection(); - }); - graphics_panel_sizer->Add(m_downscale_filter, 0, wxALL | wxEXPAND, 5); - } - - { - wxString choices[] = { _("Keep aspect ratio"), _("Stretch") }; - m_fullscreen_scaling = new wxRadioBox(graphics_panel, wxID_ANY, _("Fullscreen scaling"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 5, wxRA_SPECIFY_COLS); - m_fullscreen_scaling->SetToolTip(_("Controls the output aspect ratio when it doesn't match the ratio of the game")); - m_fullscreen_scaling->Bind(wxEVT_RADIOBOX, [](wxCommandEvent& event) { - GetConfig().fullscreen_scaling = event.GetSelection(); - }); - graphics_panel_sizer->Add(m_fullscreen_scaling, 0, wxALL | wxEXPAND, 5); - } - - graphics_panel->SetSizerAndFit(graphics_panel_sizer); - return graphics_panel; -} - -wxPanel* GeneralSettings2::AddAudioPage(wxNotebook* notebook) -{ - auto audio_panel = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto audio_panel_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto box = new wxStaticBox(audio_panel, wxID_ANY, _("General")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto audio_general_row = new wxFlexGridSizer(0, 3, 0, 0); - audio_general_row->SetFlexibleDirection(wxBOTH); - audio_general_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - audio_general_row->Add(new wxStaticText(box, wxID_ANY, _("API")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_audio_api = new wxChoice(box, wxID_ANY); - if (IAudioAPI::IsAudioAPIAvailable(IAudioAPI::DirectSound)) - m_audio_api->Append(kDirectSound); - if (IAudioAPI::IsAudioAPIAvailable(IAudioAPI::XAudio27)) - m_audio_api->Append(kXAudio27); - if (IAudioAPI::IsAudioAPIAvailable(IAudioAPI::XAudio2)) - m_audio_api->Append(kXAudio2); - if (IAudioAPI::IsAudioAPIAvailable(IAudioAPI::Cubeb)) - m_audio_api->Append(kCubeb); - - m_audio_api->SetSelection(0); - m_audio_api->SetToolTip(_("Select one of the available audio back ends")); - audio_general_row->Add(m_audio_api, 0, wxALL, 5); - - m_audio_api->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioAPISelected, this); - - audio_general_row->AddSpacer(0); - - audio_general_row->Add(new wxStaticText(box, wxID_ANY, _("Latency")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_audio_latency = new wxSlider(box, wxID_ANY, 2, 0, IAudioAPI::kBlockCount - 1, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL); - m_audio_latency->SetToolTip(_("Controls the amount of buffered audio data\nHigher values will create a delay in audio playback, but may avoid audio problems when emulation is too slow")); - audio_general_row->Add(m_audio_latency, 0, wxEXPAND | wxALL, 5); - auto latency_text = new wxStaticText(box, wxID_ANY, "24ms"); - audio_general_row->Add(latency_text, 0, wxALIGN_CENTER_VERTICAL | wxALL | wxALIGN_RIGHT, 5); - m_audio_latency->Bind(wxEVT_SLIDER, &GeneralSettings2::OnLatencySliderChanged, this, wxID_ANY, wxID_ANY, new wxControlObject(latency_text)); - m_audio_latency->Bind(wxEVT_SLIDER, &GeneralSettings2::OnAudioLatencyChanged, this); - - box_sizer->Add(audio_general_row, 1, wxEXPAND, 5); - audio_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - const wxString audio_channel_choices[] = { _("Mono"), _("Stereo") , _("Surround") }; - { - auto box = new wxStaticBox(audio_panel, wxID_ANY, _("TV")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto audio_tv_row = new wxFlexGridSizer(0, 3, 0, 0); - audio_tv_row->SetFlexibleDirection(wxBOTH); - audio_tv_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - audio_tv_row->Add(new wxStaticText(box, wxID_ANY, _("Device")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_tv_device = new wxChoice(box, wxID_ANY); - m_tv_device->SetMinSize(wxSize(300, -1)); - m_tv_device->SetToolTip(_("Select the active audio output device for Wii U TV")); - audio_tv_row->Add(m_tv_device, 0, wxEXPAND | wxALL, 5); - audio_tv_row->AddSpacer(0); - - m_tv_device->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioDeviceSelected, this); - - audio_tv_row->Add(new wxStaticText(box, wxID_ANY, _("Channels")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_tv_channels = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(audio_channel_choices), audio_channel_choices); - - m_tv_channels->SetSelection(1); // set default to stereo - m_tv_channels->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioChannelsSelected, this); - audio_tv_row->Add(m_tv_channels, 0, wxEXPAND | wxALL, 5); - audio_tv_row->AddSpacer(0); - - audio_tv_row->Add(new wxStaticText(box, wxID_ANY, _("Volume")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_tv_volume = new wxSlider(box, wxID_ANY, 100, 0, 100, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL); - audio_tv_row->Add(m_tv_volume, 0, wxEXPAND | wxALL, 5); - auto audio_tv_volume_text = new wxStaticText(box, wxID_ANY, "100%"); - audio_tv_row->Add(audio_tv_volume_text, 0, wxALIGN_CENTER_VERTICAL | wxALL | wxALIGN_RIGHT, 5); - - m_tv_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnSliderChangedPercent, this, wxID_ANY, wxID_ANY, new wxControlObject(audio_tv_volume_text)); - m_tv_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnVolumeChanged, this); - - box_sizer->Add(audio_tv_row, 1, wxEXPAND, 5); - audio_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto box = new wxStaticBox(audio_panel, wxID_ANY, _("Gamepad")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto audio_pad_row = new wxFlexGridSizer(0, 3, 0, 0); - audio_pad_row->SetFlexibleDirection(wxBOTH); - audio_pad_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - audio_pad_row->Add(new wxStaticText(box, wxID_ANY, _("Device")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_pad_device = new wxChoice(box, wxID_ANY); - m_pad_device->SetMinSize(wxSize(300, -1)); - m_pad_device->SetToolTip(_("Select the active audio output device for Wii U GamePad")); - audio_pad_row->Add(m_pad_device, 0, wxEXPAND | wxALL, 5); - audio_pad_row->AddSpacer(0); - - m_pad_device->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioDeviceSelected, this); - - const wxString audio_channel_drc_choices[] = { _("Stereo") }; // stereo for now only - - audio_pad_row->Add(new wxStaticText(box, wxID_ANY, _("Channels")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_pad_channels = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(audio_channel_drc_choices), audio_channel_drc_choices); - - m_pad_channels->SetSelection(0); // set default to stereo - - m_pad_channels->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioChannelsSelected, this); - audio_pad_row->Add(m_pad_channels, 0, wxEXPAND | wxALL, 5); - audio_pad_row->AddSpacer(0); - - audio_pad_row->Add(new wxStaticText(box, wxID_ANY, _("Volume")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_pad_volume = new wxSlider(box, wxID_ANY, 100, 0, 100); - audio_pad_row->Add(m_pad_volume, 0, wxEXPAND | wxALL, 5); - auto audio_pad_volume_text = new wxStaticText(box, wxID_ANY, "100%"); - audio_pad_row->Add(audio_pad_volume_text, 0, wxALIGN_CENTER_VERTICAL | wxALL | wxALIGN_RIGHT, 5); - - m_pad_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnSliderChangedPercent, this, wxID_ANY, wxID_ANY, new wxControlObject(audio_pad_volume_text)); - m_pad_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnVolumeChanged, this); - - box_sizer->Add(audio_pad_row, 1, wxEXPAND, 5); - audio_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto box = new wxStaticBox(audio_panel, wxID_ANY, _("Microphone (Experimental)")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto audio_input_row = new wxFlexGridSizer(0, 3, 0, 0); - audio_input_row->SetFlexibleDirection(wxBOTH); - audio_input_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - audio_input_row->Add(new wxStaticText(box, wxID_ANY, _("Device")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_input_device = new wxChoice(box, wxID_ANY); - m_input_device->SetMinSize(wxSize(300, -1)); - m_input_device->SetToolTip(_("Select the active audio input device for Wii U GamePad")); - audio_input_row->Add(m_input_device, 0, wxEXPAND | wxALL, 5); - audio_input_row->AddSpacer(0); - - m_input_device->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioDeviceSelected, this); - - const wxString audio_channel_drc_choices[] = { _("Mono") }; // mono for now only - - audio_input_row->Add(new wxStaticText(box, wxID_ANY, _("Channels")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_input_channels = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(audio_channel_drc_choices), audio_channel_drc_choices); - - m_input_channels->SetSelection(0); // set default to stereo - - m_input_channels->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioChannelsSelected, this); - audio_input_row->Add(m_input_channels, 0, wxEXPAND | wxALL, 5); - audio_input_row->AddSpacer(0); - - audio_input_row->Add(new wxStaticText(box, wxID_ANY, _("Volume")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_input_volume = new wxSlider(box, wxID_ANY, 100, 0, 100); - audio_input_row->Add(m_input_volume, 0, wxEXPAND | wxALL, 5); - auto audio_input_volume_text = new wxStaticText(box, wxID_ANY, "100%"); - audio_input_row->Add(audio_input_volume_text, 0, wxALIGN_CENTER_VERTICAL | wxALL | wxALIGN_RIGHT, 5); - - m_input_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnSliderChangedPercent, this, wxID_ANY, wxID_ANY, new wxControlObject(audio_input_volume_text)); - m_input_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnVolumeChanged, this); - - box_sizer->Add(audio_input_row, 1, wxEXPAND, 5); - audio_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto box = new wxStaticBox(audio_panel, wxID_ANY, _("Trap Team Portal")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto portal_audio_row = new wxFlexGridSizer(0, 3, 0, 0); - portal_audio_row->SetFlexibleDirection(wxBOTH); - portal_audio_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - portal_audio_row->Add(new wxStaticText(box, wxID_ANY, _("Device")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_portal_device = new wxChoice(box, wxID_ANY, wxDefaultPosition); - m_portal_device->SetMinSize(wxSize(300, -1)); - m_portal_device->SetToolTip(_("Select the active audio output device for Wii U GamePad")); - portal_audio_row->Add(m_portal_device, 0, wxEXPAND | wxALL, 5); - portal_audio_row->AddSpacer(0); - - m_portal_device->Bind(wxEVT_CHOICE, &GeneralSettings2::OnAudioDeviceSelected, this); - - portal_audio_row->Add(new wxStaticText(box, wxID_ANY, _("Volume")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_portal_volume = new wxSlider(box, wxID_ANY, 100, 0, 100); - portal_audio_row->Add(m_portal_volume, 0, wxEXPAND | wxALL, 5); - auto audio_pad_volume_text = new wxStaticText(box, wxID_ANY, "100%"); - portal_audio_row->Add(audio_pad_volume_text, 0, wxALIGN_CENTER_VERTICAL | wxALL | wxALIGN_RIGHT, 5); - - m_portal_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnSliderChangedPercent, this, wxID_ANY, wxID_ANY, new wxControlObject(audio_pad_volume_text)); - m_portal_volume->Bind(wxEVT_SLIDER, &GeneralSettings2::OnVolumeChanged, this); - - box_sizer->Add(portal_audio_row, 1, wxEXPAND, 5); - audio_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - audio_panel->SetSizerAndFit(audio_panel_sizer); - return audio_panel; -} - -wxPanel* GeneralSettings2::AddOverlayPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* panel_sizer = new wxBoxSizer(wxVERTICAL); - - const wxString positions[]{ _("Disabled"), _("Top left"), _("Top center"), _("Top right"), _("Bottom left"), _("Bottom center"), _("Bottom right") }; - const wxString text_scale[]{ "50%", "75%", "100%", "125%", "150%", "175%", "200%", "225%", "250%", "275%", "300%" }; - { - auto box = new wxStaticBox(panel, wxID_ANY, _("Overlay")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto position_row = new wxFlexGridSizer(1, 0, 0, 0); - position_row->SetFlexibleDirection(wxBOTH); - position_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - { - position_row->Add(new wxStaticText(box, wxID_ANY, _("Position")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_overlay_position = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(positions), positions); - m_overlay_position->SetSelection(0); - m_overlay_position->SetToolTip(_("Controls the overlay which displays technical information while playing")); - position_row->Add(m_overlay_position, 0, wxALL, 5); - - position_row->AddSpacer(25); - - position_row->Add(new wxStaticText(box, wxID_ANY, _("Text Color")), 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5); - m_overlay_font_color = new wxColourPickerCtrl(box, wxID_ANY, *wxWHITE, wxDefaultPosition, wxDefaultSize, wxCLRP_SHOW_ALPHA); - m_overlay_font_color->SetToolTip(_("Sets the text color of the overlay")); - position_row->Add(m_overlay_font_color, 0, wxEXPAND | wxALL, 5); - - position_row->AddSpacer(25); - - position_row->Add(new wxStaticText(box, wxID_ANY, _("Scale")), 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5); - m_overlay_scale = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(text_scale), text_scale); - m_overlay_scale->SetToolTip(_("Sets the scale of the overlay text")); - position_row->Add(m_overlay_scale, 0, wxEXPAND | wxALL, 5); - } - box_sizer->Add(position_row, 0, wxEXPAND, 5); - - auto settings2_row = new wxFlexGridSizer(0, 4, 2, 0); - { - m_overlay_fps = new wxCheckBox(box, wxID_ANY, _("FPS")); - m_overlay_fps->SetToolTip(_("The number of frames per second. Average over last 5 seconds")); - settings2_row->Add(m_overlay_fps, 0, wxALL, 5); - - m_overlay_drawcalls = new wxCheckBox(box, wxID_ANY, _("Draw calls per frame")); - m_overlay_drawcalls->SetToolTip(_("The number of draw calls per frame. Average over last 5 seconds")); - settings2_row->Add(m_overlay_drawcalls, 0, wxALL, 5); - - m_overlay_cpu = new wxCheckBox(box, wxID_ANY, _("CPU usage")); - m_overlay_cpu->SetToolTip(_("CPU usage of Cemu in percent")); - settings2_row->Add(m_overlay_cpu, 0, wxALL, 5); - - m_overlay_cpu_per_core = new wxCheckBox(box, wxID_ANY, _("CPU per core usage")); - m_overlay_cpu_per_core->SetToolTip(_("Total cpu usage in percent for each core")); - settings2_row->Add(m_overlay_cpu_per_core, 0, wxALL, 5); - - m_overlay_ram = new wxCheckBox(box, wxID_ANY, _("RAM usage")); - m_overlay_ram->SetToolTip(_("Cemu RAM usage in MB")); - settings2_row->Add(m_overlay_ram, 0, wxALL, 5); - - m_overlay_vram = new wxCheckBox(box, wxID_ANY, _("VRAM usage")); -#if BOOST_OS_WINDOWS - using RtlGetVersion_t = LONG(WINAPI*)(PRTL_OSVERSIONINFOW lpVersionInformation); - const auto pRtlGetVersion = (RtlGetVersion_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlGetVersion"); - //if(IsWindows8Point1OrGreater()) requires manifest - RTL_OSVERSIONINFOW info{}; - // Windows 8.1 6.3* - if (pRtlGetVersion && pRtlGetVersion(&info) == 0 && ((info.dwMajorVersion == 6 && info.dwMinorVersion >= 3) || info.dwMajorVersion > 6)) - m_overlay_vram->SetToolTip(_("The VRAM usage of Cemu in MB")); - else - { - m_overlay_vram->SetToolTip(_("This option requires Win8.1+")); - m_overlay_vram->Disable(); - } -#else - m_overlay_vram->SetToolTip(_("The VRAM usage of Cemu in MB")); -#endif - - settings2_row->Add(m_overlay_vram, 0, wxALL, 5); - - m_overlay_debug = new wxCheckBox(box, wxID_ANY, _("Debug")); - m_overlay_debug->SetToolTip(_("Displays internal debug information (Vulkan only)")); - settings2_row->Add(m_overlay_debug, 0, wxALL, 5); - } - box_sizer->Add(settings2_row, 0, wxEXPAND, 5); - - panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto box = new wxStaticBox(panel, wxID_ANY, _("Notifications")); - auto box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto position_row = new wxFlexGridSizer(1, 0, 0, 0); - position_row->SetFlexibleDirection(wxBOTH); - position_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - { - position_row->Add(new wxStaticText(box, wxID_ANY, _("Position")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_notification_position = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(positions), positions); - m_notification_position->SetSelection(0); - m_notification_position->SetToolTip(_("Controls the notification position while playing")); - position_row->Add(m_notification_position, 0, wxALL, 5); - - position_row->AddSpacer(25); - - position_row->Add(new wxStaticText(box, wxID_ANY, _("Text Color")), 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5); - m_notification_font_color = new wxColourPickerCtrl(box, wxID_ANY, *wxWHITE, wxDefaultPosition, wxDefaultSize, wxCLRP_SHOW_ALPHA); - m_notification_font_color->SetToolTip(_("Sets the text color of notifications")); - position_row->Add(m_notification_font_color, 0, wxEXPAND | wxALL, 5); - - position_row->Add(new wxStaticText(box, wxID_ANY, _("Scale")), 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5); - m_notification_scale = new wxChoice(box, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(text_scale), text_scale); - m_notification_scale->SetToolTip(_("Sets the scale of the notification text")); - position_row->Add(m_notification_scale, 0, wxEXPAND | wxALL, 5); - } - box_sizer->Add(position_row, 0, wxEXPAND, 5); - - auto settings1_row = new wxFlexGridSizer(1, 0, 2, 0); - { - m_controller_profile_name = new wxCheckBox(box, wxID_ANY, _("Controller profiles")); - m_controller_profile_name->SetToolTip(_("Displays the active controller profile when starting a game")); - settings1_row->Add(m_controller_profile_name, 0, wxALL, 5); - - m_controller_low_battery = new wxCheckBox(box, wxID_ANY, _("Low battery")); - m_controller_low_battery->SetToolTip(_("Shows a notification when a low controller battery has been detected")); - settings1_row->Add(m_controller_low_battery, 0, wxALL, 5); - - m_shader_compiling = new wxCheckBox(box, wxID_ANY, _("Shader compiler")); - m_shader_compiling->SetToolTip(_("Shows a notification after shaders have been compiled")); - settings1_row->Add(m_shader_compiling, 0, wxALL, 5); - - m_friends_data = new wxCheckBox(box, wxID_ANY, _("Friend list")); - m_friends_data->SetToolTip(_("Shows friend list related data if online")); - settings1_row->Add(m_friends_data, 0, wxALL, 5); - } - box_sizer->Add(settings1_row, 0, wxEXPAND, 5); - - - panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - panel->SetSizerAndFit(panel_sizer); - - return panel; -} - -wxPanel* GeneralSettings2::AddAccountPage(wxNotebook* notebook) -{ - auto* online_panel = new wxPanel(notebook); - auto* online_panel_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* box = new wxStaticBox(online_panel, wxID_ANY, _("Account settings")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* content = new wxFlexGridSizer(0, 4, 0, 0); - content->SetFlexibleDirection(wxBOTH); - content->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - content->AddGrowableCol(1, 1); - content->AddGrowableCol(2, 0); - content->AddGrowableCol(3, 0); - - content->Add(new wxStaticText(box, wxID_ANY, _("Active account")), 1, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_active_account = new wxChoice(box, wxID_ANY); - m_active_account->SetMinSize({ 250, -1 }); - content->Add(m_active_account, 0, wxEXPAND | wxALL, 5); - m_active_account->Bind(wxEVT_CHOICE, &GeneralSettings2::OnActiveAccountChanged, this); - - m_create_account = new wxButton(box, wxID_ANY, _("Create")); - content->Add(m_create_account, 0, wxEXPAND | wxALL | wxALIGN_RIGHT, 5); - m_create_account->Bind(wxEVT_BUTTON, &GeneralSettings2::OnAccountCreate, this); - - m_delete_account = new wxButton(box, wxID_ANY, _("Delete")); - content->Add(m_delete_account, 0, wxEXPAND | wxALL | wxALIGN_RIGHT, 5); - m_delete_account->Bind(wxEVT_BUTTON, &GeneralSettings2::OnAccountDelete, this); - - box_sizer->Add(content, 1, wxEXPAND, 5); - - online_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - - if (CafeSystem::IsTitleRunning()) - { - m_active_account->Enable(false); - m_create_account->Enable(false); - m_delete_account->Enable(false); - } - } - - - { - wxString choices[] = { _("Offline"), _("Nintendo"), _("Pretendo"), _("Custom") }; - m_active_service = new wxRadioBox(online_panel, wxID_ANY, _("Network Service"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 4, wxRA_SPECIFY_COLS); - if (!NetworkConfig::XMLExists()) - m_active_service->Enable(3, false); - - m_active_service->SetItemToolTip(0, _("Online functionality disabled for this account")); - m_active_service->SetItemToolTip(1, _("Connect to the official Nintendo Network Service")); - m_active_service->SetItemToolTip(2, _("Connect to the Pretendo Network Service")); - m_active_service->SetItemToolTip(3, _("Connect to a custom Network Service (configured via network_services.xml)")); - - m_active_service->Bind(wxEVT_RADIOBOX, &GeneralSettings2::OnAccountServiceChanged,this); - online_panel_sizer->Add(m_active_service, 0, wxEXPAND | wxALL, 5); - - if (CafeSystem::IsTitleRunning()) - { - m_active_service->Enable(false); - } - } - - { - auto* box = new wxStaticBox(online_panel, wxID_ANY, _("Online play requirements")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* row = new wxFlexGridSizer(0, 2, 0, 0); - row->SetFlexibleDirection(wxBOTH); - row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - const wxImage tmp = wxBITMAP_PNG_FROM_DATA(PNG_ERROR).ConvertToImage(); - m_validate_online = new wxBitmapButton(box, wxID_ANY, tmp.Scale(16, 16)); - m_validate_online->Bind(wxEVT_BUTTON, &GeneralSettings2::OnShowOnlineValidator, this); - row->Add(m_validate_online, 0, wxEXPAND | wxALL, 5); - - m_online_status = new wxStaticText(box, wxID_ANY, _("No account selected")); - row->Add(m_online_status, 1, wxALL | wxALIGN_CENTRE_VERTICAL, 5); - - box_sizer->Add(row, 1, wxEXPAND, 5); - - auto* tutorial_link = new wxHyperlinkCtrl(box, wxID_ANY, _("Online play tutorial"), "https://cemu.info/online-guide"); - box_sizer->Add(tutorial_link, 0, wxALL, 5); - - - online_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - m_account_information = new wxCollapsiblePane(online_panel, wxID_ANY, _("Account information")); - auto win = m_account_information->GetPane(); - - auto content = new wxBoxSizer(wxVERTICAL); - - m_account_grid = new wxPropertyGrid(win, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxPG_HIDE_MARGIN | wxPG_STATIC_SPLITTER); - m_account_grid->SetExtraStyle(wxPG_EX_HELP_AS_TOOLTIPS); - m_account_grid->SetMinSize({ 300, -1 }); - //m_account_grid->Append(new wxPropertyCategory("Main")); - - auto* persistent_id_gprop = m_account_grid->Append(new wxStringProperty("PersistentId", kPropertyPersistentId)); - persistent_id_gprop->SetHelpString(_("The persistent id is the internal folder name used for your saves")); - m_account_grid->SetPropertyReadOnly(persistent_id_gprop); - - m_account_grid->Append(new wxStringProperty(_("Mii name"), kPropertyMiiName))->SetHelpString(_("The mii name is the profile name")); - m_account_grid->Append(new wxStringProperty(_("Birthday"), kPropertyBirthday)); - - wxPGChoices gender; - gender.Add(_("Female"), 0); - gender.Add(_("Male"), 1); - m_account_grid->Append(new wxEnumProperty(_("Gender"), kPropertyGender, gender)); - - m_account_grid->Append(new wxStringProperty(_("Email"), kPropertyEmail)); - - wxPGChoices countries; - for (int i = 0; i < NCrypto::GetCountryCount(); ++i) - { - const auto country = NCrypto::GetCountryAsString(i); - if (country && (i == 0 || !boost::equals(country, "NN"))) - { - countries.Add(country, i); - } - } - m_account_grid->Append(new wxEnumProperty(_("Country"), kPropertyCountry, countries)); - - m_account_grid->Bind(wxEVT_PG_CHANGED, &GeneralSettings2::OnAccountSettingsChanged, this); - - content->Add(m_account_grid, 1, wxEXPAND | wxALL, 5); - - win->SetSizer(content); - content->SetSizeHints(win); - - online_panel_sizer->Add(m_account_information, 0, wxEXPAND | wxALL, 5); - } - - online_panel->SetSizerAndFit(online_panel_sizer); - return online_panel; -} - -wxPanel* GeneralSettings2::AddDebugPage(wxNotebook* notebook) -{ - auto* panel = new wxPanel(notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* debug_panel_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); - debug_row->SetFlexibleDirection(wxBOTH); - debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - debug_row->Add(new wxStaticText(panel, wxID_ANY, _("Crash dump")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - -#if BOOST_OS_WINDOWS - wxString dump_choices[] = {_("Disabled"), _("Lite"), _("Full")}; -#elif BOOST_OS_UNIX - wxString dump_choices[] = {_("Disabled"), _("Enabled")}; -#endif - m_crash_dump = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(dump_choices), dump_choices); - m_crash_dump->SetSelection(0); -#if BOOST_OS_WINDOWS - m_crash_dump->SetToolTip(_("Creates a dump when Cemu crashes\nOnly enable when requested by a developer!\nThe Full option will create a very large dump file (includes a full RAM dump of the Cemu process)")); -#elif BOOST_OS_UNIX - m_crash_dump->SetToolTip(_("Creates a core dump when Cemu crashes\nOnly enable when requested by a developer!")); -#endif - debug_row->Add(m_crash_dump, 0, wxALL | wxEXPAND, 5); - debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); - } - - { - auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); - debug_row->SetFlexibleDirection(wxBOTH); - debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - debug_row->Add(new wxStaticText(panel, wxID_ANY, _("GDB Stub port")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_gdb_port = new wxSpinCtrl(panel, wxID_ANY, "1337", wxDefaultPosition, wxDefaultSize, 0, 1000, 65535); - m_gdb_port->SetToolTip(_("Changes the port that the GDB stub will use, which you can use by either starting Cemu with the --enable-gdbstub option or by enabling it the Debug tab.")); - - debug_row->Add(m_gdb_port, 0, wxALL | wxEXPAND, 5); - debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); - } - -#ifdef ENABLE_METAL - { - auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); - debug_row->SetFlexibleDirection(wxBOTH); - debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - debug_row->Add(new wxStaticText(panel, wxID_ANY, _("GPU capture save directory"), wxDefaultPosition, wxDefaultSize, 0), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_gpu_capture_dir = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_DONTWRAP); - m_gpu_capture_dir->SetMinSize(wxSize(150, -1)); - m_gpu_capture_dir->SetToolTip(_("Cemu will save the GPU captures done by selecting Debug -> GPU capture (Metal) in the menu bar in this directory. If a debugger with support for GPU captures (like Xcode) is attached, the capture will be opened in that debugger instead. If such debugger is not attached, METAL_CAPTURE_ENABLED must be set to 1 as an environment variable.")); - - debug_row->Add(m_gpu_capture_dir, 0, wxALL | wxEXPAND, 5); - debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); - } - - { - auto* debug_row = new wxFlexGridSizer(0, 2, 0, 0); - debug_row->SetFlexibleDirection(wxBOTH); - debug_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - m_framebuffer_fetch = new wxCheckBox(panel, wxID_ANY, _("Framebuffer fetch")); - m_framebuffer_fetch->SetToolTip(_("Enable framebuffer fetch for eligible textures on supported devices.\nMetal only")); - - debug_row->Add(m_framebuffer_fetch, 0, wxALL | wxEXPAND, 5); - debug_panel_sizer->Add(debug_row, 0, wxALL | wxEXPAND, 5); - } -#endif - - panel->SetSizerAndFit(debug_panel_sizer); - - return panel; -} - -GeneralSettings2::GeneralSettings2(wxWindow* parent, bool game_launched) - : wxDialog(parent, wxID_ANY, _("General settings"), wxDefaultPosition, wxDefaultSize, wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER), m_game_launched(game_launched) -{ - SetIcon(wxICON(X_SETTINGS)); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - auto* notebook = new wxNotebook(this, wxID_ANY); - - notebook->AddPage(AddGeneralPage(notebook), _("General")); - notebook->AddPage(AddGraphicsPage(notebook), _("Graphics")); - notebook->AddPage(AddAudioPage(notebook), _("Audio")); - notebook->AddPage(AddOverlayPage(notebook), _("Overlay")); - notebook->AddPage(AddAccountPage(notebook), _("Account")); - notebook->AddPage(AddDebugPage(notebook), _("Debug")); - - Bind(wxEVT_CLOSE_WINDOW, &GeneralSettings2::OnClose, this); - - // - - sizer->Add(notebook, 1, wxEXPAND | wxALL, 5); - - SetSizerAndFit(sizer); - Layout(); - Centre(wxBOTH); - - // - - UpdateOnlineAccounts(); - UpdateAudioDeviceList(); - - ApplyConfig(); - HandleGraphicsApiSelection(); - - DisableSettings(game_launched); -} - -uint32 GeneralSettings2::GetSelectedAccountPersistentId() -{ - const auto active_account = m_active_account->GetSelection(); - if (active_account == wxNOT_FOUND) - return GetConfig().account.m_persistent_id.GetInitValue(); - return dynamic_cast(m_active_account->GetClientObject(active_account))->GetAccount().GetPersistentId(); -} - -void GeneralSettings2::StoreConfig() -{ - auto& app = wxGetApp(); - auto& config = GetConfig(); - auto& wxGuiConfig = GetWxGUIConfig(); - - wxGuiConfig.use_discord_presence = m_discord_presence->IsChecked(); - wxGuiConfig.fullscreen_menubar = m_fullscreen_menubar->IsChecked(); - wxGuiConfig.check_update = m_auto_update->IsChecked(); - wxGuiConfig.save_screenshot = m_save_screenshot->IsChecked(); - wxGuiConfig.receive_untested_updates = m_receive_untested_releases->IsChecked(); -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - wxGuiConfig.feral_gamemode = m_feral_gamemode->IsChecked(); -#endif -#if BOOST_OS_WINDOWS - wxGuiConfig.msw_theme = m_msw_theme->GetSelection(); -#endif - config.play_boot_sound = m_play_boot_sound->IsChecked(); - config.disable_screensaver = m_disable_screensaver->IsChecked(); - // toggle while a game is running - if (CafeSystem::IsTitleRunning()) - { - ScreenSaver::SetInhibit(config.disable_screensaver); - } - - // -1 is default wx widget value -> set to dummy 0 so mainwindow and padwindow will update it - wxGuiConfig.window_position = m_save_window_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; - wxGuiConfig.window_size = m_save_window_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; - wxGuiConfig.pad_position = m_save_padwindow_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; - wxGuiConfig.pad_size = m_save_padwindow_position_size->IsChecked() ? Vector2i{ 0,0 } : Vector2i{-1,-1}; - - config.game_paths.clear(); - for (auto& path : m_game_paths->GetStrings()) - config.game_paths.emplace_back(path.utf8_string()); - - auto selection = m_language->GetSelection(); - if (selection == 0) - wxGuiConfig.language = wxLANGUAGE_DEFAULT; - else - { - const auto language = m_language->GetStringSelection(); - for (const auto& lang : app.GetLanguages()) - { - if (lang->DescriptionNative == language) - { - wxGuiConfig.language = lang->Language; - break; - } - } - } - - // audio - if (m_audio_api->GetStringSelection() == kDirectSound) - config.audio_api = IAudioAPI::DirectSound; - else if (m_audio_api->GetStringSelection() == kXAudio27) - config.audio_api = IAudioAPI::XAudio27; - else if (m_audio_api->GetStringSelection() == kXAudio2) - config.audio_api = IAudioAPI::XAudio2; - else if (m_audio_api->GetStringSelection() == kCubeb) - config.audio_api = IAudioAPI::Cubeb; - - config.audio_delay = m_audio_latency->GetValue(); - config.tv_channels = (AudioChannels)m_tv_channels->GetSelection(); - //config.pad_channels = (AudioChannels)m_pad_channels->GetSelection(); - config.pad_channels = kStereo; // (AudioChannels)m_pad_channels->GetSelection(); - //config.input_channels = (AudioChannels)m_input_channels->GetSelection(); - config.input_channels = kMono; // (AudioChannels)m_input_channels->GetSelection(); - - config.tv_volume = m_tv_volume->GetValue(); - config.pad_volume = m_pad_volume->GetValue(); - config.input_volume = m_input_volume->GetValue(); - config.portal_volume = m_portal_volume->GetValue(); - - config.tv_device.clear(); - const auto tv_device = m_tv_device->GetSelection(); - if (tv_device != wxNOT_FOUND && tv_device != 0 && m_tv_device->HasClientObjectData()) - { - const auto* device_description = (wxDeviceDescription*)m_tv_device->GetClientObject(tv_device); - if(device_description) - config.tv_device = device_description->GetDescription()->GetIdentifier(); - } - - config.pad_device.clear(); - const auto pad_device = m_pad_device->GetSelection(); - if (pad_device != wxNOT_FOUND && pad_device != 0 && m_pad_device->HasClientObjectData()) - { - const auto* device_description = (wxDeviceDescription*)m_pad_device->GetClientObject(pad_device); - if (device_description) - config.pad_device = device_description->GetDescription()->GetIdentifier(); - } - - config.input_device = L""; - const auto input_device = m_input_device->GetSelection(); - if (input_device != wxNOT_FOUND && input_device != 0 && m_input_device->HasClientObjectData()) - { - const auto* device_description = (wxDeviceDescription*)m_input_device->GetClientObject(input_device); - if (device_description) - config.input_device = device_description->GetDescription()->GetIdentifier(); - } - - config.portal_device.clear(); - const auto portal_device = m_portal_device->GetSelection(); - if (portal_device != wxNOT_FOUND && portal_device != 0 && m_portal_device->HasClientObjectData()) - { - const auto* device_description = (wxDeviceDescription*)m_portal_device->GetClientObject(portal_device); - if (device_description) - config.portal_device = device_description->GetDescription()->GetIdentifier(); - } - - // graphics - config.graphic_api = m_api_map[m_graphic_api->GetSelection()]; - - if (!CafeSystem::IsTitleRunning()) - { - selection = m_graphic_device->GetSelection(); -#ifdef ENABLE_VULKAN - if (config.graphic_api == GraphicAPI::kVulkan) - { - if (selection != wxNOT_FOUND) - { - const auto* info = (wxVulkanUUID*)m_graphic_device->GetClientObject(selection); - if (info) - config.vk_graphic_device_uuid = info->GetDeviceInfo().uuid; - else - config.vk_graphic_device_uuid = {}; - } - else - config.vk_graphic_device_uuid = {}; - } -#endif -#ifdef ENABLE_METAL - if (config.graphic_api == GraphicAPI::kMetal) - { - if (selection != wxNOT_FOUND) - { - const auto* info = (wxMetalUUID*)m_graphic_device->GetClientObject(selection); - if (info) - config.mtl_graphic_device_uuid = info->GetDeviceInfo().uuid; - else - config.mtl_graphic_device_uuid = {}; - } - else - config.mtl_graphic_device_uuid = {}; - } -#endif - } - - - config.gx2drawdone_sync = m_gx2drawdone_sync->IsChecked(); -#ifdef ENABLE_METAL - config.force_mesh_shaders = m_force_mesh_shaders->IsChecked(); -#endif - config.async_compile = m_async_compile->IsChecked(); - - config.overlay.position = (ScreenPosition)m_overlay_position->GetSelection(); wxASSERT((int)config.overlay.position <= (int)ScreenPosition::kBottomRight); - config.overlay.text_color = m_overlay_font_color->GetColour().GetRGBA(); - config.overlay.text_scale = m_overlay_scale->GetSelection() * 25 + 50; - - config.overlay.fps = m_overlay_fps->GetValue(); - config.overlay.drawcalls = m_overlay_drawcalls->GetValue(); - config.overlay.cpu_usage = m_overlay_cpu->GetValue(); - config.overlay.cpu_per_core_usage = m_overlay_cpu_per_core->GetValue(); - config.overlay.ram_usage = m_overlay_ram->GetValue(); - config.overlay.vram_usage = m_overlay_vram->GetValue(); - config.overlay.debug = m_overlay_debug->GetValue(); - - config.notification.position = (ScreenPosition)m_notification_position->GetSelection(); wxASSERT((int)config.notification.position <= (int)ScreenPosition::kBottomRight); - config.notification.text_color = m_notification_font_color->GetColour().GetRGBA(); - config.notification.text_scale = m_notification_scale->GetSelection() * 25 + 50; - config.notification.controller_profiles = m_controller_profile_name->GetValue(); - config.notification.controller_battery = m_controller_low_battery->GetValue(); - config.notification.shader_compiling = m_shader_compiling->GetValue(); - config.notification.friends = m_friends_data->GetValue(); - - // account - config.account.m_persistent_id = GetSelectedAccountPersistentId(); - - // debug - config.crash_dump = (CrashDump)m_crash_dump->GetSelection(); - config.gdb_port = m_gdb_port->GetValue(); -#ifdef ENABLE_METAL - config.gpu_capture_dir = m_gpu_capture_dir->GetValue().utf8_string(); - config.framebuffer_fetch = m_framebuffer_fetch->IsChecked(); -#endif - - GetConfigHandle().Save(); -} - -GeneralSettings2::~GeneralSettings2() -{ - Unbind(wxEVT_CLOSE_WINDOW, &GeneralSettings2::OnClose, this); -} - -void GeneralSettings2::OnClose(wxCloseEvent& event) -{ - StoreConfig(); - if (m_has_account_change) - { - wxCommandEvent refresh_event(wxEVT_ACCOUNTLIST_REFRESH); - GetParent()->ProcessWindowEvent(refresh_event); - } - event.Skip(); -} - -void GeneralSettings2::ValidateConfig() -{ - GetConfigHandle().Load(); - - auto& data = GetConfigHandle().data(); - // todo - //data.fullscreen_scaling = min(max(data.fullscreen_scaling,)) -} - -void GeneralSettings2::DisableSettings(bool game_launched) -{ - if (game_launched) - { - m_graphic_api->Disable(); - m_graphic_api->SetToolTip(_("Graphics API cannot be changed while a title is running")); - m_graphic_device->Disable(); - m_graphic_device->SetToolTip(_("Graphics Device cannot be changed while a title is running")); - } -} - -void GeneralSettings2::OnAudioLatencyChanged(wxCommandEvent& event) -{ - IAudioAPI::s_audioDelay = event.GetInt(); - event.Skip(); -} - -void GeneralSettings2::OnVolumeChanged(wxCommandEvent& event) -{ - - if(event.GetEventObject() == m_input_volume) - { - std::shared_lock lock(g_audioInputMutex); - if (g_inputAudio) - g_inputAudio->SetVolume(event.GetInt()); - } - else - { - std::shared_lock lock(g_audioMutex); - if(event.GetEventObject() == m_pad_volume) - { - if (g_padAudio) - g_padAudio->SetVolume(event.GetInt()); - - g_padVolume = event.GetInt(); - } - else if (event.GetEventObject() == m_tv_volume) - { - if (g_tvAudio) - g_tvAudio->SetVolume(event.GetInt()); - } - else - { - if(g_portalAudio) - g_portalAudio->SetVolume(event.GetInt()); - } - } - - - event.Skip(); -} - -void GeneralSettings2::OnInputVolumeChanged(wxCommandEvent& event) -{ - std::shared_lock lock(g_audioMutex); - if (g_padAudio) - g_padAudio->SetInputVolume(event.GetInt()); - - event.Skip(); -} - -void GeneralSettings2::OnSliderChangedPercent(wxCommandEvent& event) -{ - const auto slider = dynamic_cast(event.GetEventObject()); - wxASSERT(slider); - - const auto control = dynamic_cast(event.GetEventUserData()); - wxASSERT(control); - - auto slider_text = control->GetControl(); - wxASSERT(slider_text); - - const auto value = event.GetInt(); - slider->SetValue(value); - slider_text->SetLabel(wxString::Format("%d%%", value)); - - event.Skip(); -} - -void GeneralSettings2::OnLatencySliderChanged(wxCommandEvent& event) -{ - const auto slider = dynamic_cast(event.GetEventObject()); - wxASSERT(slider); - - const auto control = dynamic_cast(event.GetEventUserData()); - wxASSERT(control); - - auto slider_text = control->GetControl(); - wxASSERT(slider_text); - - const auto value = event.GetInt(); - slider->SetValue(value); - slider_text->SetLabel(wxString::Format("%dms", value * 12)); - - event.Skip(); -} - -void GeneralSettings2::UpdateAudioDeviceList() -{ - m_tv_device->Clear(); - m_pad_device->Clear(); - m_input_device->Clear(); - m_portal_device->Clear(); - - m_tv_device->Append(_("Disabled")); - m_pad_device->Append(_("Disabled")); - m_input_device->Append(_("Disabled")); - m_portal_device->Append(_("Disabled")); - - const auto audio_api = (IAudioAPI::AudioAPI)GetConfig().audio_api; - const auto devices = IAudioAPI::GetDevices(audio_api); - for (auto& device : devices) - { - m_tv_device->Append(device->GetName(), new wxDeviceDescription(device)); - m_pad_device->Append(device->GetName(), new wxDeviceDescription(device)); - m_portal_device->Append(device->GetName(), new wxDeviceDescription(device)); - } - - const auto input_audio_api = IAudioInputAPI::Cubeb; //(IAudioAPI::AudioAPI)GetConfig().input_audio_api; - const auto input_devices = IAudioInputAPI::GetDevices(input_audio_api); - - for (auto& device : input_devices) - { - m_input_device->Append(device->GetName(), new wxInputDeviceDescription(device)); - } - - if(m_tv_device->GetCount() > 1) - m_tv_device->SetSelection(1); - else - m_tv_device->SetSelection(0); - - m_pad_device->SetSelection(0); - - m_input_device->SetSelection(0); - - m_portal_device->SetSelection(0); - - // todo reset global instance of audio device -} - -void GeneralSettings2::ResetAccountInformation() -{ - m_account_grid->SetSplitterPosition(100); - m_active_account->SetSelection(0); - - for(auto it = m_account_grid->GetIterator(); !it.AtEnd(); ++it) - { - (*it)->SetValueToUnspecified(); - } - - // refresh pane size - m_account_information->InvalidateBestSize(); - #if BOOST_OS_WINDOWS - m_account_information->OnStateChange(GetBestSize()); - #endif -} - -void GeneralSettings2::OnAccountCreate(wxCommandEvent& event) -{ - wxASSERT(Account::HasFreeAccountSlots()); - - wxCreateAccountDialog dialog(this); - if (dialog.ShowModal() == wxID_CANCEL) - return; - - Account account(dialog.GetPersistentId(), dialog.GetMiiName().ToStdWstring()); - account.Save(); - Account::RefreshAccounts(); - - const int index = m_active_account->Append(account.ToString(), new wxAccountData(account)); - - // update ui - m_active_account->SetSelection(index); - UpdateAccountInformation(); - - m_create_account->Enable(m_active_account->GetCount() < 0xC); - m_delete_account->Enable(m_active_account->GetCount() > 1); - - // send main window event - wxASSERT(GetParent()); - wxCommandEvent refresh_event(wxEVT_ACCOUNTLIST_REFRESH); - GetParent()->ProcessWindowEvent(refresh_event); -} - -void GeneralSettings2::OnAccountDelete(wxCommandEvent& event) -{ - if(m_active_account->GetCount() == 1) - { - wxMessageBox(_("Can't delete the only account!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - const auto selection = m_active_account->GetSelection(); - wxASSERT(selection != wxNOT_FOUND); - auto* obj = dynamic_cast(m_active_account->GetClientObject(selection)); - wxASSERT(obj); - auto& account = obj->GetAccount(); - - const std::wstring format_str = _("Are you sure you want to delete the account {} with id {:x}?").ToStdWstring(); - const std::wstring msg = fmt::format(fmt::runtime(format_str), - std::wstring{ account.GetMiiName() }, account.GetPersistentId()); - - const int answer = wxMessageBox(msg, _("Confirmation"), wxYES_NO | wxCENTRE | wxICON_QUESTION, this); - if (answer == wxNO) - return; - - // todo: ask if saves should be deleted too? - - const fs::path path = account.GetFileName(); - try - { - fs::remove_all(path.parent_path()); - m_active_account->Delete(selection); - m_active_account->SetSelection(0); - Account::RefreshAccounts(); - UpdateAccountInformation(); - - m_create_account->Enable(m_active_account->GetCount() < 0xC); - m_delete_account->Enable(m_active_account->GetCount() > 1); - } - catch(const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, sys.what()); - } - -} - -void GeneralSettings2::OnAccountSettingsChanged(wxPropertyGridEvent& event) -{ - wxPGProperty* property = event.GetProperty(); - if (!property) - return; - - const wxAny value = property->GetValue(); - if (value.IsNull()) - return; - - const auto selection = m_active_account->GetSelection(); - wxASSERT(selection != wxNOT_FOUND); - auto* obj = dynamic_cast(m_active_account->GetClientObject(selection)); - wxASSERT(obj); - auto& account = obj->GetAccount(); - - // TODO make id changeable to free ids + current it? - bool refresh_accounts = false; - if (property->GetName() == kPropertyMiiName) - { - std::wstring new_name = value.As().ToStdWstring(); - if (new_name.empty()) - new_name = L"default"; - - account.SetMiiName(new_name); - refresh_accounts = true; - } - else if (property->GetName() == kPropertyBirthday) - { - const std::string birthday = value.As().ToStdString(); - const boost::char_separator sep{ "-" }; - - std::vector tokens; - for (const auto& token : boost::tokenizer(birthday, sep)) - { - tokens.emplace_back(token); - } - - if (tokens.size() == 3) - { - account.SetBirthYear(ConvertString(tokens[0])); - account.SetBirthMonth(ConvertString(tokens[1])); - account.SetBirthDay(ConvertString(tokens[2])); - } - } - else if (property->GetName() == kPropertyGender) - { - account.SetGender(value.As()); - } - else if (property->GetName() == kPropertyEmail) - { - account.SetEmail(value.As().ToStdString()); - - } - else if (property->GetName() == kPropertyCountry) - { - account.SetCountry(value.As()); - } - else - cemu_assert_debug(false); - - account.Save(); - Account::RefreshAccounts(); // refresh internal account list - UpdateAccountInformation(); // refresh on invalid values - - if(refresh_accounts) - { - wxCommandEvent refresh_event(wxEVT_ACCOUNTLIST_REFRESH); - GetParent()->ProcessWindowEvent(refresh_event); - } -} - -void GeneralSettings2::UpdateAccountInformation() -{ - m_account_grid->SetSplitterPosition(100); - - const auto selection = m_active_account->GetSelection(); - if(selection == wxNOT_FOUND) - { - m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_ERROR).ConvertToImage().Scale(16, 16)); - m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() & ~wxBORDER_NONE); - ResetAccountInformation(); - m_online_status->SetLabel(_("No account selected")); - return; - } - - const auto* obj = dynamic_cast(m_active_account->GetClientObject(selection)); - wxASSERT(obj); - const auto& account = obj->GetAccount(); - - m_active_account->SetString(selection, account.ToString()); - - m_account_grid->GetProperty(kPropertyPersistentId)->SetValueFromString(fmt::format("{:x}", account.GetPersistentId())); - m_account_grid->GetProperty(kPropertyMiiName)->SetValueFromString(std::wstring{ account.GetMiiName() }); - m_account_grid->GetProperty(kPropertyBirthday)->SetValueFromString(fmt::format("{:04d}-{:02d}-{:02d}", account.GetBirthYear(), account.GetBirthMonth(), account.GetBirthDay())); - - const auto gender_property = m_account_grid->GetProperty(kPropertyGender); // gender 2 can be also female? - gender_property->SetChoiceSelection(std::min(gender_property->GetChoices().GetCount() - 1, (uint32)account.GetGender())); - - m_account_grid->GetProperty(kPropertyEmail)->SetValueFromString(std::string{ account.GetEmail() }); - - auto* country_property = dynamic_cast(m_account_grid->GetProperty(kPropertyCountry)); - wxASSERT(country_property); - int index = (country_property)->GetIndexForValue(account.GetCountry()); - if (index == wxNOT_FOUND) - index = 0; - country_property->SetChoiceSelection(index); - - const bool online_fully_valid = account.IsValidOnlineAccount() && ActiveSettings::HasRequiredOnlineFiles(); - if (ActiveSettings::HasRequiredOnlineFiles()) - { - if(account.IsValidOnlineAccount()) - m_online_status->SetLabel(_("Selected account is a valid online account")); - else - m_online_status->SetLabel(_("Selected account is not linked to a NNID or PNID")); - } - else - { - if(NCrypto::OTP_IsPresent() != NCrypto::SEEPROM_IsPresent()) - m_online_status->SetLabel(_("OTP.bin or SEEPROM.bin is missing")); - else if(NCrypto::OTP_IsPresent() && NCrypto::SEEPROM_IsPresent()) - m_online_status->SetLabel(_("OTP and SEEPROM present but no certificate files were found")); - else - m_online_status->SetLabel(_("Online play is not set up. Follow the guide below to get started")); - } - - if(online_fully_valid) - { - m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_CHECK_YES).ConvertToImage().Scale(16, 16)); - m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() | wxBORDER_NONE); - } - else - { - m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_ERROR).ConvertToImage().Scale(16, 16)); - m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() & ~wxBORDER_NONE); - } - - // enable/disable network service field depending on online requirements - m_active_service->Enable(online_fully_valid && !CafeSystem::IsTitleRunning()); - if(online_fully_valid) - { - NetworkService service = GetConfig().GetAccountNetworkService(account.GetPersistentId()); - m_active_service->SetSelection(static_cast(service)); - // set the config option here for the selected service - // this will guarantee that it's actually written to settings.xml - // allowing us to eventually get rid of the legacy option in the (far) future - GetConfig().SetAccountSelectedService(account.GetPersistentId(), service); - } - else - { - m_active_service->SetSelection(0); // force offline - } - wxString tmp = _("Network service"); - tmp.append(" ("); - tmp.append(wxString::FromUTF8(boost::nowide::narrow(account.GetMiiName()))); - tmp.append(")"); - m_active_service->SetLabel(tmp); - - // refresh pane size - m_account_grid->InvalidateBestSize(); - //m_account_grid->GetParent()->FitInside(); - //m_account_information->OnStateChange(GetBestSize()); idk.. -} - -void GeneralSettings2::UpdateOnlineAccounts() -{ - m_active_account->Clear(); - for(const auto& account : Account::GetAccounts()) - { - m_active_account->Append(fmt::format(L"{} ({:x})", std::wstring{ account.GetMiiName() }, account.GetPersistentId()), - new wxAccountData(account)); - } - - m_active_account->SetSelection(0); - m_create_account->Enable(m_active_account->GetCount() < 0xC); - m_delete_account->Enable(m_active_account->GetCount() > 1); - UpdateAccountInformation(); -} - -void GeneralSettings2::HandleGraphicsApiSelection() -{ - int selection = m_vsync->GetSelection(); - if(selection == wxNOT_FOUND) - selection = GetConfig().vsync; - - m_vsync->Clear(); - - auto api = m_api_map[m_graphic_api->GetSelection()]; - switch (api) - { -#ifdef ENABLE_OPENGL - case GraphicAPI::kOpenGL: - { - // OpenGL - m_vsync->AppendString(_("Off")); - m_vsync->AppendString(_("On")); - if (selection == 0) - m_vsync->Select(0); - else - m_vsync->Select(1); - - m_graphic_device->Clear(); - m_graphic_device->Disable(); - - m_gx2drawdone_sync->Enable(); - m_async_compile->Disable(); -#ifdef ENABLE_METAL - m_force_mesh_shaders->Disable(); -#endif - break; - } -#endif -#ifdef ENABLE_VULKAN - case GraphicAPI::kVulkan: - { - // Vulkan - m_gx2drawdone_sync->Disable(); - m_async_compile->Enable(); -#ifdef ENABLE_METAL - m_force_mesh_shaders->Disable(); -#endif - - m_vsync->AppendString(_("Off")); - m_vsync->AppendString(_("Double buffering")); - m_vsync->AppendString(_("Triple buffering")); -#if BOOST_OS_WINDOWS - m_vsync->AppendString(_("Match emulated display (Experimental)")); -#endif - - m_vsync->Select(selection); - - m_graphic_device->Enable(); - m_graphic_device->Clear(); - if (!CafeSystem::IsTitleRunning()) - { - auto devices = VulkanRenderer::GetDevices(); - if(!devices.empty()) - { - for(const auto& device : devices) - { - m_graphic_device->Append(device.name, new wxVulkanUUID(device)); - } - m_graphic_device->SetSelection(0); - - const auto& config = GetConfig(); - for(size_t i = 0; i < devices.size(); ++i) - { - if(config.vk_graphic_device_uuid == devices[i].uuid) - { - m_graphic_device->SetSelection(i); - break; - } - } - } - } - else - { - cemu_assert(g_renderer != nullptr); - m_graphic_device->Append(g_renderer->GetDeviceName()); - m_graphic_device->SetSelection(0); - } - break; - } - #endif -#ifdef ENABLE_METAL - case GraphicAPI::kMetal: - { - // Metal - m_gx2drawdone_sync->Disable(); - m_async_compile->Enable(); - m_force_mesh_shaders->Enable(); - - m_vsync->AppendString(_("Off")); - m_vsync->AppendString(_("On")); - - m_vsync->Select(selection); - - m_graphic_device->Enable(); - m_graphic_device->Clear(); - - if (!CafeSystem::IsTitleRunning()) - { - auto devices = MetalRenderer::GetDevices(); - if(!devices.empty()) - { - for (const auto& device : devices) - { - m_graphic_device->Append(device.name, new wxMetalUUID(device)); - } - m_graphic_device->SetSelection(0); - - const auto& config = GetConfig(); - for (size_t i = 0; i < devices.size(); ++i) - { - if (config.mtl_graphic_device_uuid == devices[i].uuid) - { - m_graphic_device->SetSelection(i); - break; - } - } - } - } - else - { - cemu_assert(g_renderer != nullptr); - m_graphic_device->Append(g_renderer->GetDeviceName()); - m_graphic_device->SetSelection(0); - } - break; - } -#endif - } -} - -void GeneralSettings2::ApplyConfig() -{ - ValidateConfig(); - auto& wxGUIconfig = GetWxGUIConfig(); - auto& config = GetConfig(); - - if (LaunchSettings::GetMLCPath().has_value()) - m_mlc_path->SetValue(wxHelper::FromPath(LaunchSettings::GetMLCPath().value())); - else - m_mlc_path->SetValue(wxString::FromUTF8(config.mlc_path.GetValue())); - - m_save_window_position_size->SetValue(wxGUIconfig.window_position != Vector2i{-1,-1}); - m_save_padwindow_position_size->SetValue(wxGUIconfig.pad_position != Vector2i{-1,-1}); - - m_discord_presence->SetValue(wxGUIconfig.use_discord_presence); - m_fullscreen_menubar->SetValue(wxGUIconfig.fullscreen_menubar); - - m_auto_update->SetValue(wxGUIconfig.check_update); - m_receive_untested_releases->SetValue(wxGUIconfig.receive_untested_updates); - m_save_screenshot->SetValue(wxGUIconfig.save_screenshot); - - m_disable_screensaver->SetValue(config.disable_screensaver); - m_play_boot_sound->SetValue(config.play_boot_sound); -#if BOOST_OS_WINDOWS - m_msw_theme->SetSelection(wxGUIconfig.msw_theme); -#endif -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - m_feral_gamemode->SetValue(wxGUIconfig.feral_gamemode); -#endif - // temporary workaround because feature crashes on macOS -#if BOOST_OS_MACOS - m_disable_screensaver->SetValue(false); -#endif - - m_game_paths->Clear(); - for (auto& path : config.game_paths) - { - m_game_paths->Append(wxString::FromUTF8(path)); - } - - const auto& app = wxGetApp(); - for (const auto& language : app.GetLanguages()) - { - if (wxGUIconfig.language == language->Language) - { - m_language->SetStringSelection(language->DescriptionNative); - break; - } - } - - // graphics - for (int i = 0; i < (int)m_api_map.size(); ++i) - { - if (m_api_map[i] == config.graphic_api) - { - m_graphic_api->SetSelection(i); - break; - } - } - m_vsync->SetSelection(config.vsync); - m_overrideGamma->SetValue(config.overrideAppGammaPreference); - m_overrideGammaValue->SetValue(config.overrideGammaValue); - m_userDisplayisSRGB->SetValue(config.userDisplayGamma == 0.0f); - m_userDisplayGamma->SetValue(config.userDisplayGamma); - if(m_userDisplayisSRGB->GetValue()) - { - m_userDisplayGamma->Disable(); - m_userDisplayGamma->SetValue(2.2f); - } - m_async_compile->SetValue(config.async_compile); - m_gx2drawdone_sync->SetValue(config.gx2drawdone_sync); -#ifdef ENABLE_METAL - m_force_mesh_shaders->SetValue(config.force_mesh_shaders); -#endif - m_upscale_filter->SetSelection(config.upscale_filter); - m_downscale_filter->SetSelection(config.downscale_filter); - m_fullscreen_scaling->SetSelection(config.fullscreen_scaling); - - wxASSERT((uint32)config.overlay.position < m_overlay_position->GetCount()); - m_overlay_position->SetSelection((int)config.overlay.position); - m_overlay_font_color->SetColour(wxColour((unsigned long)config.overlay.text_color)); - - uint32 selection = (config.overlay.text_scale - 50) / 25; - wxASSERT(selection < m_overlay_scale->GetCount()); - m_overlay_scale->SetSelection(selection); - - m_overlay_fps->SetValue(config.overlay.fps); - m_overlay_drawcalls->SetValue(config.overlay.drawcalls); - m_overlay_cpu->SetValue(config.overlay.cpu_usage); - m_overlay_cpu_per_core->SetValue(config.overlay.cpu_per_core_usage); - m_overlay_ram->SetValue(config.overlay.ram_usage); - m_overlay_vram->SetValue(config.overlay.vram_usage); - m_overlay_debug->SetValue(config.overlay.debug); - - wxASSERT((uint32)config.notification.position < m_notification_position->GetCount()); - m_notification_position->SetSelection((int)config.notification.position); - m_notification_font_color->SetColour(wxColour((unsigned long)config.notification.text_color)); - - selection = (config.notification.text_scale - 50) / 25; - wxASSERT(selection < m_notification_scale->GetCount()); - m_notification_scale->SetSelection(selection); - - m_controller_profile_name->SetValue(config.notification.controller_profiles); - m_controller_low_battery->SetValue(config.notification.controller_battery); - m_shader_compiling->SetValue(config.notification.shader_compiling); - m_friends_data->SetValue(config.notification.friends); - - // audio - if(config.audio_api == IAudioAPI::DirectSound) - m_audio_api->SetStringSelection(kDirectSound); - else if(config.audio_api == IAudioAPI::XAudio27) - m_audio_api->SetStringSelection(kXAudio27); - else if(config.audio_api == IAudioAPI::XAudio2) - m_audio_api->SetStringSelection(kXAudio2); - else if(config.audio_api == IAudioAPI::Cubeb) - m_audio_api->SetStringSelection(kCubeb); - - SendSliderEvent(m_audio_latency, config.audio_delay); - - m_tv_channels->SetSelection(config.tv_channels); - //m_pad_channels->SetSelection(config.pad_channels); - m_pad_channels->SetSelection(0); - //m_input_channels->SetSelection(config.pad_channels); - m_input_channels->SetSelection(0); - - SendSliderEvent(m_tv_volume, config.tv_volume); - - if (!config.tv_device.empty() && m_tv_device->HasClientObjectData()) - { - for(uint32 i = 0; i < m_tv_device->GetCount(); ++i) - { - const auto device_description = (wxDeviceDescription*)m_tv_device->GetClientObject(i); - if (device_description && config.tv_device == device_description->GetDescription()->GetIdentifier()) - { - m_tv_device->SetSelection(i); - break; - } - } - } - else - m_tv_device->SetSelection(0); - - SendSliderEvent(m_pad_volume, config.pad_volume); - if (!config.pad_device.empty() && m_pad_device->HasClientObjectData()) - { - for (uint32 i = 0; i < m_pad_device->GetCount(); ++i) - { - const auto device_description = (wxDeviceDescription*)m_pad_device->GetClientObject(i); - if (device_description && config.pad_device == device_description->GetDescription()->GetIdentifier()) - { - m_pad_device->SetSelection(i); - break; - } - } - } - else - m_pad_device->SetSelection(0); - - SendSliderEvent(m_input_volume, config.input_volume); - if (!config.input_device.empty() && m_input_device->HasClientObjectData()) - { - for (uint32 i = 0; i < m_input_device->GetCount(); ++i) - { - const auto device_description = (wxInputDeviceDescription*)m_input_device->GetClientObject(i); - if (device_description && config.input_device == device_description->GetDescription()->GetIdentifier()) - { - m_input_device->SetSelection(i); - break; - } - } - } - else - m_input_device->SetSelection(0); - - SendSliderEvent(m_portal_volume, config.portal_volume); - if (!config.portal_device.empty() && m_portal_device->HasClientObjectData()) - { - for (uint32 i = 0; i < m_portal_device->GetCount(); ++i) - { - const auto device_description = (wxDeviceDescription*)m_portal_device->GetClientObject(i); - if (device_description && config.portal_device == device_description->GetDescription()->GetIdentifier()) - { - m_portal_device->SetSelection(i); - break; - } - } - } - else - m_portal_device->SetSelection(0); - - // account - UpdateOnlineAccounts(); - m_active_account->SetSelection(0); - for(uint32 i = 0; i < m_active_account->GetCount(); ++i) - { - const auto* obj = dynamic_cast(m_active_account->GetClientObject(i)); - wxASSERT(obj); - if(obj->GetAccount().GetPersistentId() == ActiveSettings::GetPersistentId()) - { - m_active_account->SetSelection(i); - break; - } - } - m_active_service->SetSelection((int)config.GetAccountNetworkService(ActiveSettings::GetPersistentId())); - - UpdateAccountInformation(); - - // debug - m_crash_dump->SetSelection((int)config.crash_dump.GetValue()); - m_gdb_port->SetValue(config.gdb_port.GetValue()); -#ifdef ENABLE_METAL - m_gpu_capture_dir->SetValue(wxString::FromUTF8(config.gpu_capture_dir.GetValue())); - m_framebuffer_fetch->SetValue(config.framebuffer_fetch); -#endif -} - -void GeneralSettings2::OnAudioAPISelected(wxCommandEvent& event) -{ - IAudioAPI::AudioAPI api; - if (m_audio_api->GetStringSelection() == kDirectSound) - api = IAudioAPI::DirectSound; - else if (m_audio_api->GetStringSelection() == kXAudio27) - api = IAudioAPI::XAudio27; - else if (m_audio_api->GetStringSelection() == kXAudio2) - api = IAudioAPI::XAudio2; - else if (m_audio_api->GetStringSelection() == kCubeb) - api = IAudioAPI::Cubeb; - else - { - wxFAIL_MSG("invalid audio api selected!"); - return; - } - - GetConfig().audio_api = api; - UpdateAudioDeviceList(); - OnAudioDeviceSelected(event); -} - -#define AX_FRAMES_PER_GROUP 4 - -void GeneralSettings2::UpdateAudioDevice() -{ - auto& config = GetConfig(); - - std::unique_lock lock(g_audioMutex); - std::unique_lock inputLock(g_audioInputMutex); - - // tv audio device - { - const auto selection = m_tv_device->GetSelection(); - if (selection == wxNOT_FOUND) - { - cemu_assert_debug(false); - return; - } - - g_tvAudio.reset(); - - if (m_tv_device->HasClientObjectData()) - { - const auto description = (wxDeviceDescription*)m_tv_device->GetClientObject(selection); - if (description) - { - sint32 channels; - if (m_game_launched && g_tvAudio) - channels = g_tvAudio->GetChannels(); - else - channels = CemuConfig::AudioChannelsToNChannels(config.tv_channels); - - try - { - g_tvAudio = IAudioAPI::CreateDevice((IAudioAPI::AudioAPI)config.audio_api, description->GetDescription(), 48000, channels, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16); - g_tvAudio->SetVolume(m_tv_volume->GetValue()); - } - catch (std::runtime_error& ex) - { - cemuLog_log(LogType::Force, "can't initialize tv audio: {}", ex.what()); - } - } - } - } - - // pad audio device - { - const auto selection = m_pad_device->GetSelection(); - if (selection == wxNOT_FOUND) - { - cemu_assert_debug(false); - return; - } - - g_padAudio.reset(); - - if (m_pad_device->HasClientObjectData()) - { - const auto description = (wxDeviceDescription*)m_pad_device->GetClientObject(selection); - if (description) - { - sint32 channels; - if (m_game_launched && g_padAudio) - channels = g_padAudio->GetChannels(); - else - channels = CemuConfig::AudioChannelsToNChannels(config.pad_channels); - - g_padVolume = m_pad_volume->GetValue(); - - try - { - g_padAudio = IAudioAPI::CreateDevice((IAudioAPI::AudioAPI)config.audio_api, description->GetDescription(), 48000, channels, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16); - g_padAudio->SetVolume(m_pad_volume->GetValue()); - } - catch (std::runtime_error& ex) - { - cemuLog_log(LogType::Force, "can't initialize pad audio: {}", ex.what()); - } - } - } - } - - // input audio device - { - const auto selection = m_input_device->GetSelection(); - if (selection == wxNOT_FOUND) - { - cemu_assert_debug(false); - return; - } - - g_inputAudio.reset(); - - if (m_input_device->HasClientObjectData()) - { - const auto description = (wxInputDeviceDescription*)m_input_device->GetClientObject(selection); - if (description) - { - sint32 channels; - if (m_game_launched && g_inputAudio) - channels = g_inputAudio->GetChannels(); - else - channels = CemuConfig::AudioChannelsToNChannels(config.input_channels); - - try - { - g_inputAudio = IAudioInputAPI::CreateDevice(IAudioInputAPI::AudioInputAPI::Cubeb, description->GetDescription(), 32000, channels, snd_core::AX_SAMPLES_PER_3MS_32KHZ, 16); - g_inputAudio->SetVolume(m_input_volume->GetValue()); - } - catch (std::runtime_error& ex) - { - cemuLog_log(LogType::Force, "can't initialize pad audio: {}", ex.what()); - } - } - } - } - - // skylander portal audio device - { - const auto selection = m_portal_device->GetSelection(); - if (selection == wxNOT_FOUND) - { - cemu_assert_debug(false); - return; - } - - g_portalAudio.reset(); - - if (m_portal_device->HasClientObjectData()) - { - const auto description = (wxDeviceDescription*)m_portal_device->GetClientObject(selection); - if (description) - { - sint32 channels; - if (m_game_launched && g_portalAudio) - channels = g_portalAudio->GetChannels(); - else - channels = 1; - - try - { - g_portalAudio = IAudioAPI::CreateDevice((IAudioAPI::AudioAPI)config.audio_api, description->GetDescription(), 8000, 1, 32, 16); - g_portalAudio->SetVolume(m_portal_volume->GetValue()); - } - catch (std::runtime_error& ex) - { - cemuLog_log(LogType::Force, "can't initialize portal audio: {}", ex.what()); - } - } - } - - } -} - -void GeneralSettings2::OnAudioDeviceSelected(wxCommandEvent& event) -{ - UpdateAudioDevice(); -} - -void GeneralSettings2::OnAudioChannelsSelected(wxCommandEvent& event) -{ - const auto obj = wxDynamicCast(event.GetEventObject(), wxChoice); - wxASSERT(obj); - if (obj->GetSelection() == wxNOT_FOUND) - return; - - auto& config = GetConfig(); - if (obj == m_tv_channels) - { - if (config.tv_channels == (AudioChannels)obj->GetSelection()) - return; - - config.tv_channels = (AudioChannels)obj->GetSelection(); - } - else if (obj == m_pad_channels) - { - if (config.pad_channels == (AudioChannels)obj->GetSelection()) - return; - - config.pad_channels = (AudioChannels)obj->GetSelection(); - } - else - cemu_assert_debug(false); - - if(m_game_launched) - wxMessageBox(_("You have to restart the game in order to apply the new settings."), _("Information"), wxOK | wxCENTRE, this); - else - UpdateAudioDevice(); -} - -void GeneralSettings2::OnGraphicAPISelected(wxCommandEvent& event) -{ - cemu_assert_debug(!CafeSystem::IsTitleRunning()); - HandleGraphicsApiSelection(); -} - -void GeneralSettings2::OnUserDisplaySRGBSelected(wxCommandEvent& event) -{ - m_userDisplayGamma->SetValue(2.2f); - if(event.IsChecked()) - m_userDisplayGamma->Disable(); - else - m_userDisplayGamma->Enable(); - auto& config = GetConfig(); - config.userDisplayGamma = m_userDisplayGamma->GetValue() * !event.IsChecked(); -} - -void GeneralSettings2::OnAddPathClicked(wxCommandEvent& event) -{ - wxDirDialog path_dialog(this, _("Select a directory containing games."), wxEmptyString, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) - return; - - const auto path = path_dialog.GetPath(); - // test if already included - for (auto& s : m_game_paths->GetStrings()) - { - if (s == path) - return; - } - - m_game_paths->Append(path); - m_reload_gamelist = true; - - // trigger title list rescan with new path configuration - CafeTitleList::ClearScanPaths(); - for (auto& it : m_game_paths->GetStrings()) - CafeTitleList::AddScanPath(wxHelper::MakeFSPath(it)); - CafeTitleList::Refresh(); -} - -void GeneralSettings2::OnRemovePathClicked(wxCommandEvent& event) -{ - const auto selection = m_game_paths->GetSelection(); - if (selection == wxNOT_FOUND) - return; - - m_game_paths->Delete(selection); - m_reload_gamelist = true; - // trigger title list rescan with new path configuration - CafeTitleList::ClearScanPaths(); - for (auto& it : m_game_paths->GetStrings()) - CafeTitleList::AddScanPath(wxHelper::MakeFSPath(it)); - CafeTitleList::Refresh(); -} - -void GeneralSettings2::OnActiveAccountChanged(wxCommandEvent& event) -{ - UpdateAccountInformation(); - m_has_account_change = true; -} - -void GeneralSettings2::OnAccountServiceChanged(wxCommandEvent& event) -{ - auto& config = GetConfig(); - uint32 peristentId = GetSelectedAccountPersistentId(); - config.SetAccountSelectedService(peristentId, static_cast(m_active_service->GetSelection())); - UpdateAccountInformation(); -} - -void GeneralSettings2::OnMLCPathSelect(wxCommandEvent& event) -{ - if(CafeSystem::IsTitleRunning()) - { - wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - // show directory dialog - wxDirDialog path_dialog(this, _("Select MLC directory"), wxEmptyString, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().empty()) - return; - // check if the choosen MLC path is an already initialized MLC location - fs::path newMlc = wxHelper::MakeFSPath(path_dialog.GetPath()); - if(CemuApp::CheckMLCPath(newMlc)) - { - // ask user if they are sure they want to use this folder and let them know that accounts and saves wont transfer - wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); - wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); - if(dialog.ShowModal() == wxID_NO) - return; - if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) // creating also acts as a check for read+write access - { - wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - } - else - { - // ask user if they want to create a new mlc structure at the choosen location - wxString message = _("The selected directory does not contain the expected MLC structure. Do you want to create a new MLC structure in this directory?\nNote that changing the MLC location will not transfer any accounts or save files."); - wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); - if( !CemuApp::CreateDefaultMLCFiles(newMlc) ) - { - wxMessageBox(_("Failed to create default MLC files in the selected directory. The MLC path has not been changed"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - } - // update MLC path and store any other modified settings - GetConfig().SetMLCPath(newMlc); - StoreConfig(); - wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); - // close settings and then cemu - wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); - wxPostEvent(this, closeEvent); - wxPostEvent(GetParent(), closeEvent); -} - -void GeneralSettings2::OnMLCPathClear(wxCommandEvent& event) -{ - if(CafeSystem::IsTitleRunning()) - { - wxMessageBox(_("Can't change MLC path while a game is running!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - wxString message = _("Note that changing the MLC location will not transfer any accounts or save files. Are you sure you want to change the path?"); - wxMessageDialog dialog(this, message, _("Warning"), wxYES_NO | wxCENTRE | wxICON_WARNING); - if(dialog.ShowModal() == wxID_NO) - return; - GetConfig().SetMLCPath(""); - StoreConfig(); - GetConfigHandle().Save(); - wxMessageBox(_("Cemu needs to be restarted for the changes to take effect."), _("Information"), wxOK | wxCENTRE | wxICON_INFORMATION, this); - // close settings and then cemu - wxCloseEvent closeEvent(wxEVT_CLOSE_WINDOW); - wxPostEvent(this, closeEvent); - wxPostEvent(GetParent(), closeEvent); -} - -void GeneralSettings2::OnShowOnlineValidator(wxCommandEvent& event) -{ - const auto selection = m_active_account->GetSelection(); - if (selection == wxNOT_FOUND) - return; - - const auto* obj = dynamic_cast(m_active_account->GetClientObject(selection)); - wxASSERT(obj); - const auto& account = obj->GetAccount(); - - const auto validator = account.ValidateOnlineFiles(); - if (validator) // everything valid? shouldn't happen - return; - - wxString err; - err << _("The following error(s) have been found:") << '\n'; - - if (validator.otp == OnlineValidator::FileState::Missing) - err << _("otp.bin missing in Cemu directory") << '\n'; - else if(validator.otp == OnlineValidator::FileState::Corrupted) - err << _("otp.bin is invalid") << '\n'; - - if (validator.seeprom == OnlineValidator::FileState::Missing) - err << _("seeprom.bin missing in Cemu directory") << '\n'; - else if(validator.seeprom == OnlineValidator::FileState::Corrupted) - err << _("seeprom.bin is invalid") << '\n'; - - if(!validator.missing_files.empty()) - { - err << _("Missing certificate and key files:") << '\n'; - - int counter = 0; - for (const auto& f : validator.missing_files) - { - err << f << '\n'; - - ++counter; - if(counter > 10) - { - err << "..." << '\n'; - break; - } - } - - err << '\n'; - } - - if (!validator.valid_account) - { - err << _("The currently selected account is not a valid or dumped online account:") << '\n'; - err << GetOnlineAccountErrorMessage(validator.account_error); - } - - wxMessageBox(err, _("Online Status"), wxOK | wxCENTRE | wxICON_INFORMATION); -} - -wxString GeneralSettings2::GetOnlineAccountErrorMessage(OnlineAccountError error) -{ - switch (error) - { - case OnlineAccountError::kNoAccountId: - return _("AccountId missing (The account is not connected to a NNID/PNID)"); - case OnlineAccountError::kNoPasswordCached: - return _("IsPasswordCacheEnabled is set to false (The remember password option on your Wii U must be enabled for this account before dumping it)"); - case OnlineAccountError::kPasswordCacheEmpty: - return _("AccountPasswordCache is empty (The remember password option on your Wii U must be enabled for this account before dumping it)"); - case OnlineAccountError::kNoPrincipalId: - return _("PrincipalId missing"); - default: - return "no error"; - } -} diff --git a/src/gui/wxgui/GeneralSettings2.h b/src/gui/wxgui/GeneralSettings2.h deleted file mode 100644 index 2f7e3b68..00000000 --- a/src/gui/wxgui/GeneralSettings2.h +++ /dev/null @@ -1,143 +0,0 @@ -#pragma once -#include "config/CemuConfig.h" -#include -#include -#include - -class wxCheckBox; -class wxChoice; -class wxColourPickerCtrl; -class wxListBox; -class wxNotebook; -class wxRadioBox; -class wxSlider; -class wxSpinCtrl; -class wxSpinCtrlDouble; -class wxStaticText; - -wxDECLARE_EVENT(wxEVT_ACCOUNTLIST_REFRESH, wxCommandEvent); - -class GeneralSettings2 : public wxDialog -{ -public: - GeneralSettings2(wxWindow* parent, bool game_launched); - ~GeneralSettings2(); - - [[nodiscard]] bool ShouldReloadGamelist() const { return m_reload_gamelist; } - [[nodiscard]] bool MLCModified() const { return m_mlc_modified; } - void OnClose(wxCloseEvent& event); - -private: - void ValidateConfig(); - void StoreConfig(); - void DisableSettings(bool game_launched); - - bool m_reload_gamelist = false; - bool m_mlc_modified = false; - bool m_game_launched; - - bool m_has_account_change = false; // keep track of dirty state of accounts - std::vector m_api_map; // map from dropdown index to GraphicsAPISetting, used in HandleGraphicsApiSelection - - - wxPanel* AddGeneralPage(wxNotebook* notebook); - wxPanel* AddGraphicsPage(wxNotebook* notebook); - wxPanel* AddAudioPage(wxNotebook* notebook); - wxPanel* AddOverlayPage(wxNotebook* notebook); - wxPanel* AddAccountPage(wxNotebook* notebook); - wxPanel* AddDebugPage(wxNotebook* notebook); - - // General - wxChoice * m_language; - wxCheckBox* m_save_window_position_size; - wxCheckBox* m_save_padwindow_position_size; - wxCheckBox* m_discord_presence, *m_fullscreen_menubar; - wxCheckBox* m_auto_update, *m_receive_untested_releases, *m_save_screenshot; - wxCheckBox* m_disable_screensaver; - wxCheckBox* m_play_boot_sound; -#if BOOST_OS_WINDOWS - wxChoice* m_msw_theme; -#endif -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - wxCheckBox* m_feral_gamemode; -#endif - wxListBox* m_game_paths; - wxTextCtrl* m_mlc_path; - - // Graphics - wxChoice* m_graphic_api, * m_graphic_device; - wxChoice* m_vsync; - wxCheckBox* m_overrideGamma; - wxSpinCtrlDouble* m_overrideGammaValue; - wxSpinCtrlDouble* m_userDisplayGamma; - wxCheckBox* m_userDisplayisSRGB; - - wxCheckBox *m_async_compile, *m_gx2drawdone_sync; -#ifdef ENABLE_METAL - wxCheckBox *m_force_mesh_shaders; -#endif - wxRadioBox* m_upscale_filter, *m_downscale_filter, *m_fullscreen_scaling; - wxChoice* m_overlay_position, *m_notification_position, *m_overlay_scale, *m_notification_scale; - wxCheckBox* m_controller_profile_name, *m_controller_low_battery, *m_shader_compiling, *m_friends_data; - wxCheckBox *m_overlay_fps, *m_overlay_drawcalls, *m_overlay_cpu, *m_overlay_cpu_per_core,*m_overlay_ram, *m_overlay_vram, *m_overlay_debug; - wxColourPickerCtrl *m_overlay_font_color, *m_notification_font_color; - - // Audio - wxChoice* m_audio_api; - wxSlider *m_audio_latency; - wxSlider *m_tv_volume, *m_pad_volume, *m_input_volume, *m_portal_volume; - wxChoice *m_tv_channels, *m_pad_channels, *m_input_channels; - wxChoice *m_tv_device, *m_pad_device, *m_input_device, *m_portal_device; - - // Account - wxButton* m_create_account, * m_delete_account; - wxChoice* m_active_account; - wxRadioBox* m_active_service; - wxCollapsiblePane* m_account_information; - wxPropertyGrid* m_account_grid; - wxBitmapButton* m_validate_online; - wxStaticText* m_online_status; - - // Debug - wxChoice* m_crash_dump; - wxSpinCtrl* m_gdb_port; -#ifdef ENABLE_METAL - wxTextCtrl* m_gpu_capture_dir; - wxCheckBox* m_framebuffer_fetch; -#endif - - void OnAccountCreate(wxCommandEvent& event); - void OnAccountDelete(wxCommandEvent& event); - void OnAccountSettingsChanged(wxPropertyGridEvent& event); - void OnAudioLatencyChanged(wxCommandEvent& event); - void OnVolumeChanged(wxCommandEvent& event); - void OnInputVolumeChanged(wxCommandEvent& event); - void OnSliderChangedPercent(wxCommandEvent& event); - void OnLatencySliderChanged(wxCommandEvent& event); - void OnAudioAPISelected(wxCommandEvent& event); - void OnAudioDeviceSelected(wxCommandEvent& event); - void OnAudioChannelsSelected(wxCommandEvent& event); - void OnGraphicAPISelected(wxCommandEvent& event); - void OnUserDisplaySRGBSelected(wxCommandEvent& event); - void OnAddPathClicked(wxCommandEvent& event); - void OnRemovePathClicked(wxCommandEvent& event); - void OnActiveAccountChanged(wxCommandEvent& event); - void OnMLCPathSelect(wxCommandEvent& event); - void OnMLCPathClear(wxCommandEvent& event); - void OnShowOnlineValidator(wxCommandEvent& event); - void OnAccountServiceChanged(wxCommandEvent& event); - static wxString GetOnlineAccountErrorMessage(OnlineAccountError error); - - uint32 GetSelectedAccountPersistentId(); - - // updates cemu audio devices - void UpdateAudioDevice(); - // refreshes audio device list for dropdown - void UpdateAudioDeviceList(); - - void ResetAccountInformation(); - void UpdateAccountInformation(); - void UpdateOnlineAccounts(); - void HandleGraphicsApiSelection(); - void ApplyConfig(); -}; diff --git a/src/gui/wxgui/GettingStartedDialog.cpp b/src/gui/wxgui/GettingStartedDialog.cpp deleted file mode 100644 index df3c940e..00000000 --- a/src/gui/wxgui/GettingStartedDialog.cpp +++ /dev/null @@ -1,283 +0,0 @@ -#include "wxgui/GettingStartedDialog.h" - -#include -#include -#include -#include -#include -#include - -#include "config/ActiveSettings.h" -#include "wxCemuConfig.h" -#include "wxgui/CemuApp.h" -#include "wxgui/DownloadGraphicPacksWindow.h" -#include "wxgui/GraphicPacksWindow2.h" -#include "wxgui/input/InputSettings2.h" -#include "config/CemuConfig.h" - -#include "Cafe/TitleList/TitleList.h" - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif - -#include "wxHelper.h" - -wxDEFINE_EVENT(EVT_REFRESH_FIRST_PAGE, wxCommandEvent); // used to refresh the first page after the language change - -wxPanel* GettingStartedDialog::CreatePage1() -{ - auto* mainPanel = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* page1_sizer = new wxBoxSizer(wxVERTICAL); - { - auto* sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(new wxStaticBitmap(mainPanel, wxID_ANY, wxICON(M_WND_ICON128)), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_page1.staticText11 = new wxStaticText(mainPanel, wxID_ANY, _("It looks like you're starting Cemu for the first time.\nThis quick setup assistant will help you get the best experience")); - m_page1.staticText11->Wrap(-1); - sizer->Add(m_page1.staticText11, 0, wxALL, 5); - page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); - } - - if(ActiveSettings::IsPortableMode()) - { - m_page1.portableModeInfoText = new wxStaticText(mainPanel, wxID_ANY, _("Cemu is running in portable mode")); - m_page1.portableModeInfoText->Show(true); - page1_sizer->Add(m_page1.portableModeInfoText, 0, wxALL, 5); - - } - - // language selection -#if 0 - { - m_page1.languageBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Language")); - m_page1.languageText = new wxStaticText(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, _("Select the language you want to use in Cemu")); - m_page1.languageBoxSizer->Add(m_page1.languageText, 0, wxALL, 5); - - wxString language_choices[] = { _("Default") }; - wxChoice* m_language = new wxChoice(m_page1.languageBoxSizer->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, std::size(language_choices), language_choices); - m_language->SetSelection(0); - - for (const auto& language : wxGetApp().GetLanguages()) - { - m_language->Append(language->DescriptionNative); - } - - m_language->SetSelection(0); - m_page1.languageBoxSizer->Add(m_language, 0, wxALL | wxEXPAND, 5); - - page1_sizer->Add(m_page1.languageBoxSizer, 0, wxALL | wxEXPAND, 5); - - m_language->Bind(wxEVT_CHOICE, [this, m_language](const auto&) - { - const auto language = m_language->GetStringSelection(); - auto selection = m_language->GetSelection(); - if (selection == 0) - GetConfig().language = wxLANGUAGE_DEFAULT; - else - { - auto& app = wxGetApp(); - const auto language = m_language->GetStringSelection(); - for (const auto& lang : app.GetLanguages()) - { - if (lang->DescriptionNative == language) - { - app.LocalizeUI(static_cast(lang->Language)); - wxCommandEvent event(EVT_REFRESH_FIRST_PAGE); - wxPostEvent(this, event); - break; - } - } - } - }); - } -#endif - - { - m_page1.gamePathBoxSizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Game paths")); - m_page1.gamePathText = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("The game path is scanned by Cemu to automatically locate your games, game updates and DLCs. We recommend creating a dedicated directory in which\nyou place all your Wii U game files. Additional paths can be set later in Cemu's general settings. All common Wii U game formats are supported by Cemu.")); - m_page1.gamePathBoxSizer->Add(m_page1.gamePathText, 0, wxALL, 5); - - auto* game_path_sizer = new wxBoxSizer(wxHORIZONTAL); - - m_page1.gamePathText2 = new wxStaticText(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, _("Game path")); - game_path_sizer->Add(m_page1.gamePathText2, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_page1.gamePathPicker = new wxDirPickerCtrl(m_page1.gamePathBoxSizer->GetStaticBox(), wxID_ANY, wxEmptyString, _("Select a folder")); - game_path_sizer->Add(m_page1.gamePathPicker, 1, wxALL, 5); - - m_page1.gamePathBoxSizer->Add(game_path_sizer, 0, wxEXPAND, 5); - - page1_sizer->Add(m_page1.gamePathBoxSizer, 0, wxALL | wxEXPAND, 5); - } - - { - auto* sizer = new wxStaticBoxSizer(wxVERTICAL, mainPanel, _("Graphic packs && mods")); - - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("Graphic packs improve games by offering the ability to change resolution, increase FPS, tweak visuals or add gameplay modifications.\nGet started by opening the graphic packs configuration window.\n")), 0, wxALL, 5); - - auto* download_gp = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Download and configure graphic packs")); - download_gp->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnConfigureGPs, this); - sizer->Add(download_gp, 0, wxALIGN_CENTER | wxALL, 5); - - page1_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); - } - - { - auto* sizer = new wxFlexGridSizer(0, 1, 0, 0); - sizer->AddGrowableCol(0); - sizer->AddGrowableRow(0); - sizer->SetFlexibleDirection(wxBOTH); - sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - - auto* next = new wxButton(mainPanel, wxID_ANY, _("Next")); - next->Bind(wxEVT_BUTTON, [this](const auto&){m_notebook->SetSelection(1); }); - sizer->Add(next, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); - - page1_sizer->Add(sizer, 1, wxEXPAND, 5); - } - - mainPanel->SetSizer(page1_sizer); - return mainPanel; -} - -wxPanel* GettingStartedDialog::CreatePage2() -{ - auto* result = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* page2_sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* sizer = new wxStaticBoxSizer(new wxStaticBox(result, wxID_ANY, _("Input settings")), wxVERTICAL); - - sizer->Add(new wxStaticText(sizer->GetStaticBox(), wxID_ANY, _("You can configure one controller for each player.\nWe advise you to always use GamePad as emulated input for the first player, since many games expect the GamePad to be present.\nIt is also required for touch functionality.\nThe default global hotkeys are:\nCTRL - show pad screen\nCTRL + TAB - toggle pad screen\nALT + ENTER - toggle fullscreen\nESC - leave fullscreen\n\nIf you're having trouble configuring your controller, make sure to have it in idle state and press calibrate.\nAlso don't set the axis deadzone too low.")), 0, wxALL, 5); - - auto* input_button = new wxButton(sizer->GetStaticBox(), wxID_ANY, _("Configure input")); - input_button->Bind(wxEVT_BUTTON, &GettingStartedDialog::OnInputSettings, this); - sizer->Add(input_button, 0, wxALIGN_CENTER | wxALL, 5); - - page2_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); - } - - { - auto* sizer = new wxStaticBoxSizer(new wxStaticBox(result, wxID_ANY, _("Additional options")), wxVERTICAL); - - auto* option_sizer = new wxFlexGridSizer(0, 2, 0, 0); - option_sizer->SetFlexibleDirection(wxBOTH); - option_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - m_page2.fullscreenCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Start games with fullscreen")); - option_sizer->Add(m_page2.fullscreenCheckbox, 0, wxALL, 5); - - m_page2.separateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Open separate pad screen")); - option_sizer->Add(m_page2.separateCheckbox, 0, wxALL, 5); - - m_page2.updateCheckbox = new wxCheckBox(sizer->GetStaticBox(), wxID_ANY, _("Automatically check for updates")); - option_sizer->Add(m_page2.updateCheckbox, 0, wxALL, 5); -#if BOOST_OS_LINUX - if (!std::getenv("APPIMAGE")) { - m_page2.updateCheckbox->Disable(); - } -#endif - sizer->Add(option_sizer, 1, wxEXPAND, 5); - page2_sizer->Add(sizer, 0, wxALL | wxEXPAND, 5); - } - - { - auto* sizer = new wxFlexGridSizer(0, 3, 0, 0); - sizer->AddGrowableCol(1); - sizer->AddGrowableRow(0); - sizer->SetFlexibleDirection(wxBOTH); - sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_ALL); - - auto* previous = new wxButton(result, wxID_ANY, _("Previous")); - previous->Bind(wxEVT_BUTTON, [this](const auto&) {m_notebook->SetSelection(0); }); - sizer->Add(previous, 0, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); - - auto* close = new wxButton(result, wxID_ANY, _("Close")); - close->Bind(wxEVT_BUTTON, [this](const auto&){ Close(); }); - sizer->Add(close, 1, wxALIGN_BOTTOM | wxALIGN_RIGHT | wxALL, 5); - - page2_sizer->Add(sizer, 1, wxEXPAND | wxLEFT, 5); - } - - result->SetSizer(page2_sizer); - return result; -} - -void GettingStartedDialog::ApplySettings() -{ - auto& config = GetWxGUIConfig(); - m_page2.fullscreenCheckbox->SetValue(config.fullscreen.GetValue()); - m_page2.updateCheckbox->SetValue(config.check_update.GetValue()); - m_page2.separateCheckbox->SetValue(config.pad_open.GetValue()); -} - -void GettingStartedDialog::UpdateWindowSize() -{ - for (auto it = m_notebook->GetChildren().GetFirst(); it; it = it->GetNext()) - { - it->GetData()->Layout(); - } - m_notebook->Layout(); - Layout(); - Fit(); -} - -void GettingStartedDialog::OnClose(wxCloseEvent& event) -{ - event.Skip(); - - auto& wxGUIConfig = GetWxGUIConfig(); - wxGUIConfig.fullscreen = m_page2.fullscreenCheckbox->GetValue(); - wxGUIConfig.check_update = m_page2.updateCheckbox->GetValue(); - wxGUIConfig.pad_open = m_page2.separateCheckbox->GetValue(); - - auto& config = GetConfig(); - const fs::path gamePath = wxHelper::MakeFSPath(m_page1.gamePathPicker->GetPath()); - std::error_code ec; - if (!gamePath.empty() && fs::exists(gamePath, ec)) - { - const auto it = std::find(config.game_paths.cbegin(), config.game_paths.cend(), gamePath); - if (it == config.game_paths.cend()) - { - config.game_paths.emplace_back(_pathToUtf8(gamePath)); - } - } -} - -GettingStartedDialog::GettingStartedDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Getting started"), wxDefaultPosition, { 740,530 }, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_notebook = new wxSimplebook(this, wxID_ANY); - - auto* m_page1 = CreatePage1(); - m_notebook->AddPage(m_page1, wxEmptyString); - - auto* m_page2 = CreatePage2(); - m_notebook->AddPage(m_page2, wxEmptyString); - - sizer->Add(m_notebook, 1, wxEXPAND | wxALL, 5); - - this->SetSizer(sizer); - this->Centre(wxBOTH); - this->Bind(wxEVT_CLOSE_WINDOW, &GettingStartedDialog::OnClose, this); - - ApplySettings(); - UpdateWindowSize(); -} - -void GettingStartedDialog::OnConfigureGPs(wxCommandEvent& event) -{ - DownloadGraphicPacksWindow dialog(this); - dialog.ShowModal(); - GraphicPacksWindow2::RefreshGraphicPacks(); - GraphicPacksWindow2 window(this, 0); - window.ShowModal(); -} - -void GettingStartedDialog::OnInputSettings(wxCommandEvent& event) -{ - InputSettings2 dialog(this); - dialog.ShowModal(); -} diff --git a/src/gui/wxgui/GettingStartedDialog.h b/src/gui/wxgui/GettingStartedDialog.h deleted file mode 100644 index 9dfd69b4..00000000 --- a/src/gui/wxgui/GettingStartedDialog.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class GettingStartedDialog : public wxDialog -{ -public: - GettingStartedDialog(wxWindow* parent = nullptr); - -private: - wxPanel* CreatePage1(); - wxPanel* CreatePage2(); - void ApplySettings(); - void UpdateWindowSize(); - - void OnClose(wxCloseEvent& event); - void OnConfigureGPs(wxCommandEvent& event); - void OnInputSettings(wxCommandEvent& event); - - wxSimplebook* m_notebook; - - struct - { - // header - wxStaticText* staticText11{}; - wxStaticText* portableModeInfoText{}; - - // game path box - wxStaticBoxSizer* gamePathBoxSizer{}; - wxStaticText* gamePathText{}; - wxStaticText* gamePathText2{}; - wxDirPickerCtrl* gamePathPicker{}; - }m_page1; - - struct - { - wxCheckBox* fullscreenCheckbox; - wxCheckBox* separateCheckbox; - wxCheckBox* updateCheckbox; - }m_page2; -}; - diff --git a/src/gui/wxgui/GraphicPacksWindow2.cpp b/src/gui/wxgui/GraphicPacksWindow2.cpp deleted file mode 100644 index 21e25b07..00000000 --- a/src/gui/wxgui/GraphicPacksWindow2.cpp +++ /dev/null @@ -1,741 +0,0 @@ -#include "helpers/wxHelpers.h" -#include "wxgui/wxgui.h" -#include "wxgui/GraphicPacksWindow2.h" -#include "wxgui/DownloadGraphicPacksWindow.h" -#include "wxgui/DownloadCustomGraphicPackWindow.h" -#include "Cafe/GraphicPack/GraphicPack2.h" -#include "config/CemuConfig.h" -#include "config/ActiveSettings.h" - -#include "Cafe/HW/Latte/Core/LatteAsyncCommands.h" - -#include "Cafe/CafeSystem.h" -#include "Cafe/TitleList/TitleList.h" - -#include "wxHelper.h" - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif - -// main.cpp -class wxGraphicPackData : public wxTreeItemData -{ -public: - wxGraphicPackData(GraphicPackPtr pack) - : m_pack(std::move(pack)) { } - - const GraphicPackPtr& GetGraphicPack() const { return m_pack; } - -private: - GraphicPackPtr m_pack; -}; - -void GraphicPacksWindow2::FillGraphicPackList() const -{ - wxWindowUpdateLocker lock(m_graphic_pack_tree); - - m_graphic_pack_tree->DeleteAllItems(); - auto graphic_packs = GraphicPack2::GetGraphicPacks(); - - const auto root = m_graphic_pack_tree->AddRoot("Root"); - - const bool has_filter = !m_filter.empty(); - - for(auto& p : graphic_packs) - { - if (!p->IsUniversal()) - { - // filter graphic packs by given title id - if (m_filter_installed_games && !m_installed_games.empty()) - { - bool found = false; - for (uint64 titleId : p->GetTitleIds()) - { - if (std::find(m_installed_games.cbegin(), m_installed_games.cend(), titleId) != m_installed_games.cend()) - { - found = true; - break; - } - } - - if (!found) - continue; - } - - // filter graphic packs by given title id - if(has_filter) - { - bool found = false; - - if (boost::icontains(p->GetVirtualPath(), m_filter)) - found = true; - else - { - for (uint64 titleId : p->GetTitleIds()) - { - if (boost::icontains(fmt::format("{:x}", titleId), m_filter)) - { - found = true; - break; - } - } - } - - if (!found) - continue; - } - } - - const auto& path = p->GetVirtualPath(); - auto tokens = TokenizeView(path, '/'); - auto node = root; - for(size_t i=0; iAppendItem(parent_node, wxString::FromUTF8(token)); - } - } - else - { - // last token - // if a node with same name already exists, add a number suffix - for (sint32 s = 0; s < 999; s++) - { - wxString nodeName = wxString::FromUTF8(token); - if (s > 0) - nodeName.append(formatWxString(" #{}", s + 1)); - - node = FindTreeItem(parent_node, nodeName); - if (!node.IsOk()) - { - node = m_graphic_pack_tree->AppendItem(parent_node, nodeName); - break; - } - } - } - } - - if(node.IsOk() && node != root) - { - m_graphic_pack_tree->SetItemData(node, new wxGraphicPackData(p)); - bool canEnable = true; - - if (p->GetVersion() == 3) - { - auto tmp_text = m_graphic_pack_tree->GetItemText(node); - m_graphic_pack_tree->SetItemText(node, tmp_text + " (may not be compatible with Vulkan)"); - } - else if (p->GetVersion() != 3 && p->GetVersion() != 4 && p->GetVersion() != 5 && p->GetVersion() != 6 && p->GetVersion() != GraphicPack2::GFXPACK_VERSION_7 && p->GetVersion() != GraphicPack2::GFXPACK_VERSION_8) - { - auto tmp_text = m_graphic_pack_tree->GetItemText(node); - m_graphic_pack_tree->SetItemText(node, tmp_text + " (Unsupported version)"); - m_graphic_pack_tree->SetItemTextColour(node, m_incompatible_colour); - canEnable = false; - } - else if (p->IsActivated()) - m_graphic_pack_tree->SetItemTextColour(node, m_activated_colour); - - m_graphic_pack_tree->MakeCheckable(node, p->IsEnabled()); - if (!canEnable) - m_graphic_pack_tree->DisableCheckBox(node); - } - } - - m_graphic_pack_tree->Sort(root, true); - - if (!m_filter.empty()) - { - size_t counter = 0; - ExpandChildren({ root }, counter); - } -} - - -void GraphicPacksWindow2::GetChildren(const wxTreeItemId& id, std::vector& children) const -{ - wxTreeItemIdValue cookie; - wxTreeItemId child = m_graphic_pack_tree->GetFirstChild(id, cookie); - while (child.IsOk()) - { - children.emplace_back(child); - child = m_graphic_pack_tree->GetNextChild(child, cookie); - } -} - -void GraphicPacksWindow2::ExpandChildren(const std::vector& ids, size_t& counter) const -{ - std::vector children; - for (const auto& id : ids) - GetChildren(id, children); - - counter += children.size(); - if (counter >= 30 || children.empty()) - return; - - for (const auto& id : ids) - { - if(id != m_graphic_pack_tree->GetRootItem() && m_graphic_pack_tree->HasChildren(id)) - m_graphic_pack_tree->Expand(id); - } - - ExpandChildren(children, counter); -} - -void GraphicPacksWindow2::RefreshGraphicPacks() -{ - GraphicPack2::ClearGraphicPacks(); - GraphicPack2::LoadAll(); -} - -GraphicPacksWindow2::GraphicPacksWindow2(wxWindow* parent, uint64_t title_id_filter) - : wxDialog(parent, wxID_ANY, _("Graphic packs"), wxDefaultPosition, wxSize(1000,670), wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER), - m_installed_games(CafeTitleList::GetAllTitleIds()) -{ - if (title_id_filter != 0) - m_filter = fmt::format("{:x}", title_id_filter); - - m_filter_installed_games = !m_installed_games.empty(); - - SetIcon(wxICON(X_BOX)); - SetMinSize(wxSize(500, 400)); - auto main_sizer = new wxBoxSizer(wxVERTICAL); - - m_splitter_window = new wxSplitterWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3D); - m_splitter_window->Bind(wxEVT_SIZE, &GraphicPacksWindow2::OnSizeChanged, this); - m_splitter_window->Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &GraphicPacksWindow2::SashPositionChanged, this); - - // left side - auto left_panel = new wxPanel(m_splitter_window); - { - auto sizer = new wxBoxSizer(wxVERTICAL); - - wxFlexGridSizer* filter_row = new wxFlexGridSizer(0, 3, 0, 0); - filter_row->AddGrowableCol(1); - filter_row->SetFlexibleDirection(wxBOTH); - filter_row->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - const auto text = new wxStaticText(left_panel, wxID_ANY, _("Filter")); - text->Wrap(-1); - filter_row->Add(text, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_filter_text = new wxTextCtrl(left_panel, wxID_ANY, wxString::FromUTF8(m_filter)); - filter_row->Add(m_filter_text, 0, wxALL | wxEXPAND, 5); - m_filter_text->Bind(wxEVT_COMMAND_TEXT_UPDATED, &GraphicPacksWindow2::OnFilterUpdate, this); - - m_installed_games_only = new wxCheckBox(left_panel, wxID_ANY, _("Installed games")); - m_installed_games_only->SetValue(m_filter_installed_games); - filter_row->Add(m_installed_games_only, 0, wxALL | wxEXPAND, 5); - m_installed_games_only->Bind(wxEVT_CHECKBOX, &GraphicPacksWindow2::OnInstalledGamesChanged, this); - if (m_installed_games.empty()) - m_installed_games_only->Disable(); - - sizer->Add(filter_row, 0, wxEXPAND, 5); - - m_graphic_pack_tree = new wxCheckTree(left_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT); - m_graphic_pack_tree->Bind(wxEVT_TREE_SEL_CHANGED, &GraphicPacksWindow2::OnTreeSelectionChanged, this); - m_graphic_pack_tree->Bind(wxEVT_CHECKTREE_CHOICE, &GraphicPacksWindow2::OnTreeChoiceChanged, this); - //m_graphic_pack_tree->SetMinSize(wxSize(600, 400)); - sizer->Add(m_graphic_pack_tree, 1, wxEXPAND | wxALL, 5); - - left_panel->SetSizerAndFit(sizer); - } - - // right side - m_right_panel = new wxPanel(m_splitter_window, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxNO_BORDER | wxFULL_REPAINT_ON_RESIZE); - { - auto* sizer = new wxBoxSizer(wxVERTICAL); - { - m_gp_options = new wxScrolled(m_right_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxNO_BORDER | wxVSCROLL); - m_gp_options->SetScrollRate(-1, 10); - - auto* inner_sizer = new wxBoxSizer(wxVERTICAL); - { - auto* box = new wxStaticBox(m_gp_options, wxID_ANY, _("Graphic pack")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - m_graphic_pack_name = new wxStaticText(box, wxID_ANY, wxEmptyString); - box_sizer->Add(m_graphic_pack_name, 1, wxEXPAND | wxALL, 5); - - inner_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - { - auto* box = new wxStaticBox(m_gp_options, wxID_ANY, _("Description")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - m_graphic_pack_description = new wxStaticText(box, wxID_ANY, wxEmptyString); - box_sizer->Add(m_graphic_pack_description, 1, wxEXPAND | wxALL, 5); - - inner_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - m_preset_sizer = new wxBoxSizer(wxVERTICAL); - inner_sizer->Add(m_preset_sizer, 0, wxEXPAND, 0); - - { - auto* box = new wxStaticBox(m_gp_options, wxID_ANY, _("Control")); - auto* box_sizer = new wxStaticBoxSizer(box, wxHORIZONTAL); - - m_reload_shaders = new wxButton(box, wxID_ANY, _("Reload edited shaders")); - m_reload_shaders->Bind(wxEVT_BUTTON, &GraphicPacksWindow2::OnReloadShaders, this); - m_reload_shaders->Disable(); - box_sizer->Add(m_reload_shaders, 0, wxEXPAND | wxALL, 5); - - inner_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } - - inner_sizer->Add(new wxStaticText(m_gp_options, wxID_ANY, wxEmptyString), 1, wxALL | wxEXPAND, 5); - - m_gp_options->SetSizerAndFit(inner_sizer); - m_gp_options->Hide(); - - sizer->Add(m_gp_options, 1, wxEXPAND | wxRESERVE_SPACE_EVEN_IF_HIDDEN); - } - - - sizer->Add(new wxStaticLine(m_right_panel, wxID_ANY), 0, wxLEFT|wxRIGHT | wxEXPAND, 3); - - auto* row = new wxBoxSizer(wxHORIZONTAL); - - m_update_graphicPacks = new wxButton(m_right_panel, wxID_ANY, _("Download latest community graphic packs")); - m_update_graphicPacks->Bind(wxEVT_BUTTON, &GraphicPacksWindow2::OnCheckForUpdates, this); - row->Add(m_update_graphicPacks, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_download_from_url = new wxHyperlinkCtrl(m_right_panel, wxID_ANY, _("Or download pack from URL..."), _("")); - m_download_from_url->Bind(wxEVT_HYPERLINK, &GraphicPacksWindow2::OnClickCustomDownload, this); - row->Add(m_download_from_url, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - sizer->Add(row, 0, wxALL | wxEXPAND, 5); - - m_right_panel->SetSizerAndFit(sizer); - } - - m_splitter_window->SetMinimumPaneSize(50); - m_splitter_window->SplitVertically(left_panel, m_right_panel, (sint32)(m_ratio * m_splitter_window->GetParent()->GetSize().GetWidth())); - main_sizer->Add(m_splitter_window, 1, wxEXPAND, 5); - - - m_info_bar = new wxInfoBar(this); - m_info_bar->SetShowHideEffects(wxSHOW_EFFECT_BLEND, wxSHOW_EFFECT_BLEND); - m_info_bar->SetEffectDuration(500); - main_sizer->Add(m_info_bar, 0, wxALL | wxEXPAND, 5); - - SetSizer(main_sizer); - - UpdateTitleRunning(CafeSystem::IsTitleRunning()); - FillGraphicPackList(); -} - -void GraphicPacksWindow2::SaveStateToConfig() -{ - auto& data = GetConfigHandle().data(); - data.graphic_pack_entries.clear(); - - for (const auto& gp : GraphicPack2::GetGraphicPacks()) - { - auto filename = _utf8ToPath(gp->GetNormalizedPathString()); - if (gp->IsEnabled()) - { - data.graphic_pack_entries.try_emplace(filename); - auto& it = data.graphic_pack_entries[filename]; - // otherwise store all selected presets - for (const auto& preset : gp->GetActivePresets()) - it.try_emplace(preset->category, preset->name); - } - else if(gp->IsDefaultEnabled()) - { - // save that its disabled - data.graphic_pack_entries.try_emplace(filename); - auto& it = data.graphic_pack_entries[filename]; - it.try_emplace("_disabled", "false"); - } - } - - GetConfigHandle().Save(); -} - -GraphicPacksWindow2::~GraphicPacksWindow2() -{ - m_graphic_pack_tree->Unbind(wxEVT_CHECKTREE_CHOICE, &GraphicPacksWindow2::OnTreeSelectionChanged, this); - m_graphic_pack_tree->Unbind(wxEVT_CHECKTREE_CHOICE, &GraphicPacksWindow2::OnTreeChoiceChanged, this); - // m_active_preset->Unbind(wxEVT_CHOICE, &GraphicPacksWindow2::OnActivePresetChanged, this); - m_reload_shaders->Unbind(wxEVT_BUTTON, &GraphicPacksWindow2::OnReloadShaders, this); - - SaveStateToConfig(); -} - -wxTreeItemId GraphicPacksWindow2::FindTreeItem(const wxTreeItemId& root, const wxString& text) const -{ - wxTreeItemIdValue cookie; - for(auto item = m_graphic_pack_tree->GetFirstChild(root, cookie); item.IsOk(); item = m_graphic_pack_tree->GetNextSibling(item)) - { - if (m_graphic_pack_tree->GetItemText(item) == text) - return item; - } - - return {}; -} - -void GraphicPacksWindow2::LoadPresetSelections(const GraphicPackPtr& gp) -{ - std::vector order; - auto presets = gp->GetCategorizedPresets(order); - for(const auto& category : order) - { - const auto& entry = presets[category]; - - // test if any preset is visible and update its status - if (std::none_of(entry.cbegin(), entry.cend(), [gp](const auto& p) - { - return p->visible; - })) - { - continue; - } - - wxString categoryWxStr = wxString::FromUTF8(category); - wxString label(category.empty() ? _("Active preset") : categoryWxStr); - auto* box = new wxStaticBox(m_preset_sizer->GetContainingWindow(), wxID_ANY, label); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - auto* preset = new wxChoice(box, wxID_ANY); - preset->SetClientObject(new wxStringClientData(categoryWxStr)); - preset->Bind(wxEVT_CHOICE, &GraphicPacksWindow2::OnActivePresetChanged, this); - - std::optional active_preset; - for (auto& pentry : entry) - { - if (!pentry->visible) - continue; - - preset->AppendString(wxString::FromUTF8(pentry->name)); - if (pentry->active) - active_preset = pentry->name; - } - - if (active_preset) - preset->SetStringSelection(wxString::FromUTF8(active_preset.value())); - else if (preset->GetCount() > 0) - preset->SetSelection(0); - - box_sizer->Add(preset, 1, wxEXPAND | wxALL, 5); - - m_preset_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5); - } -} - -void GraphicPacksWindow2::OnTreeSelectionChanged(wxTreeEvent& event) -{ - wxWindowUpdateLocker lock(this); - - bool item_deselected = m_graphic_pack_tree->GetSelection() == m_graphic_pack_tree->GetRootItem(); - if (item_deselected) - return; - - const auto selection = m_graphic_pack_tree->GetSelection(); - if (selection.IsOk()) - { - const auto data = dynamic_cast(m_graphic_pack_tree->GetItemData(selection)); - if (data) - { - if(!m_gp_options->IsShown()) - m_gp_options->Show(); - - const auto& gp = data->GetGraphicPack(); - if (gp != m_shown_graphic_pack) - { - m_preset_sizer->Clear(true); - m_gp_name = gp->GetName(); - m_graphic_pack_name->SetLabel(wxString::FromUTF8(m_gp_name)); - - if (gp->GetDescription().empty()) - m_gp_description = _("This graphic pack has no description").utf8_string(); - else - m_gp_description = gp->GetDescription(); - - m_graphic_pack_description->SetLabel(wxString::FromUTF8(m_gp_description)); - - LoadPresetSelections(gp); - - m_reload_shaders->Enable(gp->HasShaders()); - - m_shown_graphic_pack = gp; - - m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 20); - m_graphic_pack_name->GetGrandParent()->Layout(); - - m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 20); - m_graphic_pack_description->GetGrandParent()->Layout(); - - m_right_panel->FitInside(); - m_right_panel->Layout(); - } - return; - } - } - - m_preset_sizer->Clear(true); - m_graphic_pack_name->SetLabel(wxEmptyString); - m_graphic_pack_description->SetLabel(wxEmptyString); - - m_reload_shaders->Disable(); - - m_shown_graphic_pack.reset(); - - m_gp_options->Hide(); - m_right_panel->FitInside(); - m_right_panel->Layout(); -} - -void GraphicPacksWindow2::OnTreeChoiceChanged(wxTreeEvent& event) -{ - auto item = event.GetItem(); - if (!item.IsOk()) - return; - - const bool state = event.GetExtraLong() != 0; - - const auto data = dynamic_cast(m_graphic_pack_tree->GetItemData(item)); - if (!data) - return; - - auto& graphic_pack = data->GetGraphicPack(); - graphic_pack->SetEnabled(state); - - bool requiresRestart = graphic_pack->RequiresRestart(true, false); - bool isRunning = CafeSystem::IsTitleRunning() && graphic_pack->ContainsTitleId(CafeSystem::GetForegroundTitleId()); - if (isRunning) - { - if (state) - { - GraphicPack2::ActivateGraphicPack(graphic_pack); - if (!requiresRestart) - { - ReloadPack(graphic_pack); - m_graphic_pack_tree->SetItemTextColour(item, m_activated_colour); - } - } - else - { - if (!requiresRestart) - { - DeleteShadersFromRuntimeCache(graphic_pack); - m_graphic_pack_tree->SetItemTextColour(item, m_default_colour); - } - GraphicPack2::DeactivateGraphicPack(graphic_pack); - } - } - - if (!m_info_bar->IsShown() && (isRunning && requiresRestart)) - m_info_bar->ShowMessage(_("Restart of Cemu required for changes to take effect")); - - // also change selection to activated gp - m_graphic_pack_tree->SelectItem(item); -} - -// In some environments with GTK (e.g. a flatpak app with org.freedesktop.Platform 22.08 runtime), -// destroying the event source inside the handler crashes the app. -// As a workaround to that, the wxWindow that needs to be destroyed is hidden and then -// destroyed at a later time, outside the handler. -void GraphicPacksWindow2::ClearPresets() -{ - size_t item_count = m_preset_sizer->GetItemCount(); - std::vector sizers; - sizers.reserve(item_count); - for (size_t i = 0; i < item_count; i++) - sizers.push_back(m_preset_sizer->GetItem(i)->GetSizer()); - - for (auto&& sizer : sizers) - { - auto static_box_sizer = dynamic_cast(sizer); - if (static_box_sizer) - { - wxStaticBox* parent_window = static_box_sizer->GetStaticBox(); - if (parent_window) - { - m_preset_sizer->Detach(sizer); - parent_window->Hide(); - CallAfter([=]() - { - parent_window->DestroyChildren(); - delete static_box_sizer; - }); - } - } - } -} - -void GraphicPacksWindow2::OnActivePresetChanged(wxCommandEvent& event) -{ - if (!m_shown_graphic_pack) - return; - - const auto obj = wxDynamicCast(event.GetEventObject(), wxChoice); - wxASSERT(obj); - const auto string_data = dynamic_cast(obj->GetClientObject()); - wxASSERT(string_data); - const auto preset = obj->GetStringSelection().utf8_string(); - if(m_shown_graphic_pack->SetActivePreset(string_data->GetData().utf8_string(), preset)) - { - wxWindowUpdateLocker lock(this); - ClearPresets(); - LoadPresetSelections(m_shown_graphic_pack); - //m_preset_sizer->GetContainingWindow()->Layout(); - //m_right_panel->FitInside(); - m_right_panel->FitInside(); - m_right_panel->Layout(); - } - - if (!m_shown_graphic_pack->RequiresRestart(false, true)) - ReloadPack(m_shown_graphic_pack); - else if (!m_info_bar->IsShown()) - m_info_bar->ShowMessage(_("Restart of Cemu required for changes to take effect")); -} - -void GraphicPacksWindow2::OnReloadShaders(wxCommandEvent& event) -{ - if (m_shown_graphic_pack) - ReloadPack(m_shown_graphic_pack); -} - -void GraphicPacksWindow2::OnClickCustomDownload(wxCommandEvent& event) -{ - DownloadCustomGraphicPackWindow frame(this); - if (frame.ShowModal() == wxID_OK) - { - RefreshGraphicPacks(); - FillGraphicPackList(); - } -} - -void GraphicPacksWindow2::OnCheckForUpdates(wxCommandEvent& event) -{ - DownloadGraphicPacksWindow frame(this); - SaveStateToConfig(); - const int updateResult = frame.ShowModal(); - if (updateResult == wxID_OK) - { - if (!CafeSystem::IsTitleRunning()) - { - // remember virtual paths of all the enabled packs - std::map previouslyEnabledPacks; - for(auto& it : GraphicPack2::GetGraphicPacks()) - { - if(it->IsEnabled()) - previouslyEnabledPacks.emplace(it->GetNormalizedPathString(), it->GetVirtualPath()); - } - // reload graphic packs - RefreshGraphicPacks(); - FillGraphicPackList(); - // remove packs which are still present - for(auto& it : GraphicPack2::GetGraphicPacks()) - previouslyEnabledPacks.erase(it->GetNormalizedPathString()); - if(!previouslyEnabledPacks.empty()) - { - std::string lost_packs; - for(auto& it : previouslyEnabledPacks) - { - lost_packs.append(it.second); - lost_packs.push_back('\n'); - } - wxString message = _("This update removed or renamed the following graphic packs:"); - message << "\n \n" << wxString::FromUTF8(lost_packs) << " \n" << _("You may need to set them up again."); - wxMessageBox(message, _("Warning"), wxOK | wxCENTRE | wxICON_INFORMATION, this); - } - } - } -} - -void GraphicPacksWindow2::OnSizeChanged(wxSizeEvent& event) -{ - const auto obj = (wxSplitterWindow*)event.GetEventObject(); - wxASSERT(obj); - - const auto width = std::max(obj->GetMinimumPaneSize(), obj->GetParent()->GetClientSize().GetWidth()); - obj->SetSashPosition((sint32)(m_ratio * width)); - - if (!m_gp_name.empty()) - m_graphic_pack_name->SetLabel(wxString::FromUTF8(m_gp_name)); - - if (!m_gp_description.empty()) - m_graphic_pack_description->SetLabel(wxString::FromUTF8(m_gp_description)); - - m_graphic_pack_name->Wrap(m_graphic_pack_name->GetParent()->GetClientSize().GetWidth() - 10); - m_graphic_pack_description->Wrap(m_graphic_pack_description->GetParent()->GetClientSize().GetWidth() - 10); - - event.Skip(); -} - -void GraphicPacksWindow2::SashPositionChanged(wxEvent& event) -{ - const auto obj = (wxSplitterWindow*)event.GetEventObject(); - wxASSERT(obj); - - const auto width = std::max(obj->GetMinimumPaneSize(), obj->GetParent()->GetClientSize().GetWidth()); - m_ratio = (float)obj->GetSashPosition() / width; - event.Skip(); -} - -void GraphicPacksWindow2::OnFilterUpdate(wxEvent& event) -{ - m_filter = m_filter_text->GetValue().utf8_string(); - FillGraphicPackList(); - event.Skip(); -} - -void GraphicPacksWindow2::OnInstalledGamesChanged(wxCommandEvent& event) -{ - m_filter_installed_games = m_installed_games_only->GetValue(); - FillGraphicPackList(); - event.Skip(); -} - -void GraphicPacksWindow2::UpdateTitleRunning(bool running) -{ - m_update_graphicPacks->Enable(!running); - m_download_from_url->Enable(!running); - if(running) - { - m_download_from_url->SetToolTip(_("Graphic packs cannot be updated while a game is running.")); - m_update_graphicPacks->SetToolTip(_("Graphic packs cannot be updated while a game is running.")); - } - else - { - m_update_graphicPacks->SetToolTip(nullptr); - m_download_from_url->SetToolTip(nullptr); - } -} - -void GraphicPacksWindow2::ReloadPack(const GraphicPackPtr& graphic_pack) const -{ - if (graphic_pack->HasShaders() || graphic_pack->HasPatches() || graphic_pack->HasCustomVSyncFrequency()) - { - if (graphic_pack->Reload()) - { - DeleteShadersFromRuntimeCache(graphic_pack); - } - } -} - -void GraphicPacksWindow2::DeleteShadersFromRuntimeCache(const GraphicPackPtr& graphic_pack) const -{ - for (const auto& shader : graphic_pack->GetCustomShaders()) - { - LatteConst::ShaderType shaderType; - if (shader.type == GraphicPack2::GP_SHADER_TYPE::VERTEX) - shaderType = LatteConst::ShaderType::Vertex; - else if (shader.type == GraphicPack2::GP_SHADER_TYPE::GEOMETRY) - shaderType = LatteConst::ShaderType::Geometry; - else if (shader.type == GraphicPack2::GP_SHADER_TYPE::PIXEL) - shaderType = LatteConst::ShaderType::Pixel; - LatteAsyncCommands_queueDeleteShader(shader.shader_base_hash, shader.shader_aux_hash, shaderType); - } -} - diff --git a/src/gui/wxgui/GraphicPacksWindow2.h b/src/gui/wxgui/GraphicPacksWindow2.h deleted file mode 100644 index bf8647de..00000000 --- a/src/gui/wxgui/GraphicPacksWindow2.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "wxcomponents/checktree.h" - -#include "Cafe/GraphicPack/GraphicPack2.h" - -class wxSplitterWindow; -class wxPanel; -class wxButton; -class wxChoice; - -class GraphicPacksWindow2 : public wxDialog -{ -public: - GraphicPacksWindow2(wxWindow* parent, uint64_t title_id_filter); - ~GraphicPacksWindow2(); - - static void RefreshGraphicPacks(); - void UpdateTitleRunning(bool running); - -private: - std::string m_filter; - bool m_filter_installed_games; - std::vector m_installed_games; - - void ClearPresets(); - void FillGraphicPackList() const; - void GetChildren(const wxTreeItemId& id, std::vector& children) const; - void ExpandChildren(const std::vector& ids, size_t& counter) const; - - wxSplitterWindow * m_splitter_window; - - wxPanel* m_right_panel; - wxScrolled* m_gp_options; - - wxCheckTree * m_graphic_pack_tree; - wxTextCtrl* m_filter_text; - wxCheckBox* m_installed_games_only; - - wxStaticText* m_graphic_pack_name, *m_graphic_pack_description; - wxBoxSizer* m_preset_sizer; - std::vector m_active_preset; - wxButton* m_reload_shaders; - wxHyperlinkCtrl* m_download_from_url; - wxButton* m_update_graphicPacks; - wxInfoBar* m_info_bar; - - GraphicPackPtr m_shown_graphic_pack; - std::string m_gp_name, m_gp_description; - - float m_ratio = 0.55f; - wxColour m_default_colour = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - wxColour m_activated_colour = wxSystemSettings::SelectLightDark(wxColour(0x00, 0xCC, 0x00), wxColour(0x42, 0xB3, 0x42)); - wxColour m_incompatible_colour = wxSystemSettings::SelectLightDark(wxColour(0xCC, 0x00, 0x00), wxColour(0xDE, 0x49, 0x49)); - - wxTreeItemId FindTreeItem(const wxTreeItemId& root, const wxString& text) const; - void LoadPresetSelections(const GraphicPackPtr& gp); - - void OnTreeSelectionChanged(wxTreeEvent& event); - void OnTreeChoiceChanged(wxTreeEvent& event); - void OnActivePresetChanged(wxCommandEvent& event); - void OnReloadShaders(wxCommandEvent& event); - void OnClickCustomDownload(wxCommandEvent& event); - void OnCheckForUpdates(wxCommandEvent& event); - void OnSizeChanged(wxSizeEvent& event); - void SashPositionChanged(wxEvent& event); - void OnFilterUpdate(wxEvent& event); - void OnInstalledGamesChanged(wxCommandEvent& event); - - void SaveStateToConfig(); - - void ReloadPack(const GraphicPackPtr& graphic_pack) const; - void DeleteShadersFromRuntimeCache(const GraphicPackPtr& graphic_pack) const; - -}; \ No newline at end of file diff --git a/src/gui/wxgui/LoggingWindow.cpp b/src/gui/wxgui/LoggingWindow.cpp deleted file mode 100644 index 6c6ca6c3..00000000 --- a/src/gui/wxgui/LoggingWindow.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "wxgui/LoggingWindow.h" - -#include "Cemu/Logging/CemuLogging.h" -#include "wxgui/helpers/wxLogEvent.h" - -#include -#include -#include - -wxDEFINE_EVENT(EVT_LOG, wxLogEvent); - -LoggingWindow::LoggingWindow(wxFrame* parent) - : wxFrame(parent, wxID_ANY, _("Logging window"), wxDefaultPosition, wxSize(800, 600), wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) -{ - auto* sizer = new wxBoxSizer( wxVERTICAL ); - { - auto filter_row = new wxBoxSizer( wxHORIZONTAL ); - - filter_row->Add(new wxStaticText( this, wxID_ANY, _("Filter")), 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - - wxString choices[] = {"Unsupported APIs calls", "Coreinit Logging", "Coreinit File-Access", "Coreinit Thread-Synchronization", "Coreinit Memory", "Coreinit MP", "Coreinit Thread", "nn::nfp", "GX2", "Audio", "Input", "Socket", "Save", "H264", "Graphic pack patches", "Texture cache", "Texture readback", "OpenGL debug output", "Vulkan validation layer", "Metal debug output"}; - m_filter = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, std::size(choices), choices); - m_filter->Bind(wxEVT_COMBOBOX, &LoggingWindow::OnFilterChange, this); - m_filter->Bind(wxEVT_TEXT, &LoggingWindow::OnFilterChange, this); - filter_row->Add(m_filter, 1, wxALL, 5 ); - - m_filter_message = new wxCheckBox(this, wxID_ANY, _("Filter messages")); - m_filter_message->Bind(wxEVT_CHECKBOX, &LoggingWindow::OnFilterMessageChange, this); - filter_row->Add(m_filter_message, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - - sizer->Add( filter_row, 0, wxEXPAND, 5 ); - } - - m_log_list = new wxLogCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxScrolledWindowStyle, true); - sizer->Add( m_log_list, 1, wxALL | wxEXPAND, 5 ); - - this->SetSizer( sizer ); - this->Layout(); - - this->Bind(EVT_LOG, &LoggingWindow::OnLogMessage, this); - - cemuLog_setCallbacks(this); -} - -LoggingWindow::~LoggingWindow() -{ - this->Unbind(EVT_LOG, &LoggingWindow::OnLogMessage, this); - - cemuLog_clearCallbacks(); -} - -void LoggingWindow::Log(std::string_view filter, std::string_view message) -{ - wxLogEvent event(std::string {filter}, std::string{ message }); - OnLogMessage(event); - - //const auto log_event = new wxLogEvent(filter, message); - //wxQueueEvent(s_instance, log_event); -} - -void LoggingWindow::Log(std::string_view filter, std::wstring_view message) -{ - wxLogEvent event(std::string {filter}, std::wstring{ message }); - OnLogMessage(event); - - //const auto log_event = new wxLogEvent(filter, message); - //wxQueueEvent(s_instance, log_event); -} - -void LoggingWindow::OnLogMessage(wxLogEvent& event) -{ - m_log_list->PushEntry(event.GetFilter(), event.GetMessage()); -} - -void LoggingWindow::OnFilterChange(wxCommandEvent& event) -{ - m_log_list->SetActiveFilter(m_filter->GetValue().utf8_string()); - event.Skip(); -} - -void LoggingWindow::OnFilterMessageChange(wxCommandEvent& event) -{ - m_log_list->SetFilterMessage(m_filter_message->GetValue()); - event.Skip(); -} diff --git a/src/gui/wxgui/LoggingWindow.h b/src/gui/wxgui/LoggingWindow.h deleted file mode 100644 index d1ba2689..00000000 --- a/src/gui/wxgui/LoggingWindow.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "Cemu/Logging/CemuLogging.h" - -#include -#include -#include -#include "wxgui/components/wxLogCtrl.h" - -class wxLogEvent; - -class LoggingWindow : public wxFrame, public LoggingCallbacks -{ - public: - LoggingWindow(wxFrame* parent); - ~LoggingWindow(); - - void Log(std::string_view filter, std::string_view message) override; - void Log(std::string_view filter, std::wstring_view message) override; - - private: - void OnLogMessage(wxLogEvent& event); - void OnFilterChange(wxCommandEvent& event); - void OnFilterMessageChange(wxCommandEvent& event); - - wxComboBox* m_filter; - wxLogCtrl* m_log_list; - wxCheckBox* m_filter_message; -}; diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp deleted file mode 100644 index db4dfbe9..00000000 --- a/src/gui/wxgui/MainWindow.cpp +++ /dev/null @@ -1,2516 +0,0 @@ -#include "MainWindow.h" - -// subwindows -#include "TitleManager.h" -#include "GeneralSettings2.h" -#include "GameUpdateWindow.h" -#include "CemuUpdateWindow.h" -#include "GraphicPacksWindow2.h" -#include "AudioDebuggerWindow.h" -#include "input/InputSettings2.h" -#include "input/HotkeySettings.h" -#include "debugger/DebuggerWindow2.h" -#include "EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" -#include "windows/PPCThreadsViewer/DebugPPCThreadsWindow.h" -#include "windows/TextureRelationViewer/TextureRelationWindow.h" - -//wxgui + misc. -#include "wxgui.h" -#include "wxCemuConfig.h" -#include "interface/WindowSystem.h" -#include "wxHelper.h" -#include "helpers/wxHelpers.h" -#include "PadViewFrame.h" - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif - -// settings -#include "config/CemuConfig.h" -#include "config/LaunchSettings.h" -#include "config/ActiveSettings.h" - -// External functionality headers -#include "input/InputManager.h" -#include "Cafe/TitleList/TitleList.h" -#include "Cemu/DiscordPresence/DiscordPresence.h" -#include "util/ScreenSaver/ScreenSaver.h" -#include "util/helpers/SystemException.h" -#include "Cafe/HW/Latte/Renderer/Vulkan/VsyncDriver.h" -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) -#include -#endif -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND -#include "helpers/wxWayland.h" -#endif - -// Renderer Canvasses -#ifdef ENABLE_OPENGL -#include "canvas/OpenGLCanvas.h" -#endif -#ifdef ENABLE_VULKAN -#include "canvas/VulkanCanvas.h" -#endif -#ifdef ENABLE_METAL -#include "canvas/MetalCanvas.h" -#include "Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h" -#endif - -//Cafe libs -#include "Cafe/OS/libs/nfc/nfc.h" -#include "Cafe/OS/libs/swkbd/swkbd.h" - -#include "Cafe/HW/Latte/Renderer/Renderer.h" // For renderer API checks - -extern WindowSystem::WindowInfo g_window_info; -extern std::shared_mutex g_mutex; - -enum -{ - // ui elements - MAINFRAME_GAMELIST_ID = 20000, //wxID_HIGHEST + 1, - // file - MAINFRAME_MENU_ID_FILE_LOAD = 20100, - MAINFRAME_MENU_ID_FILE_INSTALL_UPDATE, - MAINFRAME_MENU_ID_FILE_OPEN_CEMU_FOLDER, - MAINFRAME_MENU_ID_FILE_OPEN_MLC_FOLDER, - MAINFRAME_MENU_ID_FILE_OPEN_SHADERCACHE_FOLDER, - MAINFRAME_MENU_ID_FILE_CLEAR_SPOTPASS_CACHE, - MAINFRAME_MENU_ID_FILE_EXIT, - MAINFRAME_MENU_ID_FILE_END_EMULATION, - MAINFRAME_MENU_ID_FILE_RECENT_0, - MAINFRAME_MENU_ID_FILE_RECENT_LAST = MAINFRAME_MENU_ID_FILE_RECENT_0 + 15, - // options - MAINFRAME_MENU_ID_OPTIONS_FULLSCREEN = 20200, - MAINFRAME_MENU_ID_OPTIONS_SECOND_WINDOW_PADVIEW, - MAINFRAME_MENU_ID_OPTIONS_GRAPHIC, - MAINFRAME_MENU_ID_OPTIONS_GRAPHIC_PACKS2, - MAINFRAME_MENU_ID_OPTIONS_GENERAL, - MAINFRAME_MENU_ID_OPTIONS_GENERAL2, - MAINFRAME_MENU_ID_OPTIONS_AUDIO, - MAINFRAME_MENU_ID_OPTIONS_INPUT, - MAINFRAME_MENU_ID_OPTIONS_HOTKEY, - MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, - // options -> account - MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350, - MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_12 = 20350 + 11, - - // options -> system language - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_JAPANESE = 20500, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ENGLISH, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_FRENCH, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_GERMAN, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ITALIAN, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_SPANISH, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_CHINESE, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_KOREAN, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_DUTCH, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_PORTUGUESE, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_RUSSIAN, - MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_TAIWANESE, - // tools - MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER = 20600, - MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, - MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, - MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, - // cpu - // cpu->timer speed - MAINFRAME_MENU_ID_TIMER_SPEED_1X = 20700, - MAINFRAME_MENU_ID_TIMER_SPEED_2X = 20701, - MAINFRAME_MENU_ID_TIMER_SPEED_4X = 20702, - MAINFRAME_MENU_ID_TIMER_SPEED_8X = 20703, - MAINFRAME_MENU_ID_TIMER_SPEED_05X = 20704, - MAINFRAME_MENU_ID_TIMER_SPEED_025X = 20705, - MAINFRAME_MENU_ID_TIMER_SPEED_0125X = 20706, - - // nfc->Touch NFC file - MAINFRAME_MENU_ID_NFC_TOUCH_NFC_FILE = 21000, - MAINFRAME_MENU_ID_NFC_RECENT_0, - MAINFRAME_MENU_ID_NFC_RECENT_LAST = MAINFRAME_MENU_ID_NFC_RECENT_0 + 15, - // debug - MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN = 21100, - MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, - MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, - MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, - MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, - MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, - MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, - MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY, - MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, - MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, - - // debug->logging - MAINFRAME_MENU_ID_DEBUG_LOGGING_MESSAGE = 21499, - MAINFRAME_MENU_ID_DEBUG_LOGGING0 = 21500, - MAINFRAME_MENU_ID_DEBUG_ADVANCED_PPC_INFO = 21599, - // debug->dump - MAINFRAME_MENU_ID_DEBUG_DUMP_TEXTURES = 21600, - MAINFRAME_MENU_ID_DEBUG_DUMP_SHADERS, - MAINFRAME_MENU_ID_DEBUG_DUMP_RECOMPILER_FUNCTIONS, - MAINFRAME_MENU_ID_DEBUG_DUMP_RAM, - MAINFRAME_MENU_ID_DEBUG_DUMP_RPX, - MAINFRAME_MENU_ID_DEBUG_DUMP_FST, - MAINFRAME_MENU_ID_DEBUG_DUMP_CURL_REQUESTS, - // help - MAINFRAME_MENU_ID_HELP_ABOUT = 21700, - MAINFRAME_MENU_ID_HELP_UPDATE, - // custom - MAINFRAME_ID_TIMER1 = 21800, -}; - -wxDEFINE_EVENT(wxEVT_SET_WINDOW_TITLE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_REQUEST_GAMELIST_REFRESH, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_LAUNCH_GAME, wxLaunchGameEvent); -wxDEFINE_EVENT(wxEVT_REQUEST_RECREATE_CANVAS, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_REQUEST_GAME_EXIT, wxCommandEvent); - -wxBEGIN_EVENT_TABLE(MainWindow, wxFrame) -EVT_TIMER(MAINFRAME_ID_TIMER1, MainWindow::OnTimer) -EVT_CLOSE(MainWindow::OnClose) -EVT_SIZE(MainWindow::OnSizeEvent) -EVT_DPI_CHANGED(MainWindow::OnDPIChangedEvent) -EVT_MOVE(MainWindow::OnMove) -// file menu -EVT_MENU(MAINFRAME_MENU_ID_FILE_LOAD, MainWindow::OnFileMenu) -EVT_MENU(MAINFRAME_MENU_ID_FILE_INSTALL_UPDATE, MainWindow::OnInstallUpdate) -EVT_MENU(MAINFRAME_MENU_ID_FILE_OPEN_CEMU_FOLDER, MainWindow::OnOpenFolder) -EVT_MENU(MAINFRAME_MENU_ID_FILE_OPEN_MLC_FOLDER, MainWindow::OnOpenFolder) -EVT_MENU(MAINFRAME_MENU_ID_FILE_OPEN_SHADERCACHE_FOLDER, MainWindow::OnOpenFolder) -EVT_MENU(MAINFRAME_MENU_ID_FILE_CLEAR_SPOTPASS_CACHE, MainWindow::OnClearSpotPassCache) -EVT_MENU(MAINFRAME_MENU_ID_FILE_EXIT, MainWindow::OnFileExit) -EVT_MENU(MAINFRAME_MENU_ID_FILE_END_EMULATION, MainWindow::OnFileMenu) -EVT_MENU_RANGE(MAINFRAME_MENU_ID_FILE_RECENT_0 + 0, MAINFRAME_MENU_ID_FILE_RECENT_LAST, MainWindow::OnFileMenu) -// options -> region menu -EVT_MENU_RANGE(MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1, MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_12, MainWindow::OnAccountSelect) -EVT_MENU_RANGE(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_JAPANESE, MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_TAIWANESE, MainWindow::OnConsoleLanguage) -// options menu -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_FULLSCREEN, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_SECOND_WINDOW_PADVIEW, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GRAPHIC, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GRAPHIC_PACKS2, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_AUDIO, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, MainWindow::OnOptionsInput) -EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, MainWindow::OnOptionsInput) -// tools menu -EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) -EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput) -EVT_MENU(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, MainWindow::OnToolsInput) -EVT_MENU(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, MainWindow::OnToolsInput) -// cpu menu -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_8X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_4X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_2X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_1X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_05X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_025X, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_0125X, MainWindow::OnDebugSetting) -// nfc menu -EVT_MENU(MAINFRAME_MENU_ID_NFC_TOUCH_NFC_FILE, MainWindow::OnNFCMenu) -EVT_MENU_RANGE(MAINFRAME_MENU_ID_NFC_RECENT_0 + 0, MAINFRAME_MENU_ID_NFC_RECENT_LAST, MainWindow::OnNFCMenu) -// debug -> logging menu -EVT_MENU_RANGE(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + 0, MAINFRAME_MENU_ID_DEBUG_LOGGING0 + 98, MainWindow::OnDebugLoggingToggleFlagGeneric) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_ADVANCED_PPC_INFO, MainWindow::OnPPCInfoToggle) -// debug -> dump menu -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_TEXTURES, MainWindow::OnDebugDumpGeneric) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_SHADERS, MainWindow::OnDebugDumpGeneric) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_RECOMPILER_FUNCTIONS, MainWindow::OnDebugDumpGeneric) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_CURL_REQUESTS, MainWindow::OnDebugSetting) -// debug -> Other options -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_RAM, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_RPX, MainWindow::OnDebugSetting) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_DUMP_FST, MainWindow::OnDebugSetting) -// debug -> View ... -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, MainWindow::OnLoggingWindow) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, MainWindow::OnGDBStubToggle) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, MainWindow::OnDebugViewPPCThreads) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, MainWindow::OnDebugViewPPCDebugger) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, MainWindow::OnDebugViewAudioDebugger) -EVT_MENU(MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, MainWindow::OnDebugViewTextureRelations) -// help menu -EVT_MENU(MAINFRAME_MENU_ID_HELP_ABOUT, MainWindow::OnHelpAbout) -EVT_MENU(MAINFRAME_MENU_ID_HELP_UPDATE, MainWindow::OnHelpUpdate) -// misc -EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAMELIST_REFRESH, MainWindow::OnRequestGameListRefresh) - -EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_BEGIN_UPDATE, MainWindow::OnGameListBeginUpdate) -EVT_COMMAND(wxID_ANY, wxEVT_GAMELIST_END_UPDATE, MainWindow::OnGameListEndUpdate) -EVT_COMMAND(wxID_ANY, wxEVT_ACCOUNTLIST_REFRESH, MainWindow::OnAccountListRefresh) -EVT_COMMAND(wxID_ANY, wxEVT_SET_WINDOW_TITLE, MainWindow::OnSetWindowTitle) -EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_RECREATE_CANVAS, MainWindow::OnRequestRecreateCanvas) -EVT_COMMAND(wxID_ANY, wxEVT_REQUEST_GAME_EXIT, MainWindow::OnRequestGameExit) - -wxEND_EVENT_TABLE() - -class wxGameDropTarget : public wxFileDropTarget -{ -public: - wxGameDropTarget(MainWindow* window) : m_window(window) {} - bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames) override - { - if(!m_window->IsGameLaunched() && filenames.GetCount() == 1) - return m_window->FileLoad(_utf8ToPath(filenames[0].utf8_string()), wxLaunchGameEvent::INITIATED_BY::DRAG_AND_DROP); - - return false; - } - -private: - MainWindow* m_window; -}; - -class wxAmiiboDropTarget : public wxFileDropTarget -{ -public: - wxAmiiboDropTarget(MainWindow* window) : m_window(window) {} - bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames) override - { - if (!m_window->IsGameLaunched() || filenames.GetCount() != 1) - return false; - uint32 nfcError; - std::string path = filenames[0].utf8_string(); - if (nfc::TouchTagFromFile(_utf8ToPath(path), &nfcError)) - { - GetWxGUIConfig().AddRecentNfcFile(path); - m_window->UpdateNFCMenu(); - return true; - } - else - { - if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) - wxMessageBox(_("Cannot open file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - } - -private: - MainWindow* m_window; -}; - -MainWindow::MainWindow() - : wxFrame(nullptr, wxID_ANY, GetInitialWindowTitle(), wxDefaultPosition, wxSize(1280, 720), wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN | wxRESIZE_BORDER) -{ -#ifdef __WXMAC__ - // Not necessary to set wxApp::s_macExitMenuItemId as automatically handled - wxApp::s_macAboutMenuItemId = MAINFRAME_MENU_ID_HELP_ABOUT; - wxApp::s_macPreferencesMenuItemId = MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS; -#endif - g_window_info.window_main = initHandleContextFromWxWidgetsWindow(this); - g_mainFrame = this; - CafeSystem::SetImplementation(this); - - RecreateMenu(); - SetClientSize(1280, 720); - SetIcon(wxICON(M_WND_ICON128)); - -#if BOOST_OS_MACOS - this->EnableFullScreenView(true); -#endif - -#if BOOST_OS_WINDOWS - HICON hWindowIcon = (HICON)LoadImageA(NULL, "M_WND_ICON16", IMAGE_ICON, 16, 16, LR_LOADFROMFILE); - SendMessage(this->GetHWND(), WM_SETICON, ICON_SMALL, (LPARAM)hWindowIcon); -#endif - - auto* main_sizer = new wxBoxSizer(wxVERTICAL); - auto load_file = LaunchSettings::GetLoadFile(); - auto load_title_id = LaunchSettings::GetLoadTitleID(); - bool quick_launch = false; - - if (load_file) - { - MainWindow::RequestLaunchGame(load_file.value(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); - quick_launch = true; - } - else if (load_title_id) - { - TitleInfo info; - TitleId baseId; - if (CafeTitleList::FindBaseTitleId(load_title_id.value(), baseId) && CafeTitleList::GetFirstByTitleId(baseId, info)) - { - MainWindow::RequestLaunchGame(info.GetPath(), wxLaunchGameEvent::INITIATED_BY::COMMAND_LINE); - quick_launch = true; - } - else - { - wxString errorMsg = fmt::format("Title ID {:016x} not found", load_title_id.value()); - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - - } - } - SetSizer(main_sizer); - if (!quick_launch) - { - CreateGameListAndStatusBar(); - } - else - { - // launching game via -g or -t option. Don't set up or load game list - m_game_list = nullptr; - m_info_bar = nullptr; - } - SetSizer(main_sizer); - - m_last_mouse_move_time = std::chrono::steady_clock::now(); - - m_timer = new wxTimer(this, MAINFRAME_ID_TIMER1); - m_timer->Start(500); - - LoadSettings(); - - #ifdef ENABLE_DISCORD_RPC - if (GetWxGUIConfig().use_discord_presence) - m_discord = std::make_unique(); - #endif - - Bind(wxEVT_OPEN_GRAPHIC_PACK, &MainWindow::OnGraphicWindowOpen, this); - Bind(wxEVT_LAUNCH_GAME, &MainWindow::OnLaunchFromFile, this); - - if (LaunchSettings::GDBStubEnabled()) - { - g_gdbstub = std::make_unique(GetConfig().gdb_port); - } - - if (LaunchSettings::OpenDebuggerEnabled()) - OpenPPCDebugger(); -} - -MainWindow::~MainWindow() -{ - if (m_padView) - { - m_padView->Destroy(); - m_padView = nullptr; - } - - m_timer->Stop(); - - std::unique_lock lock(g_mutex); - g_mainFrame = nullptr; -} - -void MainWindow::CreateGameListAndStatusBar() -{ - if(m_main_panel) - return; // already displayed - m_main_panel = new wxPanel(this); - auto* sizer = new wxBoxSizer(wxVERTICAL); - // game list - m_game_list = new wxGameList(m_main_panel, MAINFRAME_GAMELIST_ID); - m_game_list->Bind(wxEVT_OPEN_SETTINGS, [this](auto&) {OpenSettings(); }); - m_game_list->SetDropTarget(new wxGameDropTarget(this)); - sizer->Add(m_game_list, 1, wxEXPAND); - - // info, warning bar - m_info_bar = new wxInfoBar(m_main_panel); - m_info_bar->SetShowHideEffects(wxSHOW_EFFECT_BLEND, wxSHOW_EFFECT_BLEND); - m_info_bar->SetEffectDuration(500); - sizer->Add(m_info_bar, 0, wxALL | wxEXPAND, 5); - - m_main_panel->SetSizer(sizer); - - auto* main_sizer = this->GetSizer(); - main_sizer->Add(m_main_panel, 1, wxEXPAND, 0, nullptr); -} - -void MainWindow::DestroyGameListAndStatusBar() -{ - if(!m_main_panel) - return; - m_main_panel->Destroy(); - m_main_panel = nullptr; - m_game_list = nullptr; - m_info_bar = nullptr; -} - -wxString MainWindow::GetInitialWindowTitle() -{ - return BUILD_VERSION_WITH_NAME_STRING; -} - -void MainWindow::OnClose(wxCloseEvent& event) -{ - if (m_debugger_window) - { - m_debugger_window->CleanupForDestroy(); - m_debugger_window->Destroy(); - m_debugger_window = nullptr; - } - - if(m_game_list) - m_game_list->OnClose(event); - - if (!IsMaximized() && !WindowSystem::IsFullScreen()) - m_restored_size = GetSize(); - - SaveSettings(); - m_timer->Stop(); - - event.Skip(); - - CafeSystem::Shutdown(); - DestroyCanvas(); -} - -bool MainWindow::InstallUpdate(const fs::path& metaFilePath) -{ - try - { - GameUpdateWindow frame(*this, metaFilePath); - const int updateResult = frame.ShowModal(); - - if (updateResult == wxID_OK) - { - CafeTitleList::AddTitleFromPath(frame.GetTargetPath()); // this will also send a notification to the game list which will update the entry - wxMessageBox(_("Title installed!"), _("Success")); - return true; - } - else - { - if (frame.GetExceptionMessage().empty()) - wxMessageBox(_("Title installation has been canceled!")); - else - { - throw std::runtime_error(frame.GetExceptionMessage()); - } - } - } - catch(const AbortException&) - { - // ignored - } - catch (const std::exception& ex) - { - wxMessageBox(ex.what(), _("Update error")); - } - return false; -} - -bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATED_BY initiatedBy) -{ - TitleInfo launchTitle{ launchPath }; - if (launchTitle.IsValid()) - { - // the title might not be in the TitleList, so we add it as a temporary entry - CafeTitleList::AddTitleFromPath(launchPath); - // title is valid, launch from TitleId - TitleId baseTitleId; - if (!CafeTitleList::FindBaseTitleId(launchTitle.GetAppTitleId(), baseTitleId)) - { - wxString t = _("Unable to launch game because the base files were not found."); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); - if (r == CafeSystem::PREPARE_STATUS_CODE::INVALID_RPX) - { - cemu_assert_debug(false); - return false; - } - else if (r == CafeSystem::PREPARE_STATUS_CODE::UNABLE_TO_MOUNT) - { - wxString t = _("Unable to mount title.\nMake sure the configured game paths are still valid and refresh the game list.\n\nFile which failed to load:\n"); - t.append(_pathToUtf8(launchPath)); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - else if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) - { - wxString t = _("Failed to launch game."); - t.append(_pathToUtf8(launchPath)); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - } - else //if (launchTitle.GetFormat() == TitleInfo::TitleDataFormat::INVALID_STRUCTURE ) - { - // title is invalid, if it's an RPX/ELF we can launch it directly - // otherwise it's an error - CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath); - if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF) - { - CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); - if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) - { - cemu_assert_debug(false); // todo - wxString t = _("Failed to launch executable. Path: "); - t.append(_pathToUtf8(launchPath)); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - } - else if (initiatedBy == wxLaunchGameEvent::INITIATED_BY::GAME_LIST) - { - wxString t = _("Unable to launch title.\nMake sure the configured game paths are still valid and refresh the game list.\n\nPath which failed to load:\n"); - t.append(_pathToUtf8(launchPath)); - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - else - { - wxString t = _("Unable to launch game\nPath:\n"); - t.append(_pathToUtf8(launchPath)); - if(launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_DISC_KEY) - { - t.append("\n\n"); - t.append(_("Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.")); - } - if(launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_TITLE_TIK) - { - t.append("\n\n"); - t.append(_("Could not decrypt title because title.tik is missing.")); - } - wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return false; - } - } - - if(launchTitle.IsValid()) - GetWxGUIConfig().AddRecentlyLaunchedFile(_pathToUtf8(launchTitle.GetPath())); - else - GetWxGUIConfig().AddRecentlyLaunchedFile(_pathToUtf8(launchPath)); - - wxWindowUpdateLocker lock(this); - - DestroyGameListAndStatusBar(); - - m_game_launched = true; - m_loadMenuItem->Enable(false); - m_installUpdateMenuItem->Enable(false); - m_memorySearcherMenuItem->Enable(true); - - m_launched_game_name = CafeSystem::GetForegroundTitleName(); - #ifdef ENABLE_DISCORD_RPC - if (m_discord) - m_discord->UpdatePresence(DiscordPresence::Playing, m_launched_game_name); - #endif - - if (GetConfig().disable_screensaver) - ScreenSaver::SetInhibit(true); - - if (FullscreenEnabled()) - SetFullScreen(true); - - //GameMode support -#if BOOST_OS_LINUX && defined(ENABLE_FERAL_GAMEMODE) - if(GetWxGUIConfig().feral_gamemode) - { - // attempt to start gamemode - if(gamemode_request_start() < 0) - { - // GameMode failed to start - cemuLog_log(LogType::Force, "Could not start GameMode"); - } - else - { - cemuLog_log(LogType::Force, "GameMode has been started."); - } - } -#endif - - CreateCanvas(); - CafeSystem::LaunchForegroundTitle(); - RecreateMenu(); - UpdateChildWindowTitleRunningState(); - - return true; -} - -void MainWindow::OnLaunchFromFile(wxLaunchGameEvent& event) -{ - if (event.GetPath().empty()) - return; - FileLoad(event.GetPath(), event.GetInitiatedBy()); -} - -void MainWindow::OnFileMenu(wxCommandEvent& event) -{ - const auto menuId = event.GetId(); - if (menuId == MAINFRAME_MENU_ID_FILE_LOAD) - { - const auto wildcard = formatWxString( - "{}|*.wud;*.wux;*.wua;*.wuhb;*.iso;*.rpx;*.elf;title.tmd" - "|{}|*.wud;*.wux;*.iso" - "|{}|title.tmd" - "|{}|*.wua" - "|{}|*.wuhb" - "|{}|*.rpx;*.elf" - "|{}|*", - _("All Wii U files (*.wud, *.wux, *.wua, *.wuhb, *.iso, *.rpx, *.elf)"), - _("Wii U image (*.wud, *.wux, *.iso, *.wad)"), - _("Wii U NUS content"), - _("Wii U archive (*.wua)"), - _("Wii U homebrew bundle (*.wuhb)"), - _("Wii U executable (*.rpx, *.elf)"), - _("All files (*.*)") - ); - - wxFileDialog openFileDialog(this, _("Open file to launch"), wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (openFileDialog.ShowModal() == wxID_CANCEL || openFileDialog.GetPath().IsEmpty()) - return; - - const wxString wxStrFilePath = openFileDialog.GetPath(); - FileLoad(_utf8ToPath(wxStrFilePath.utf8_string()), wxLaunchGameEvent::INITIATED_BY::MENU); - } - else if (menuId >= MAINFRAME_MENU_ID_FILE_RECENT_0 && menuId <= MAINFRAME_MENU_ID_FILE_RECENT_LAST) - { - const auto& config = GetWxGUIConfig(); - const size_t index = menuId - MAINFRAME_MENU_ID_FILE_RECENT_0; - if (index < config.recent_launch_files.size()) - { - fs::path path = _utf8ToPath(config.recent_launch_files[index]); - if (!path.empty()) - FileLoad(path, wxLaunchGameEvent::INITIATED_BY::MENU); - } - } - else if (menuId == MAINFRAME_MENU_ID_FILE_END_EMULATION) - { - EndEmulation(); - } -} - -void MainWindow::OnOpenFolder(wxCommandEvent& event) -{ - const auto id = event.GetId(); - if(id == MAINFRAME_MENU_ID_FILE_OPEN_CEMU_FOLDER) - wxLaunchDefaultApplication(wxHelper::FromPath(ActiveSettings::GetUserDataPath())); - else if(id == MAINFRAME_MENU_ID_FILE_OPEN_MLC_FOLDER) - wxLaunchDefaultApplication(wxHelper::FromPath(ActiveSettings::GetMlcPath())); - else if (id == MAINFRAME_MENU_ID_FILE_OPEN_SHADERCACHE_FOLDER) - wxLaunchDefaultApplication(wxHelper::FromPath(ActiveSettings::GetCachePath("shaderCache"))); -} - -void MainWindow::OnClearSpotPassCache(wxCommandEvent& event) -{ - fs::path bossPath = ActiveSettings::GetMlcPath("usr/boss"); - fs::remove_all(bossPath); - // recreate usr/boss/ - fs::create_directory(bossPath); - wxMessageBox(_("SpotPass cache cleared")); -} - -void MainWindow::OnInstallUpdate(wxCommandEvent& event) -{ - while (true) - { - wxDirDialog openDirDialog(nullptr, _("Select folder of title to install"), "", wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST, wxDefaultPosition, wxDefaultSize, _("Select the folder that stores your update, DLC or base game files")); - int modalChoice = openDirDialog.ShowModal(); - if (modalChoice == wxID_CANCEL || openDirDialog.GetPath().IsEmpty()) - break; - if (modalChoice == wxID_OK) - { - #if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD - fs::path dirPath((const char*)(openDirDialog.GetPath().fn_str())); - #else - fs::path dirPath(openDirDialog.GetPath().fn_str()); - #endif - - if ((dirPath.filename() == "code" || dirPath.filename() == "content" || dirPath.filename() == "meta") && dirPath.has_parent_path()) - { - if (!fs::exists(dirPath.parent_path() / "code") || !fs::exists(dirPath.parent_path() / "content") || !fs::exists(dirPath.parent_path() / "meta")) - { - wxMessageBox(formatWxString(_("The (parent) folder of the title you selected is missing at least one of the required subfolders (\"code\", \"content\" and \"meta\")\nMake sure that the files are complete."), dirPath.filename().string())); - continue; - } - else - dirPath = dirPath.parent_path(); - } - - if (!fs::exists(dirPath)) - wxMessageBox(_("The folder you have selected cannot be found on your system.")); - else if (!fs::exists(dirPath / "meta" / "meta.xml")) - wxMessageBox(_("Unable to find the /meta/meta.xml file inside the selected folder.")); - else - { - InstallUpdate(dirPath); - return; - } - } - } -} - -void MainWindow::OnNFCMenu(wxCommandEvent& event) -{ - if (event.GetId() == MAINFRAME_MENU_ID_NFC_TOUCH_NFC_FILE) - { - wxFileDialog - openFileDialog(this, _("Open file to load"), "", "", - "All NFC files (bin, dat, nfc)|*.bin;*.dat;*.nfc|All files (*.*)|*", wxFD_OPEN | wxFD_FILE_MUST_EXIST); // TRANSLATE - if (openFileDialog.ShowModal() == wxID_CANCEL || openFileDialog.GetPath().IsEmpty()) - return; - wxString wxStrFilePath = openFileDialog.GetPath(); - uint32 nfcError; - if (nfc::TouchTagFromFile(_utf8ToPath(wxStrFilePath.utf8_string()), &nfcError) == false) - { - if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) - wxMessageBox(_("Cannot open file")); - else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC file")); - } - else - { - GetWxGUIConfig().AddRecentNfcFile(wxStrFilePath.utf8_string()); - UpdateNFCMenu(); - } - } - else if (event.GetId() >= MAINFRAME_MENU_ID_NFC_RECENT_0 && event.GetId() <= MAINFRAME_MENU_ID_NFC_RECENT_LAST) - { - const size_t index = event.GetId() - MAINFRAME_MENU_ID_NFC_RECENT_0; - auto& config = GetWxGUIConfig(); - if (index < config.recent_nfc_files.size()) - { - const auto& path = config.recent_nfc_files[index]; - if (!path.empty()) - { - uint32 nfcError = 0; - if (nfc::TouchTagFromFile(_utf8ToPath(path), &nfcError) == false) - { - if (nfcError == NFC_TOUCH_TAG_ERROR_NO_ACCESS) - wxMessageBox(_("Cannot open file")); - else if (nfcError == NFC_TOUCH_TAG_ERROR_INVALID_FILE_FORMAT) - wxMessageBox(_("Not a valid NFC file")); - } - else - { - config.AddRecentNfcFile(path); - UpdateNFCMenu(); - } - } - } - } -} - -void MainWindow::OnFileExit(wxCommandEvent& event) -{ - Close(); -} - -void MainWindow::TogglePadView() -{ - const auto& config = GetWxGUIConfig(); - if (config.pad_open) - { - if (m_padView) - return; - - m_padView = new PadViewFrame(this); - - m_padView->Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnPadClose, this); - - m_padView->Show(true); - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - if (wxWlIsWaylandWindow(m_padView)) - wxWlSetAppId(m_padView, "info.cemu.Cemu"); -#endif - - m_padView->Initialize(); - if (m_game_launched) - m_padView->InitializeRenderCanvas(); - } - else if (m_padView) - { - m_padView->Destroy(); - m_padView = nullptr; - } -} - -#if BOOST_OS_WINDOWS - -#ifndef DBT_DEVNODES_CHANGED -#define DBT_DEVNODES_CHANGED (0x0007) -#endif -WXLRESULT MainWindow::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) -{ - if (nMsg == WM_DEVICECHANGE) - { - if (wParam == DBT_DEVNODES_CHANGED) - { - InputManager::instance().on_device_changed(); - } - } - - return wxFrame::MSWWindowProc(nMsg, wParam, lParam); -} -#endif - -void MainWindow::OpenSettings() -{ - auto& config = GetWxGUIConfig(); - const auto language = config.language; - - GeneralSettings2 frame(this, m_game_launched); - frame.ShowModal(); - const bool paths_modified = frame.ShouldReloadGamelist(); - const bool mlc_modified = frame.MLCModified(); - - if (paths_modified) - m_game_list->ReloadGameEntries(); - else - SaveSettings(); - - #ifdef ENABLE_DISCORD_RPC - if (config.use_discord_presence) - { - if (!m_discord) - { - m_discord = std::make_unique(); - if (!m_launched_game_name.empty()) - m_discord->UpdatePresence(DiscordPresence::Playing, m_launched_game_name); - } - } - else - m_discord.reset(); - #endif - - if(config.check_update && !m_game_launched) - m_update_available = CemuUpdateWindow::IsUpdateAvailableAsync(); - - if (mlc_modified) - RecreateMenu(); - - if (!config.fullscreen_menubar && IsFullScreen()) - SetMenuVisible(false); - - if (language != config.language) - wxMessageBox(_("Cemu must be restarted to apply the selected UI language."), _("Information"), wxOK | wxCENTRE, this); // TODO: change language to newly selected one -} - -void MainWindow::OnOptionsInput(wxCommandEvent& event) -{ - switch (event.GetId()) - { - case MAINFRAME_MENU_ID_OPTIONS_FULLSCREEN: - { - const bool state = m_fullscreenMenuItem->IsChecked(); - SetFullScreen(state); - break; - } - case MAINFRAME_MENU_ID_OPTIONS_SECOND_WINDOW_PADVIEW: - { - GetWxGUIConfig().pad_open = !GetWxGUIConfig().pad_open; - g_wxConfig.Save(); - - TogglePadView(); - break; - } - case MAINFRAME_MENU_ID_OPTIONS_GRAPHIC_PACKS2: - { - if (m_graphic_pack_window) - return; - - uint64 titleId = 0; - if (CafeSystem::IsTitleRunning()) - titleId = CafeSystem::GetForegroundTitleId(); - - m_graphic_pack_window = new GraphicPacksWindow2(this, titleId); - m_graphic_pack_window->Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnGraphicWindowClose, this); - m_graphic_pack_window->Show(true); - - break; - } - - case MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS: - case MAINFRAME_MENU_ID_OPTIONS_GENERAL2: - { - OpenSettings(); - break; - } - case MAINFRAME_MENU_ID_OPTIONS_INPUT: - { - auto* frame = new InputSettings2(this); - frame->ShowModal(); - frame->Destroy(); - break; - } - - case MAINFRAME_MENU_ID_OPTIONS_HOTKEY: - { - auto* frame = new HotkeySettings(this); - frame->Show(); - break; - } - } -} - -void MainWindow::OnAccountSelect(wxCommandEvent& event) -{ - const int index = event.GetId() - MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1; - const auto& accounts = Account::GetAccounts(); - wxASSERT(index >= 0 && index < (int)accounts.size()); - auto& config = GetConfig(); - config.account.m_persistent_id = accounts[index].GetPersistentId(); - // config.account.online_enabled.value = false; // reset online for safety - GetConfigHandle().Save(); -} - -void MainWindow::OnConsoleLanguage(wxCommandEvent& event) -{ - switch (event.GetId()) - { - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_JAPANESE: - GetConfig().console_language = CafeConsoleLanguage::JA; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ENGLISH: - GetConfig().console_language = CafeConsoleLanguage::EN; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_FRENCH: - GetConfig().console_language = CafeConsoleLanguage::FR; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_GERMAN: - GetConfig().console_language = CafeConsoleLanguage::DE; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ITALIAN: - GetConfig().console_language = CafeConsoleLanguage::IT; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_SPANISH: - GetConfig().console_language = CafeConsoleLanguage::ES; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_CHINESE: - GetConfig().console_language = CafeConsoleLanguage::ZH; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_KOREAN: - GetConfig().console_language = CafeConsoleLanguage::KO; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_DUTCH: - GetConfig().console_language = CafeConsoleLanguage::NL; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_PORTUGUESE: - GetConfig().console_language = CafeConsoleLanguage::PT; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_RUSSIAN: - GetConfig().console_language = CafeConsoleLanguage::RU; - break; - case MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_TAIWANESE: - GetConfig().console_language = CafeConsoleLanguage::TW; - break; - default: - cemu_assert_debug(false); - } - if (m_game_list) - { - m_game_list->DeleteCachedStrings(); - m_game_list->ReloadGameEntries(); - } - GetConfigHandle().Save(); -} - -//void MainWindow::OnCPUMode(wxCommandEvent& event) -//{ -// if (event.GetId() == MAINFRAME_MENU_ID_CPU_MODE_SINGLECORE_INTERPRETER) -// GetConfig().cpu_mode = CPUMode::SinglecoreInterpreter; -// else if (event.GetId() == MAINFRAME_MENU_ID_CPU_MODE_SINGLECORE_RECOMPILER) -// GetConfig().cpu_mode = CPUMode::SinglecoreRecompiler; -// else if (event.GetId() == MAINFRAME_MENU_ID_CPU_MODE_DUALCORE_RECOMPILER) -// GetConfig().cpu_mode = CPUMode::DualcoreRecompiler; -// else if (event.GetId() == MAINFRAME_MENU_ID_CPU_MODE_TRIPLECORE_RECOMPILER) -// GetConfig().cpu_mode = CPUMode::TriplecoreRecompiler; -// else -// cemu_assert_debug(false); -// -// GetConfigHandle().Save(); -//} - -void MainWindow::OnDebugSetting(wxCommandEvent& event) -{ - if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN) - GetConfig().render_upside_down = event.IsChecked(); - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS) - { - GetConfig().vk_accurate_barriers = event.IsChecked(); - if(!GetConfig().vk_accurate_barriers) - wxMessageBox(_("Warning: Disabling the accurate barriers option will lead to flickering graphics but may improve performance. It is highly recommended to leave it turned on."), _("Accurate barriers are off"), wxOK); - } -#ifdef ENABLE_METAL - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE) - { - cemu_assert_debug(g_renderer->GetType() == RendererAPI::Metal); - static_cast(g_renderer.get())->CaptureFrame(); - } -#endif - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY) - ActiveSettings::EnableAudioOnlyAux(event.IsChecked()); - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_DUMP_RAM) - memory_createDump(); - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_DUMP_RPX) - CafeSystem::DumpCurrentRPX(); - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_DUMP_FST) - { - /* int msgBoxAnswer = wxMessageBox(_("All files from the currently running game will be dumped to /dump/. This process can take a few minutes."), - _("Dump WUD"), wxOK | wxCANCEL | wxICON_WARNING); - if (msgBoxAnswer == wxOK) - { - volumeFST_dump(bootGame_getMountedWUD()); - wxMessageBox(_("Dump complete")); - }*/ - } - else if (event.GetId() == MAINFRAME_MENU_ID_DEBUG_DUMP_CURL_REQUESTS) - { - // toggle debug -> dump -> curl requests - const bool state = event.IsChecked(); - ActiveSettings::EnableDumpLibcurlRequests(state); - if (state) - { - try - { - const fs::path path(ActiveSettings::GetUserDataPath()); - fs::create_directories(path / "dump" / "curl"); - } - catch (const std::exception& ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "error when creating dump curl folder: {}", sys.what()); - ActiveSettings::EnableDumpLibcurlRequests(false); - } - } - } - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_8X) - ActiveSettings::SetTimerShiftFactor(0); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_4X) - ActiveSettings::SetTimerShiftFactor(1); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_2X) - ActiveSettings::SetTimerShiftFactor(2); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_1X) - ActiveSettings::SetTimerShiftFactor(3); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_05X) - ActiveSettings::SetTimerShiftFactor(4); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_025X) - ActiveSettings::SetTimerShiftFactor(5); - else if (event.GetId() == MAINFRAME_MENU_ID_TIMER_SPEED_0125X) - ActiveSettings::SetTimerShiftFactor(6); - else - cemu_assert_debug(false); - - GetConfigHandle().Save(); -} - -void MainWindow::OnDebugLoggingToggleFlagGeneric(wxCommandEvent& event) -{ - sint32 loggingIdBase = MAINFRAME_MENU_ID_DEBUG_LOGGING0; - - sint32 id = event.GetId(); - if (id >= loggingIdBase && id < (MAINFRAME_MENU_ID_DEBUG_LOGGING0 + 64)) - { - bool isEnable = event.IsChecked(); - LogType loggingType = static_cast(id - loggingIdBase); - if (isEnable) - GetConfig().log_flag = GetConfig().log_flag.GetValue() | cemuLog_getFlag(loggingType); - else - GetConfig().log_flag = GetConfig().log_flag.GetValue() & ~cemuLog_getFlag(loggingType); - cemuLog_setActiveLoggingFlags(GetConfig().log_flag.GetValue()); - GetConfigHandle().Save(); - } -} - -void MainWindow::OnPPCInfoToggle(wxCommandEvent& event) -{ - GetConfig().advanced_ppc_logging = !GetConfig().advanced_ppc_logging.GetValue(); - GetConfigHandle().Save(); -} - -void MainWindow::OnDebugDumpGeneric(wxCommandEvent& event) -{ - std::string dumpSubpath; - std::function setDumpState; - switch(event.GetId()) - { - case MAINFRAME_MENU_ID_DEBUG_DUMP_TEXTURES: - dumpSubpath = "dump/textures"; - setDumpState = ActiveSettings::EnableDumpTextures; - break; - case MAINFRAME_MENU_ID_DEBUG_DUMP_SHADERS: - dumpSubpath = "dump/shaders"; - setDumpState = ActiveSettings::EnableDumpShaders; - break; - case MAINFRAME_MENU_ID_DEBUG_DUMP_RECOMPILER_FUNCTIONS: - dumpSubpath = "dump/recompiler"; - setDumpState = ActiveSettings::EnableDumpRecompilerFunctions; - break; - default: - UNREACHABLE; - } - const bool value = event.IsChecked(); - setDumpState(value); - if (value) - { - try - { - fs::create_directories(ActiveSettings::GetUserDataPath(dumpSubpath)); - } - catch (const std::exception & ex) - { - SystemException sys(ex); - cemuLog_log(LogType::Force, "can't create folder {} in user data folder: {}", dumpSubpath, ex.what()); - setDumpState(false); - } - } -} - -void MainWindow::OnLoggingWindow(wxCommandEvent& event) -{ - if(m_logging_window) - return; - - m_logging_window = new LoggingWindow(this); - m_logging_window->Bind(wxEVT_CLOSE_WINDOW, - [this](wxCloseEvent& event) { - m_logging_window = nullptr; - event.Skip(); - }); - m_logging_window->Show(true); -} - -void MainWindow::OnGDBStubToggle(wxCommandEvent& event) -{ - if (g_gdbstub) - { - g_gdbstub.release(); - return; - } - - const auto& config = GetConfig(); - g_gdbstub = std::make_unique(config.gdb_port); -} - -void MainWindow::OnDebugViewPPCThreads(wxCommandEvent& event) -{ - auto frame = new DebugPPCThreadsWindow(*this); - frame->Show(true); -} - -void MainWindow::OnDebugViewPPCDebugger(wxCommandEvent& event) -{ - if (m_debugger_window && m_debugger_window->IsShown()) - { - m_debugger_window->Close(); - m_debugger_window = nullptr; - return; - } - - OpenPPCDebugger(); -} - -void MainWindow::OpenPPCDebugger() -{ - if (m_debugger_window) - return; - - auto rect = GetDesktopRect(); - /* - sint32 new_width = max(rect.GetWidth() * 0.70, rect.GetWidth() - 850); - this->SetSize(new_width, 480);*/ - - this->SetSize(800, 450 + 50); - this->CenterOnScreen(); - - auto pos = this->GetPosition(); - pos.y = std::min(pos.y + 200, rect.GetHeight() - 400); - this->SetPosition(pos); - - m_debugger_window = new DebuggerWindow2(*this, rect); - m_debugger_window->Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnDebuggerClose, this); - m_debugger_window->Show(true); -} - -void MainWindow::OnDebugViewAudioDebugger(wxCommandEvent& event) -{ - auto frame = new AudioDebuggerWindow(*this); - frame->Show(true); -} - -void MainWindow::OnDebugViewTextureRelations(wxCommandEvent& event) -{ - openTextureViewer(*this); -} - -void MainWindow::ShowCursor(bool state) -{ - #if BOOST_OS_WINDOWS - CURSORINFO info{}; - info.cbSize = sizeof(CURSORINFO); - GetCursorInfo(&info); - const bool visible = info.flags == CURSOR_SHOWING; - - if (state == visible) - return; - - int counter = 0; - if(state) - { - do - { - counter = ::ShowCursor(TRUE); - } while (counter < 0); - } - else - { - do - { - counter = ::ShowCursor(FALSE); - } while (counter >= 0); - } - #else - if (state) - { - wxSetCursor(wxNullCursor); // restore system default cursor - } - else - { - wxSetCursor(wxCursor(wxCURSOR_BLANK)); - } - #endif -} - -uintptr_t MainWindow::GetRenderCanvasHWND() -{ - // deprecated. We can use the global cross-platform window info structs now - #if BOOST_OS_WINDOWS - if (!m_render_canvas) - return 0; - return (uintptr_t)m_render_canvas->GetHWND(); - #else - return 0; - #endif -} - -wxRect MainWindow::GetDesktopRect() -{ - const auto pos = GetPosition(); - const auto middle = pos.x + GetSize().GetWidth() / 2; - - const auto displayCount = wxDisplay::GetCount(); - for (uint32 i = 0; i < displayCount; ++i) - { - wxDisplay display(i); - if (!display.IsOk()) - continue; - - const auto geo = display.GetGeometry(); - if (geo.x <= middle && middle <= geo.x + geo.width) - return geo; - } - return { 0,0,800,600 }; -} - -void MainWindow::LoadSettings() -{ - GetConfigHandle().Load(); - const auto& config = GetWxGUIConfig(); - - if(config.check_update) - m_update_available = CemuUpdateWindow::IsUpdateAvailableAsync(); - - if (config.window_position != Vector2i{ -1,-1 }) - this->SetPosition({ config.window_position.x, config.window_position.y }); - - if (config.window_size.x > 0 && config.window_size.y > 0) - { - this->SetSize({ config.window_size.x, config.window_size.y }); - - if (config.window_maximized) - this->Maximize(); - } - - if (config.pad_position != Vector2i{ -1,-1 }) - { - g_window_info.restored_pad_x = config.pad_position.x; - g_window_info.restored_pad_y = config.pad_position.y; - } - - if (config.pad_size != Vector2i{ -1,-1 }) - { - g_window_info.restored_pad_width = config.pad_size.x; - g_window_info.restored_pad_height = config.pad_size.y; - - g_window_info.pad_maximized = config.pad_maximized; - } - - this->TogglePadView(); - - if(m_game_list) - m_game_list->LoadConfig(); -} - -void MainWindow::SaveSettings() -{ - auto lock = GetConfigHandle().Lock(); - auto& config = GetWxGUIConfig(); - - if (config.window_position != Vector2i{ -1,-1 }) - { - config.window_position.x = m_restored_position.x; - config.window_position.y = m_restored_position.y; - } - if (config.window_size != Vector2i{ -1,-1 }) - { - config.window_size.x = m_restored_size.x; - config.window_size.y = m_restored_size.y; - config.window_maximized = IsMaximized(); - } - else - { - config.window_maximized = false; - } - - config.pad_open = m_padView != nullptr; - - if (config.pad_position != Vector2i{ -1,-1 } && g_window_info.restored_pad_x != -1) - { - config.pad_position.x = g_window_info.restored_pad_x; - config.pad_position.y = g_window_info.restored_pad_y; - } - if (config.pad_size != Vector2i{ -1,-1 } && g_window_info.restored_pad_width != -1) - { - config.pad_size.x = g_window_info.restored_pad_width; - config.pad_size.y = g_window_info.restored_pad_height; - config.pad_maximized = g_window_info.pad_maximized; - } - else - { - config.pad_maximized = false; - } - - if(m_game_list) - m_game_list->SaveConfig(); - - g_wxConfig.Save(); -} - -void MainWindow::OnMouseMove(wxMouseEvent& event) -{ - event.Skip(); - - m_last_mouse_move_time = std::chrono::steady_clock::now(); - m_mouse_position = wxGetMousePosition(); - ShowCursor(true); - - auto& instance = InputManager::instance(); - std::unique_lock lock(instance.m_main_mouse.m_mutex); - auto physPos = ToPhys(event.GetPosition()); - instance.m_main_mouse.position = { physPos.x, physPos.y }; - lock.unlock(); - - if (!IsFullScreen()) - return; - - const auto& config = GetWxGUIConfig(); - // if mouse goes to upper screen then show our menu in fullscreen mode - if (config.fullscreen_menubar) - SetMenuVisible(event.GetPosition().y < 50); -} - -void MainWindow::OnMouseLeft(wxMouseEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_main_mouse.m_mutex); - instance.m_main_mouse.left_down = event.ButtonDown(wxMOUSE_BTN_LEFT); - auto physPos = ToPhys(event.GetPosition()); - instance.m_main_mouse.position = { physPos.x, physPos.y }; - if (event.ButtonDown(wxMOUSE_BTN_LEFT)) - instance.m_main_mouse.left_down_toggle = true; - - event.Skip(); -} - -void MainWindow::OnMouseRight(wxMouseEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_main_mouse.m_mutex); - instance.m_main_mouse.right_down = event.ButtonDown(wxMOUSE_BTN_RIGHT); - auto physPos = ToPhys(event.GetPosition()); - instance.m_main_mouse.position = { physPos.x, physPos.y }; - if(event.ButtonDown(wxMOUSE_BTN_RIGHT)) - instance.m_main_mouse.right_down_toggle = true; - - event.Skip(); -} - -void MainWindow::OnGameListBeginUpdate(wxCommandEvent& event) -{ - if (m_game_list->IsShown()) - m_info_bar->ShowMessage(_("Updating game list...")); -} - -void MainWindow::OnGameListEndUpdate(wxCommandEvent& event) -{ - m_info_bar->Dismiss(); -} - -void MainWindow::OnAccountListRefresh(wxCommandEvent& event) -{ - RecreateMenu(); -} - -void MainWindow::OnRequestGameListRefresh(wxCommandEvent& event) -{ - m_game_list->ReloadGameEntries(); -} - -void MainWindow::OnSetWindowTitle(wxCommandEvent& event) -{ - this->SetTitle(event.GetString()); -} - -void MainWindow::OnKeyUp(wxKeyEvent& event) -{ - event.Skip(); - - if (swkbd_hasKeyboardInputHook()) - return; - - HotkeySettings::CaptureInput(event); -} - -void MainWindow::OnKeyDown(wxKeyEvent& event) -{ -#if defined(__APPLE__) - // On macOS, allow Cmd+Q to quit the application - if (event.CmdDown() && event.GetKeyCode() == 'Q') - { - Close(true); - } -#else - // On Windows/Linux, only Alt+F4 is allowed for quitting - if (event.AltDown() && event.GetKeyCode() == WXK_F4) - { - Close(true); - } -#endif - else - { - event.Skip(); - } -} - -void MainWindow::OnChar(wxKeyEvent& event) -{ - if (swkbd_hasKeyboardInputHook()) - swkbd_keyInput(event.GetUnicodeKey()); - - // event.Skip(); -} - -void MainWindow::OnToolsInput(wxCommandEvent& event) -{ - const auto id = event.GetId(); - switch (id) - { - case MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER: - { - if (m_toolWindow) - m_toolWindow->SetFocus(); - else - { - m_toolWindow = new MemorySearcherTool(this); - m_toolWindow->Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnMemorySearcherClose, this); - m_toolWindow->Show(true); - } - break; - } - case MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER: - case MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER: - { - const auto default_tab = id == MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER ? TitleManagerPage::TitleManager : TitleManagerPage::DownloadManager; - - if (m_title_manager) - m_title_manager->SetFocusAndTab(default_tab); - else - { - m_title_manager = new TitleManager(this, default_tab); - m_title_manager->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) - { - m_title_manager = nullptr; - event.Skip(); - }); - m_title_manager->Show(); - } - break; - } - case MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES: - { - if (m_usb_devices) - { - m_usb_devices->Show(true); - m_usb_devices->Raise(); - m_usb_devices->SetFocus(); - } - else - { - m_usb_devices = new EmulatedUSBDeviceFrame(this); - m_usb_devices->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) - { - if (event.CanVeto()) { - m_usb_devices->Show(false); - event.Veto(); - } - }); - m_usb_devices->Show(true); - } - break; - } - break; - } -} - -void MainWindow::OnGesturePan(wxPanGestureEvent& event) -{ - auto& instance = InputManager::instance(); - std::scoped_lock lock(instance.m_main_touch.m_mutex); - auto physPos = ToPhys(event.GetPosition()); - instance.m_main_touch.position = { physPos.x, physPos.y }; - instance.m_main_touch.left_down = event.IsGestureStart() || !event.IsGestureEnd(); - if (event.IsGestureStart() || !event.IsGestureEnd()) - instance.m_main_touch.left_down_toggle = true; - - - event.Skip(); -} - -void MainWindow::OnGameLoaded() -{ - if (m_debugger_window) - m_debugger_window->OnGameLoaded(); -} - -void MainWindow::AsyncSetTitle(std::string_view windowTitle) -{ - wxCommandEvent set_title_event(wxEVT_SET_WINDOW_TITLE); - set_title_event.SetString(wxString::FromUTF8(windowTitle)); - g_mainFrame->QueueEvent(set_title_event.Clone()); -} - -void MainWindow::CreateCanvas() -{ - // create panel for canvas - m_game_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxNO_BORDER | wxWANTS_CHARS); - auto* sizer = new wxBoxSizer(wxVERTICAL); - - // shouldn't be needed, but who knows - m_game_panel->Bind(wxEVT_KEY_UP, &MainWindow::OnKeyUp, this); - m_game_panel->Bind(wxEVT_CHAR, &MainWindow::OnChar, this); - - m_game_panel->SetSizer(sizer); - this->GetSizer()->Add(m_game_panel, 1, wxEXPAND); - - // create canvas - #ifdef ENABLE_OPENGL - if (ActiveSettings::GetGraphicsAPI() == kOpenGL) - m_render_canvas = GLCanvas_Create(m_game_panel, wxSize(1280, 720), true); - #endif - #ifdef ENABLE_VULKAN - if (ActiveSettings::GetGraphicsAPI() == kVulkan) - m_render_canvas = new VulkanCanvas(m_game_panel, wxSize(1280, 720), true); - #endif - #ifdef ENABLE_METAL - if (ActiveSettings::GetGraphicsAPI() == kMetal) - m_render_canvas = new MetalCanvas(m_game_panel, wxSize(1280, 720), true); - #endif - if (!m_render_canvas) - cemu_assert(false && "Failed to create canvas or invalid graphics API selected"); - cemu_assert(m_render_canvas != nullptr); - - // mouse events - m_render_canvas->Bind(wxEVT_MOTION, &MainWindow::OnMouseMove, this); - m_render_canvas->Bind(wxEVT_MOUSEWHEEL, &MainWindow::OnMouseWheel, this); - m_render_canvas->Bind(wxEVT_LEFT_DOWN, &MainWindow::OnMouseLeft, this); - m_render_canvas->Bind(wxEVT_LEFT_UP, &MainWindow::OnMouseLeft, this); - m_render_canvas->Bind(wxEVT_RIGHT_DOWN, &MainWindow::OnMouseRight, this); - m_render_canvas->Bind(wxEVT_RIGHT_UP, &MainWindow::OnMouseRight, this); - - m_render_canvas->Bind(wxEVT_GESTURE_PAN, &MainWindow::OnGesturePan, this); - - // key events - m_render_canvas->Bind(wxEVT_KEY_UP, &MainWindow::OnKeyUp, this); - m_render_canvas->Bind(wxEVT_KEY_DOWN, &MainWindow::OnKeyDown, this); - m_render_canvas->Bind(wxEVT_CHAR, &MainWindow::OnChar, this); - - m_render_canvas->SetDropTarget(new wxAmiiboDropTarget(this)); - m_game_panel->GetSizer()->Add(m_render_canvas, 1, wxEXPAND, 0, nullptr); - - GetSizer()->Layout(); - m_render_canvas->SetFocus(); - - if (m_padView) - m_padView->InitializeRenderCanvas(); -} - -void MainWindow::DestroyCanvas() -{ - if (m_padView) - { - m_padView->DestroyCanvas(); - } - if (m_render_canvas) - { - m_render_canvas->Destroy(); - m_render_canvas = nullptr; - } - if(m_game_panel) - { - m_game_panel->Destroy(); - m_game_panel = nullptr; - } -} - -void MainWindow::OnSizeEvent(wxSizeEvent& event) -{ - if (!IsMaximized() && !WindowSystem::IsFullScreen()) - m_restored_size = GetSize(); - - const wxSize client_size = GetClientSize(); - g_window_info.width = client_size.GetWidth(); - g_window_info.height = client_size.GetHeight(); - g_window_info.phys_width = ToPhys(client_size.GetWidth()); - g_window_info.phys_height = ToPhys(client_size.GetHeight()); - g_window_info.dpi_scale = GetDPIScaleFactor(); - - if (m_debugger_window && m_debugger_window->IsShown()) - m_debugger_window->OnParentMove(GetPosition(), event.GetSize()); - - event.Skip(); - - #ifdef ENABLE_VULKAN - VsyncDriver_notifyWindowPosChanged(); - #endif -} - -void MainWindow::OnDPIChangedEvent(wxDPIChangedEvent& event) -{ - event.Skip(); - const wxSize client_size = GetClientSize(); - g_window_info.width = client_size.GetWidth(); - g_window_info.height = client_size.GetHeight(); - g_window_info.phys_width = ToPhys(client_size.GetWidth()); - g_window_info.phys_height = ToPhys(client_size.GetHeight()); - g_window_info.dpi_scale = GetDPIScaleFactor(); -} - -void MainWindow::OnMove(wxMoveEvent& event) -{ - if (!IsMaximized() && !WindowSystem::IsFullScreen()) - m_restored_position = GetPosition(); - - if (m_debugger_window && m_debugger_window->IsShown()) - m_debugger_window->OnParentMove(GetPosition(), GetSize()); - #ifdef ENABLE_VULKAN - VsyncDriver_notifyWindowPosChanged(); - #endif -} - -void MainWindow::OnDebuggerClose(wxCloseEvent& event) -{ - m_debugger_window = nullptr; - event.Skip(); -} - -void MainWindow::OnPadClose(wxCloseEvent& event) -{ - auto& config = GetWxGUIConfig(); - config.pad_open = false; - if (config.pad_position != Vector2i{ -1,-1 }) - m_padView->GetPosition(&config.pad_position.x, &config.pad_position.y); - - if (config.pad_size != Vector2i{ -1,-1 }) - m_padView->GetSize(&config.pad_size.x, &config.pad_size.y); - - g_wxConfig.Save(); - - // already deleted by wxwidget - m_padView = nullptr; - - if (m_padViewMenuItem) - m_padViewMenuItem->Check(false); - - event.Skip(); -} - -void MainWindow::OnMemorySearcherClose(wxCloseEvent& event) -{ - m_toolWindow = nullptr; - event.Skip(); -} - -void MainWindow::OnMouseWheel(wxMouseEvent& event) -{ - const float delta = event.GetWheelRotation(); // in 120 steps -> max reached ~480 (?) - auto& instance = InputManager::instance(); - instance.m_mouse_wheel = (delta / 120.0f); - - event.Skip(); -} - -void MainWindow::SetFullScreen(bool state) -{ - // only update config entry if we dont't have launch parameters - if (!LaunchSettings::FullscreenEnabled().has_value()) - { - GetWxGUIConfig().fullscreen = state; - g_wxConfig.Save(); - } - if (state && !m_game_launched) - return; - g_window_info.is_fullscreen = state; - m_fullscreenMenuItem->Check(state); - - this->ShowFullScreen(state); - - if (state) - m_menu_visible = false; // menu gets always disabled by wxFULLSCREEN_NOMENUBAR - else - SetMenuVisible(true); -} - -void MainWindow::EndEmulation() // unfinished - memory leaks and crashes after repeated use (after 3x usually) -{ - CafeSystem::ShutdownTitle(); - DestroyCanvas(); - m_game_launched = false; - m_launched_game_name.clear(); - #ifdef ENABLE_DISCORD_RPC - if (m_discord) - m_discord->UpdatePresence(DiscordPresence::Idling, ""); - #endif - - if (GetConfig().disable_screensaver) - ScreenSaver::SetInhibit(false); - - // close memory searcher if created - if (m_toolWindow) - { - m_toolWindow->Close(); - m_toolWindow = nullptr; - m_memorySearcherMenuItem->Enable(false); - } - - RecreateMenu(); - CreateGameListAndStatusBar(); - DoLayout(); - UpdateChildWindowTitleRunningState(); -} - -void MainWindow::SetMenuVisible(bool state) -{ - if (m_menu_visible == state) - return; - -#if !BOOST_OS_MACOS // on macOS hiding the menu seems to cause issues (see #609) - SetMenuBar(state ? m_menuBar : nullptr); -#endif - m_menu_visible = state; -} - -void MainWindow::UpdateNFCMenu() -{ - if (m_nfcMenuSeparator0) - { - m_nfcMenu->Remove(m_nfcMenuSeparator0->GetId()); - m_nfcMenuSeparator0 = nullptr; - } - // remove recent files list - for (sint32 i = 0; i < wxCemuConfig::kMaxRecentEntries; i++) - { - if (m_nfcMenu->FindChildItem(MAINFRAME_MENU_ID_NFC_RECENT_0 + i) == nullptr) - continue; - m_nfcMenu->Remove(MAINFRAME_MENU_ID_NFC_RECENT_0 + i); - } - // add entries - const auto& config = GetWxGUIConfig(); - sint32 recentFileIndex = 0; - for (size_t i = 0; i < config.recent_nfc_files.size(); i++) - { - const auto& entry = config.recent_nfc_files[i]; - if (entry.empty()) - continue; - - if (!fs::exists(_utf8ToPath(entry))) - continue; - - if (recentFileIndex == 0) - m_nfcMenuSeparator0 = m_nfcMenu->AppendSeparator(); - - m_nfcMenu->Append(MAINFRAME_MENU_ID_NFC_RECENT_0 + i, formatWxString("{}. {}", recentFileIndex, entry)); - - recentFileIndex++; - if (recentFileIndex >= 12) - break; - } -} - -bool MainWindow::IsMenuHidden() const -{ - return m_menu_visible; -} - -void MainWindow::OnTimer(wxTimerEvent& event) -{ - if(m_update_available.valid() && future_is_ready(m_update_available)) - { - if(m_update_available.get()) - { - wxMessageDialog dialog(this, _("There's a new update available.\nDo you want to update?"), _("Update notification"), wxCENTRE | wxYES_NO); - if(dialog.ShowModal() == wxID_YES) - { - CemuUpdateWindow update_window(this); - update_window.ShowModal(); - } - } - - m_update_available = {}; - } - - if (!IsFullScreen() || m_menu_visible) - return; - - const auto mouse_position = wxGetMousePosition(); - if(m_mouse_position != mouse_position) - { - m_last_mouse_move_time = std::chrono::steady_clock::now(); - m_mouse_position = mouse_position; - ShowCursor(true); - return; - } - - auto diff = std::chrono::duration_cast(std::chrono::steady_clock::now() - m_last_mouse_move_time); - if (diff.count() > 3000) - { - ShowCursor(false); - } - -} - -#define BUILD_DATE __DATE__ " " __TIME__ - -class CemuAboutDialog : public wxDialog -{ -public: - CemuAboutDialog(wxWindow* parent = NULL) - : wxDialog(NULL, wxID_ANY, _("About Cemu"), wxDefaultPosition, wxSize(500, 700)) - { - Create(parent); - } - - void Create(wxWindow* parent = NULL) - { - SetIcon(wxICON(M_WND_ICON128)); - - wxScrolledWindow* scrolledWindow = new wxScrolledWindow(this); - - wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); - - m_scrolledSizer = new wxBoxSizer(wxVERTICAL); - - AddHeaderInfo(scrolledWindow, m_scrolledSizer); - m_scrolledSizer->AddSpacer(5); - AddLibInfo(scrolledWindow, m_scrolledSizer); - m_scrolledSizer->AddSpacer(5); - AddThanks(scrolledWindow, m_scrolledSizer); - - scrolledWindow->SetSizer(m_scrolledSizer); - scrolledWindow->FitInside(); - scrolledWindow->SetScrollRate(25, 25); - mainSizer->Add(scrolledWindow, wxSizerFlags(1).Expand().Border(wxLEFT, 10)); - - SetSizer(mainSizer); - CentreOnParent(); - } - - void AddHeaderInfo(wxWindow* parent, wxSizer* sizer) - { - auto versionString = formatWxString(_("Cemu\nVersion {0}\nCompiled on {1}\nOriginal authors: {2}"), BUILD_VERSION_STRING, BUILD_DATE, "Exzap, Petergov"); - - sizer->Add(new wxStaticText(parent, wxID_ANY, versionString), wxSizerFlags().Border(wxALL, 3).Border(wxTOP, 10)); - sizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://cemu.info", "https://cemu.info", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT)), wxSizerFlags().Expand().Border(wxTOP | wxBOTTOM, 3)); - - sizer->AddSpacer(3); - sizer->Add(new wxStaticLine(parent), wxSizerFlags().Expand().Border(wxRIGHT, 4)); - sizer->AddSpacer(5); - - wxString extraInfo(_("Cemu is a Wii U emulator.\n\nWii and Wii U are trademarks of Nintendo.\nCemu is not affiliated with Nintendo.")); - sizer->Add(new wxStaticText(parent, wxID_ANY, extraInfo), wxSizerFlags()); - } - - void AddLibInfo(wxWindow* parent, wxSizer* sizer) - { - sizer->AddSpacer(3); - sizer->Add(new wxStaticLine(parent), wxSizerFlags().Expand().Border(wxRIGHT, 4)); - sizer->AddSpacer(3); - - sizer->Add(new wxStaticText(parent, wxID_ANY, _("Used libraries and utilities:")), wxSizerFlags().Expand().Border(wxTOP | wxBOTTOM, 2)); - // zLib - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "zLib (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://www.zlib.net", "https://www.zlib.net", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // wxWidgets - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "wxWidgets (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://www.wxwidgets.org/", "https://www.wxwidgets.org/", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // OpenSSL - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "OpenSSL (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://www.openssl.org/", "https://www.openssl.org/", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // libcurl - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "libcurl (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://curl.haxx.se/libcurl/", "https://curl.haxx.se/libcurl/", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // imgui - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "imgui (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://github.com/ocornut/imgui", "https://github.com/ocornut/imgui", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // fontawesome - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "fontawesome (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://github.com/FortAwesome/Font-Awesome", "https://github.com/FortAwesome/Font-Awesome", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // boost - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "boost (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://www.boost.org", "https://www.boost.org", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // libusb - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "libusb (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://libusb.info", "https://libusb.info", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } -#if BOOST_OS_MACOS - // MoltenVK - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, -1, "MoltenVK (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, -1, "https://github.com/KhronosGroup/MoltenVK", "https://github.com/KhronosGroup/MoltenVK", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, -1, ")")); - sizer->Add(lineSizer); - } -#endif - // icons - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "icons from ")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://icons8.com", "https://icons8.com", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - sizer->Add(lineSizer); - } - // Lato font (are we still using it?) - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "\"Lato\" font by tyPoland Lukasz Dziedzic (OFL, V1.1)", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - sizer->Add(lineSizer); - } - // SDL - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "SDL (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "https://github.com/libsdl-org/SDL", "https://github.com/libsdl-org/SDL", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - // IH264 - { - wxSizer* lineSizer = new wxBoxSizer(wxHORIZONTAL); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, "Modified ih264 from Android project (")); - lineSizer->Add(new wxHyperlinkCtrl(parent, wxID_ANY, "Source", "https://cemu.info/oss/ih264d.zip", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT))); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, " ")); - wxHyperlinkCtrl* noticeLink = new wxHyperlinkCtrl(parent, wxID_ANY, "NOTICE", "", wxDefaultPosition, wxDefaultSize, (wxHL_CONTEXTMENU|wxNO_BORDER|wxHL_ALIGN_LEFT)); - noticeLink->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& event) - { - fs::path tempPath = fs::temp_directory_path(); - tempPath.append("NOTICE_IH264.txt"); - FileStream* fs = FileStream::createFile2(tempPath); - if (!fs) - return; - fs->writeString( - "/******************************************************************************\r\n" - " *\r\n" - " * Copyright (C) 2015 The Android Open Source Project\r\n" - " *\r\n" - " * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n" - " * you may not use this file except in compliance with the License.\r\n" - " * You may obtain a copy of the License at:" - " *\r\n" - " * http://www.apache.org/licenses/LICENSE-2.0\r\n" - " *\r\n" - " * Unless required by applicable law or agreed to in writing, software\r\n" - " * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n" - " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n" - " * See the License for the specific language governing permissions and\r\n" - " * limitations under the License.\r\n" - " *\r\n" - " *****************************************************************************\r\n" - " * Originally developed and contributed by Ittiam Systems Pvt. Ltd, Bangalore\r\n" - "*/\r\n" - "/*****************************************************************************/\r\n" - ); - delete fs; - wxLaunchDefaultBrowser(formatWxString("file:{}", _pathToUtf8(tempPath))); - }); - lineSizer->Add(noticeLink); - lineSizer->Add(new wxStaticText(parent, wxID_ANY, ")")); - sizer->Add(lineSizer); - } - } - - void AddThanks(wxWindow* parent, wxSizer* sizer) - { - sizer->AddSpacer(3); - sizer->Add(new wxStaticLine(parent), wxSizerFlags().Expand().Border(wxRIGHT, 4)); - sizer->AddSpacer(3); - - wxGridSizer* gridSizer = new wxGridSizer(1, 2, 0, 0); - - sizer->AddSpacer(2); - - sizer->Add(new wxStaticText(parent, wxID_ANY, _("Thanks to our Patreon supporters:")), wxSizerFlags().Expand().Border(wxTOP | wxBOTTOM, 2)); - - std::vector patreonSupporterNames{ "Maufeat", "lvlv", "F34R", "John Godgames", "Jameel Lewis", "skooks", "Cheesy", "Barrowsx", "Mored1984", "madmat007" - , "Kuhnnl", "Owen M", "lucianobugalu", "KimoMaka", "nick palma aka renaissance18", "TheGiantBros", "SpiGAndromeda" - , "Chimech0", "Nicolás Pino", "Pezzatti", "Barry Wallace", "REGNR8 Productions", "Lagia", "Freestyler316", "Dentora" - , "tactics", "Merola.C", "Ceigyx", "Mata", "BobSchneeder45", "fenixDG", "jjalapeno55", "FissionMetroid101", "Jetta88" - , "nesxdie", "Mikah", "PornfoxVR.com", "Hunter4everosa", "Bbzx", "Salim Sanehi", "FalloutpunkX", "NashOH-CL", "RaheemWala" - , "Faris Leonhart", "MahvZero", "PlaguedGuardian", "Stuffie", "CaptainLester", "Qtech", "Zaurexus", "Leonidas", "Artifesto" - , "Alca259", "SirWestofAsh", "Loli Co.", "The Technical Revolutionary", "MegaYama", "mitori", "Seymordius", "Adrian Josh Cruz", "Manuel Hoenings", "Just A Jabb" - , "pgantonio", "CannonXIII", "Lonewolf00708", "AlexsDesign.com", "NoskLo", "MrSirHaku", "xElite_V AKA William H. Johnson", "Zalnor", "Pig", "James \"SE4LS\"", "DairyOrange", "Horoko Lawrence", "bloodmc", "Officer Jenny", "Quasar", "Postposterous", "Jake Jackson", "Kaydax", "CthePredatorG" - , "Hengi", "Pyrochaser", "luma.x3"}; - - wxString nameListLeft, nameListRight; - for (size_t i = 0; i < patreonSupporterNames.size(); i++) - { - const char* name = patreonSupporterNames[i]; - wxString& nameList = ((i % 2) == 0) ? nameListLeft : nameListRight; - if (i >= 2) - nameList.append("\n"); - nameList.append(wxString::FromUTF8(name)); - } - - gridSizer->Add(new wxStaticText(parent, wxID_ANY, nameListLeft), wxSizerFlags()); - gridSizer->Add(new wxStaticText(parent, wxID_ANY, nameListRight), wxSizerFlags()); - - sizer->AddSpacer(4); - - sizer->Add(gridSizer, 1, wxEXPAND); - - sizer->AddSpacer(2); - sizer->Add(new wxStaticText(parent, wxID_ANY, _("Special thanks:")), wxSizerFlags().Expand().Border(wxTOP, 2)); - sizer->Add(new wxStaticText(parent, wxID_ANY, "espes - Also try XQEMU!\nWaltzz92"), wxSizerFlags().Expand().Border(wxTOP, 1)); - } - -protected: - wxSizer* m_scrolledSizer; -}; - -void MainWindow::OnHelpAbout(wxCommandEvent& event) -{ - CemuAboutDialog dlgAbout(this); - dlgAbout.ShowModal(); -} - -void MainWindow::OnHelpUpdate(wxCommandEvent& event) -{ - CemuUpdateWindow test(this); - test.ShowModal(); -} - -void MainWindow::RecreateMenu() -{ - if (m_menuBar) - { - SetMenuBar(nullptr); - m_menuBar->Destroy(); - m_menuBar = nullptr; - } - - auto& guiConfig = GetWxGUIConfig(); - - m_menuBar = new wxMenuBar(); - // file submenu - m_fileMenu = new wxMenu(); - - if (!m_game_launched) - { - m_loadMenuItem = m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_LOAD, _("&Load...")); - m_installUpdateMenuItem = m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_INSTALL_UPDATE, _("&Install game title, update or DLC...")); - - wxMenu* recentMenu = new wxMenu(); - sint32 recentFileIndex = 1; - m_fileMenuSeparator0 = nullptr; - m_fileMenuSeparator1 = nullptr; - for (size_t i = 0; i < guiConfig.recent_launch_files.size(); i++) - { - const std::string& pathStr = guiConfig.recent_launch_files[i]; - if (pathStr.empty()) - continue; - recentMenu->Append(MAINFRAME_MENU_ID_FILE_RECENT_0 + i, formatWxString("{}. {}", recentFileIndex, pathStr)); - recentFileIndex++; - - if (recentFileIndex >= 10) - break; - } - if (recentFileIndex == 0) - { - wxMenuItem* placeholder = recentMenu->Append(wxID_NONE, _("(No recent files)")); - placeholder->Enable(false); - } - - m_fileMenu->AppendSeparator(); - m_fileMenu->AppendSubMenu(recentMenu, _("Recent files")); - m_fileMenu->AppendSeparator(); - } - else - { -#ifdef CEMU_DEBUG_ASSERT - m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_END_EMULATION, _("Close game")); - m_fileMenuSeparator1 = m_fileMenu->AppendSeparator(); -#endif - } - - m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_OPEN_CEMU_FOLDER, _("Open Cemu folder")); - m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_OPEN_MLC_FOLDER, _("Open MLC folder")); - m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_OPEN_SHADERCACHE_FOLDER, _("Open &shader cache folder")); - m_fileMenu->AppendSeparator(); - m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_CLEAR_SPOTPASS_CACHE, _("Clear Spot&Pass cache")); - if (m_game_launched) - m_fileMenu->Enable(MAINFRAME_MENU_ID_FILE_CLEAR_SPOTPASS_CACHE, false); - m_fileMenu->AppendSeparator(); - m_exitMenuItem = m_fileMenu->Append(MAINFRAME_MENU_ID_FILE_EXIT, _("&Exit")); - m_menuBar->Append(m_fileMenu, _("&File")); - // options->account submenu - m_optionsAccountMenu = new wxMenu(); - const auto account_id = ActiveSettings::GetPersistentId(); - int index = 0; - for(const auto& account : Account::GetAccounts()) - { - wxMenuItem* item = m_optionsAccountMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 + index, account.ToString()); - item->Check(account_id == account.GetPersistentId()); - if (m_game_launched || LaunchSettings::GetPersistentId().has_value()) - item->Enable(false); - - ++index; - } - - auto& config = GetConfig(); - auto& wxConfig = GetWxGUIConfig(); - // options->console language submenu - wxMenu* optionsConsoleLanguageMenu = new wxMenu(); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ENGLISH, _("&English"))->Check(config.console_language == CafeConsoleLanguage::EN); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_JAPANESE, _("&Japanese"))->Check(config.console_language == CafeConsoleLanguage::JA); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_FRENCH, _("&French"))->Check(config.console_language == CafeConsoleLanguage::FR); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_GERMAN, _("&German"))->Check(config.console_language == CafeConsoleLanguage::DE); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_ITALIAN, _("&Italian"))->Check(config.console_language == CafeConsoleLanguage::IT); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_SPANISH, _("&Spanish"))->Check(config.console_language == CafeConsoleLanguage::ES); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_CHINESE, _("&Chinese"))->Check(config.console_language == CafeConsoleLanguage::ZH); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_KOREAN, _("&Korean"))->Check(config.console_language == CafeConsoleLanguage::KO); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_DUTCH, _("&Dutch"))->Check(config.console_language == CafeConsoleLanguage::NL); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_PORTUGUESE, _("&Portuguese"))->Check(config.console_language == CafeConsoleLanguage::PT); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_RUSSIAN, _("&Russian"))->Check(config.console_language == CafeConsoleLanguage::RU); - optionsConsoleLanguageMenu->AppendRadioItem(MAINFRAME_MENU_ID_OPTIONS_LANGUAGE_TAIWANESE, _("&Taiwanese"))->Check(config.console_language == CafeConsoleLanguage::TW); - if(IsGameLaunched()) - { - auto items = optionsConsoleLanguageMenu->GetMenuItems(); - for (auto& item : items) - { - item->Enable(false); - } - } - - // options submenu - wxMenu* optionsMenu = new wxMenu(); - m_fullscreenMenuItem = optionsMenu->AppendCheckItem(MAINFRAME_MENU_ID_OPTIONS_FULLSCREEN, _("&Fullscreen")); - m_fullscreenMenuItem->Check(FullscreenEnabled()); - - optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GRAPHIC_PACKS2, _("&Graphic packs")); - m_padViewMenuItem = optionsMenu->AppendCheckItem(MAINFRAME_MENU_ID_OPTIONS_SECOND_WINDOW_PADVIEW, _("&Separate GamePad view")); - m_padViewMenuItem->Check(wxConfig.pad_open); - optionsMenu->AppendSeparator(); - #if BOOST_OS_MACOS - optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_MAC_SETTINGS, _("&Settings..." "\tCtrl-,")); - #endif - optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings")); - optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings")); - optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_HOTKEY, _("&Hotkey settings")); - - optionsMenu->AppendSeparator(); - optionsMenu->AppendSubMenu(m_optionsAccountMenu, _("&Active account")); - optionsMenu->AppendSubMenu(optionsConsoleLanguageMenu, _("&Console language")); - m_menuBar->Append(optionsMenu, _("&Options")); - - // tools submenu - wxMenu* toolsMenu = new wxMenu(); - m_memorySearcherMenuItem = toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, _("&Memory searcher")); - m_memorySearcherMenuItem->Enable(false); - toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, _("&Title Manager")); - toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, _("&Download Manager")); - toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, _("&Emulated USB Devices")); - - m_menuBar->Append(toolsMenu, _("&Tools")); - - // cpu timer speed menu - wxMenu* timerSpeedMenu = new wxMenu(); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_1X, _("&1x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 3); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_2X, _("&2x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 2); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_4X, _("&4x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 1); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_8X, _("&8x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 0); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_05X, _("&0.5x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 4); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_025X, _("&0.25x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 5); - timerSpeedMenu->AppendRadioItem(MAINFRAME_MENU_ID_TIMER_SPEED_0125X, _("&0.125x speed"))->Check(ActiveSettings::GetTimerShiftFactor() == 6); - - // cpu submenu - wxMenu* cpuMenu = new wxMenu(); - cpuMenu->AppendSubMenu(timerSpeedMenu, _("&Timer speed")); - m_menuBar->Append(cpuMenu, _("&CPU")); - - // nfc submenu - wxMenu* nfcMenu = new wxMenu(); - m_nfcMenu = nfcMenu; - nfcMenu->Append(MAINFRAME_MENU_ID_NFC_TOUCH_NFC_FILE, _("&Scan NFC tag/amiibo from file"))->Enable(false); - m_menuBar->Append(nfcMenu, _("&NFC")); - m_nfcMenuSeparator0 = nullptr; - // debug->logging submenu - wxMenu* debugLoggingMenu = new wxMenu(); - - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::UnsupportedAPI), _("&Unsupported API calls"))->Check(cemuLog_isLoggingEnabled(LogType::UnsupportedAPI)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::APIErrors), _("&Invalid API usage"))->Check(cemuLog_isLoggingEnabled(LogType::APIErrors)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitLogging), _("&Coreinit Logging (OSReport/OSConsole)"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitLogging)); - debugLoggingMenu->AppendSeparator(); - - wxMenu* logCosModulesMenu = new wxMenu(); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING_MESSAGE, _("&Options below are for experts. Leave off if unsure"))->Enable(false); - logCosModulesMenu->AppendSeparator(); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitFile), _("coreinit File-Access API"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitFile)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThreadSync), _("coreinit Thread-Synchronization API"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThreadSync)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMem), _("coreinit Memory API"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMem)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitMP), _("coreinit MP API"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitMP)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::CoreinitThread), _("coreinit Thread API"))->Check(cemuLog_isLoggingEnabled(LogType::CoreinitThread)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Save), _("nn_save API"))->Check(cemuLog_isLoggingEnabled(LogType::Save)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_NFP), _("nn_nfp API"))->Check(cemuLog_isLoggingEnabled(LogType::NN_NFP)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_FP), _("nn_fp API"))->Check(cemuLog_isLoggingEnabled(LogType::NN_FP)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::PRUDP), _("nn_fp PRUDP"))->Check(cemuLog_isLoggingEnabled(LogType::PRUDP)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NN_BOSS), _("nn_boss API"))->Check(cemuLog_isLoggingEnabled(LogType::NN_BOSS)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NFC), _("nfc API"))->Check(cemuLog_isLoggingEnabled(LogType::NFC)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::NTAG), _("ntag API"))->Check(cemuLog_isLoggingEnabled(LogType::NTAG)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Socket), _("nsysnet API"))->Check(cemuLog_isLoggingEnabled(LogType::Socket)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::H264), _("h264 API"))->Check(cemuLog_isLoggingEnabled(LogType::H264)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::GX2), _("gx2 API"))->Check(cemuLog_isLoggingEnabled(LogType::GX2)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::SWKBD), _("swkbd API"))->Check(cemuLog_isLoggingEnabled(LogType::SWKBD)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::SoundAPI), _("Audio API"))->Check(cemuLog_isLoggingEnabled(LogType::SoundAPI)); - logCosModulesMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::InputAPI), _("Input API"))->Check(cemuLog_isLoggingEnabled(LogType::InputAPI)); - - debugLoggingMenu->AppendSubMenu(logCosModulesMenu, _("&CafeOS modules logging")); - debugLoggingMenu->AppendSeparator(); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::Patches), _("&Graphic pack patches"))->Check(cemuLog_isLoggingEnabled(LogType::Patches)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::TextureCache), _("&Texture cache warnings"))->Check(cemuLog_isLoggingEnabled(LogType::TextureCache)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::TextureReadback), _("&Texture readback"))->Check(cemuLog_isLoggingEnabled(LogType::TextureReadback)); - debugLoggingMenu->AppendSeparator(); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::OpenGLLogging), _("&OpenGL debug output"))->Check(cemuLog_isLoggingEnabled(LogType::OpenGLLogging)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::VulkanValidation), _("&Vulkan validation layer (slow)"))->Check(cemuLog_isLoggingEnabled(LogType::VulkanValidation)); - debugLoggingMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_ADVANCED_PPC_INFO, _("&Log PPC context for API"))->Check(cemuLog_advancedPPCLoggingEnabled()); - m_loggingSubmenu = debugLoggingMenu; - // debug->dump submenu - wxMenu* debugDumpMenu = new wxMenu; - debugDumpMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_DUMP_TEXTURES, _("&Textures"))->Check(ActiveSettings::DumpTexturesEnabled()); - debugDumpMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_DUMP_SHADERS, _("&Shaders"))->Check(ActiveSettings::DumpShadersEnabled()); - debugDumpMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_DUMP_RECOMPILER_FUNCTIONS, _("&Recompiled functions"))->Check(ActiveSettings::DumpRecompilerFunctionsEnabled()); - debugDumpMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_DUMP_CURL_REQUESTS, _("&nlibcurl HTTP/HTTPS requests")); - // debug submenu - wxMenu* debugMenu = new wxMenu(); - m_debugMenu = debugMenu; - debugMenu->AppendSubMenu(debugLoggingMenu, _("&Logging")); - debugMenu->AppendSubMenu(debugDumpMenu, _("&Dump")); - debugMenu->AppendSeparator(); - - auto upsidedownItem = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_RENDER_UPSIDE_DOWN, _("&Render upside-down")); - upsidedownItem->Check(ActiveSettings::RenderUpsideDownEnabled()); - if(LaunchSettings::RenderUpsideDownEnabled().has_value()) - upsidedownItem->Enable(false); - - auto accurateBarriers = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_VK_ACCURATE_BARRIERS, _("&Accurate barriers (Vulkan)")); - accurateBarriers->Check(GetConfig().vk_accurate_barriers); - -#ifdef ENABLE_METAL - auto gpuCapture = debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_GPU_CAPTURE, _("&GPU capture (Metal)")); - gpuCapture->Enable(m_game_launched && g_renderer->GetType() == RendererAPI::Metal); -#endif - - debugMenu->AppendSeparator(); - -#ifdef CEMU_DEBUG_ASSERT - auto audioAuxOnly = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_AUDIO_AUX_ONLY, _("&Audio AUX only")); - audioAuxOnly->Check(ActiveSettings::AudioOutputOnlyAux()); -#endif - - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_LOGGING_WINDOW, _("&Open logging window")); - m_gdbstub_toggle = debugMenu->AppendCheckItem(MAINFRAME_MENU_ID_DEBUG_TOGGLE_GDB_STUB, _("&Launch with GDB stub")); - m_gdbstub_toggle->Check(g_gdbstub != nullptr); - m_gdbstub_toggle->Enable(!m_game_launched); - - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_THREADS, _("&View PPC threads")); - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_PPC_DEBUGGER, _("&View PPC debugger")); - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_AUDIO_DEBUGGER, _("&View audio debugger")); - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_VIEW_TEXTURE_RELATIONS, _("&View texture cache info")); - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_DUMP_RAM, _("&Dump current RAM"))->Enable(m_game_launched); - debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_DUMP_RPX, _("&Dump current RPX"))->Enable(m_game_launched); - // debugMenu->Append(MAINFRAME_MENU_ID_DEBUG_DUMP_FST, _("&Dump WUD filesystem"))->Enable(false); - - m_menuBar->Append(debugMenu, _("&Debug")); - // help menu - wxMenu* helpMenu = new wxMenu(); - m_check_update_menu = helpMenu->Append(MAINFRAME_MENU_ID_HELP_UPDATE, _("&Check for updates")); -#if BOOST_OS_LINUX - if (!std::getenv("APPIMAGE")) { - m_check_update_menu->Enable(false); - } -#elif BOOST_OS_BSD // BSD users must update from source so disable update checks - m_check_update_menu->Enable(false); -#endif - helpMenu->AppendSeparator(); - helpMenu->Append(MAINFRAME_MENU_ID_HELP_ABOUT, _("&About Cemu")); - - m_menuBar->Append(helpMenu, _("&Help")); - - SetMenuBar(m_menuBar); - m_menu_visible = true; - - if (m_game_launched) - { - if (m_check_update_menu) - m_check_update_menu->Enable(false); - - m_memorySearcherMenuItem->Enable(true); - m_nfcMenu->Enable(MAINFRAME_MENU_ID_NFC_TOUCH_NFC_FILE, true); - - // these options cant be toggled after the renderer backend is initialized: - m_loggingSubmenu->Enable(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::OpenGLLogging), false); - m_loggingSubmenu->Enable(MAINFRAME_MENU_ID_DEBUG_LOGGING0 + stdx::to_underlying(LogType::VulkanValidation), false); - - UpdateNFCMenu(); - } - - // hide new menu in fullscreen - if (IsFullScreen()) - SetMenuVisible(false); -} - -void MainWindow::UpdateChildWindowTitleRunningState() -{ - const bool running = CafeSystem::IsTitleRunning(); - - if(m_graphic_pack_window) - m_graphic_pack_window->UpdateTitleRunning(running); -} - -void MainWindow::RestoreSettingsAfterGameExited() -{ - RecreateMenu(); -} - -void MainWindow::UpdateSettingsAfterGameLaunch() -{ - m_update_available = {}; - RecreateMenu(); -} - -void MainWindow::OnGraphicWindowClose(wxCloseEvent& event) -{ - m_graphic_pack_window->Destroy(); - m_graphic_pack_window = nullptr; -} - -void MainWindow::OnGraphicWindowOpen(wxTitleIdEvent& event) -{ - if (m_graphic_pack_window) - return; - m_graphic_pack_window = new GraphicPacksWindow2(this, event.GetTitleId()); - m_graphic_pack_window->Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnGraphicWindowClose, this); - m_graphic_pack_window->Show(true); -} - -void MainWindow::RequestGameListRefresh() -{ - auto* evt = new wxCommandEvent(wxEVT_REQUEST_GAMELIST_REFRESH); - wxQueueEvent(g_mainFrame, evt); -} - -void MainWindow::RequestLaunchGame(fs::path filePath, wxLaunchGameEvent::INITIATED_BY initiatedBy) -{ - wxLaunchGameEvent evt(filePath, initiatedBy); - wxPostEvent(g_mainFrame, evt); -} - -void MainWindow::OnRequestRecreateCanvas(wxCommandEvent& event) -{ - CounterSemaphore* sem = (CounterSemaphore*)event.GetClientData(); - DestroyCanvas(); - CreateCanvas(); - sem->increment(); -} - -void MainWindow::CafeRecreateCanvas() -{ - CounterSemaphore sem; - auto* evt = new wxCommandEvent(wxEVT_REQUEST_RECREATE_CANVAS); - evt->SetClientData((void*)&sem); - wxQueueEvent(g_mainFrame, evt); - sem.decrementWithWait(); -} - -void MainWindow::CafePPCProcessExit() -{ - // this is called from the emulated PPC thread, so queue an event instead of handling it directly - wxQueueEvent(g_mainFrame, new wxCommandEvent(wxEVT_REQUEST_GAME_EXIT)); -} - -void MainWindow::OnRequestGameExit(wxCommandEvent& event) -{ - // if the title was launched via the command line we close Cemu and use the foreground title's exit status as Cemu's process return code (via CemuApp:OnExit) - // this is useful for homebrew testing setups that use Cemu via CLI - // otherwise the title was launched from the game list, so we just stop it and return to the game list instead of closing Cemu - if (LaunchSettings::GetLoadFile() || LaunchSettings::GetLoadTitleID()) - { - Close(); - } - else - { - EndEmulation(); - } -} - -bool MainWindow::FullscreenEnabled() const -{ - return LaunchSettings::FullscreenEnabled().value_or(GetWxGUIConfig().fullscreen); -} diff --git a/src/gui/wxgui/MainWindow.h b/src/gui/wxgui/MainWindow.h deleted file mode 100644 index 483b67fa..00000000 --- a/src/gui/wxgui/MainWindow.h +++ /dev/null @@ -1,248 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "wxgui/PadViewFrame.h" -#include "wxgui/MemorySearcherTool.h" - -#include "config/XMLConfig.h" - -#include "wxgui/LoggingWindow.h" -#include "wxgui/components/wxGameList.h" - -#include -#include "Cafe/HW/Espresso/Debugger/GDBStub.h" -#include "Cafe/CafeSystem.h" - -class DebuggerWindow2; -struct GameEntry; -class DiscordPresence; -class TitleManager; -class GraphicPacksWindow2; -class EmulatedUSBDeviceFrame; -class wxLaunchGameEvent; - -wxDECLARE_EVENT(wxEVT_LAUNCH_GAME, wxLaunchGameEvent); -wxDECLARE_EVENT(wxEVT_SET_WINDOW_TITLE, wxCommandEvent); - -class wxLaunchGameEvent : public wxCommandEvent -{ -public: - enum class INITIATED_BY - { - MENU, // via file menu - DRAG_AND_DROP, - GAME_LIST, - TITLE_MANAGER, - COMMAND_LINE, // -g parameter - }; - - wxLaunchGameEvent(fs::path path, INITIATED_BY initiatedBy) - : wxCommandEvent(wxEVT_LAUNCH_GAME), m_launchPath(path), m_initiatedBy(initiatedBy) {} - - [[nodiscard]] fs::path GetPath() const { return m_launchPath; } - [[nodiscard]] INITIATED_BY GetInitiatedBy() const { return m_initiatedBy; } - - wxEvent* Clone() const { return new wxLaunchGameEvent(*this); } - -private: - fs::path m_launchPath; - INITIATED_BY m_initiatedBy; -}; - -class MainWindow : public wxFrame, public CafeSystem::SystemImplementation -{ - friend class CemuApp; - -public: - MainWindow(); - ~MainWindow(); - - void CreateGameListAndStatusBar(); - void DestroyGameListAndStatusBar(); - - void UpdateSettingsAfterGameLaunch(); - void RestoreSettingsAfterGameExited(); - - bool FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATED_BY initiatedBy); - - [[nodiscard]] bool IsGameLaunched() const { return m_game_launched; } - - void SetFullScreen(bool state); - void EndEmulation(); - void SetMenuVisible(bool state); - void UpdateNFCMenu(); - bool IsMenuHidden() const; - void TogglePadView(); - void OpenPPCDebugger(); - -#if BOOST_OS_WINDOWS - WXLRESULT MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) override; -#endif - void OpenSettings(); - - PadViewFrame* GetPadView() const { return m_padView; } - - void OnSizeEvent(wxSizeEvent& event); - void OnDPIChangedEvent(wxDPIChangedEvent& event); - void OnMove(wxMoveEvent& event); - - void OnDebuggerClose(wxCloseEvent& event); - void OnPadClose(wxCloseEvent& event); - void OnMemorySearcherClose(wxCloseEvent& event); - void OnMouseWheel(wxMouseEvent& event); - void OnClose(wxCloseEvent& event); - void OnFileMenu(wxCommandEvent& event); - void OnOpenFolder(wxCommandEvent& event); - void OnClearSpotPassCache(wxCommandEvent& event); - void OnLaunchFromFile(wxLaunchGameEvent& event); - void OnInstallUpdate(wxCommandEvent& event); - void OnFileExit(wxCommandEvent& event); - void OnNFCMenu(wxCommandEvent& event); - void OnOptionsInput(wxCommandEvent& event); - void OnAccountSelect(wxCommandEvent& event); - void OnConsoleLanguage(wxCommandEvent& event); - void OnHelpAbout(wxCommandEvent& event); - void OnHelpUpdate(wxCommandEvent& event); - void OnDebugSetting(wxCommandEvent& event); - void OnDebugLoggingToggleFlagGeneric(wxCommandEvent& event); - void OnPPCInfoToggle(wxCommandEvent& event); - void OnDebugDumpGeneric(wxCommandEvent& event); - void OnLoggingWindow(wxCommandEvent& event); - void OnGDBStubToggle(wxCommandEvent& event); - void OnDebugViewPPCThreads(wxCommandEvent& event); - void OnDebugViewPPCDebugger(wxCommandEvent& event); - void OnDebugViewAudioDebugger(wxCommandEvent& event); - void OnDebugViewTextureRelations(wxCommandEvent& event); - void OnMouseMove(wxMouseEvent& event); - void OnMouseLeft(wxMouseEvent& event); - void OnMouseRight(wxMouseEvent& event); - void OnGameListBeginUpdate(wxCommandEvent& event); - void OnGameListEndUpdate(wxCommandEvent& event); - void OnAccountListRefresh(wxCommandEvent& event); - void OnRequestGameListRefresh(wxCommandEvent& event); - void OnSetWindowTitle(wxCommandEvent& event); - - void OnKeyUp(wxKeyEvent& event); - void OnKeyDown(wxKeyEvent& event); - void OnChar(wxKeyEvent& event); - - void OnToolsInput(wxCommandEvent& event); - void OnGesturePan(wxPanGestureEvent& event); - - void OnGameLoaded(); - - void AsyncSetTitle(std::string_view windowTitle); - - void CreateCanvas(); - void DestroyCanvas(); - - static void ShowCursor(bool state); - - uintptr_t GetRenderCanvasHWND(); - - static void RequestGameListRefresh(); - static void RequestLaunchGame(fs::path filePath, wxLaunchGameEvent::INITIATED_BY initiatedBy); - -private: - bool FullscreenEnabled() const; - void RecreateMenu(); - void UpdateChildWindowTitleRunningState(); - static wxString GetInitialWindowTitle(); - - bool InstallUpdate(const fs::path& metaFilePath); - - void OnTimer(wxTimerEvent& event); - - // CafeSystem implementation - void CafeRecreateCanvas() override; - void CafePPCProcessExit() override; - - void OnRequestRestart(wxCommandEvent& event); - void OnRequestRecreateCanvas(wxCommandEvent& event); - void OnRequestGameExit(wxCommandEvent& event); - - wxRect GetDesktopRect(); - - MemorySearcherTool* m_toolWindow = nullptr; - TitleManager* m_title_manager = nullptr; - EmulatedUSBDeviceFrame* m_usb_devices = nullptr; - PadViewFrame* m_padView = nullptr; - GraphicPacksWindow2* m_graphic_pack_window = nullptr; - - wxTimer* m_timer; - wxPoint m_mouse_position{}; - std::chrono::steady_clock::time_point m_last_mouse_move_time; - wxSize m_restored_size; - wxPoint m_restored_position; - - bool m_menu_visible = false; - bool m_game_launched = false; - - #ifdef ENABLE_DISCORD_RPC - std::unique_ptr m_discord; - #endif - - std::string m_launched_game_name; - - wxMenuItem* m_gdbstub_toggle{}; - DebuggerWindow2* m_debugger_window = nullptr; - LoggingWindow* m_logging_window = nullptr; - - std::future m_update_available; - - wxMenuItem* m_check_update_menu{}; - void LoadSettings(); - void SaveSettings(); - - void OnGraphicWindowClose(wxCloseEvent& event); - void OnGraphicWindowOpen(wxTitleIdEvent& event); - - // panels - wxPanel* m_main_panel{}, * m_game_panel{}; - - // rendering - wxWindow* m_render_canvas{}; - - // gamelist - wxGameList* m_game_list{}; - wxInfoBar* m_info_bar{}; - - // menu - wxMenuBar* m_menuBar{}; - - // file - wxMenu* m_fileMenu{}; - wxMenuItem* m_fileMenuSeparator0{}; - wxMenuItem* m_fileMenuSeparator1{}; - wxMenuItem* m_loadMenuItem{}; - wxMenuItem* m_installUpdateMenuItem{}; - wxMenuItem* m_exitMenuItem{}; - - // options - wxMenu* m_optionsAccountMenu{}; - - wxMenuItem* m_fullscreenMenuItem{}; - wxMenuItem* m_padViewMenuItem{}; - - // tools - wxMenuItem* m_memorySearcherMenuItem{}; - - // cpu - wxMenu* m_cpuTimerSubmenu{}; - - // nfc - wxMenu* m_nfcMenu{}; - wxMenuItem* m_nfcMenuSeparator0{}; - - // debug - wxMenu* m_debugMenu{}; - wxMenu* m_loggingSubmenu{}; - wxMenuItem* m_asyncCompile{}; - -wxDECLARE_EVENT_TABLE(); -}; - -extern MainWindow* g_mainFrame; diff --git a/src/gui/wxgui/MemorySearcherTool.cpp b/src/gui/wxgui/MemorySearcherTool.cpp deleted file mode 100644 index 5cdfacc4..00000000 --- a/src/gui/wxgui/MemorySearcherTool.cpp +++ /dev/null @@ -1,742 +0,0 @@ -#include "wxgui/wxgui.h" - -#include "wxgui/MemorySearcherTool.h" - -#include -#include -#include -#include - -#include "config/ActiveSettings.h" -#include "wxgui/helpers/wxHelpers.h" -#include "Common/FileStream.h" -#include "util/IniParser/IniParser.h" -#include "util/helpers/StringHelpers.h" -#include "Cafe/CafeSystem.h" - -enum -{ - COMBOBOX_DATATYPE = wxID_HIGHEST + 1, - TEXT_VALUE, - BUTTON_START, - BUTTON_FILTER, - LIST_RESULTS, - LIST_ENTRYTABLE, - TIMER_REFRESH, - LIST_ENTRY_ADD, - LIST_ENTRY_REMOVE, -}; - -wxDEFINE_EVENT(wxEVT_SEARCH_FINISHED, wxCommandEvent); - -wxBEGIN_EVENT_TABLE(MemorySearcherTool, wxFrame) -EVT_CLOSE(MemorySearcherTool::OnClose) -EVT_BUTTON(BUTTON_START, MemorySearcherTool::OnSearch) -EVT_BUTTON(BUTTON_FILTER, MemorySearcherTool::OnFilter) -EVT_TIMER(TIMER_REFRESH, MemorySearcherTool::OnTimerTick) -wxEND_EVENT_TABLE() - -constexpr auto kMaxResultCount = 5000; - -const wxString kDatatypeFloat = "float"; -const wxString kDatatypeDouble = "double"; -const wxString kDatatypeString = "string"; -const wxString kDatatypeInt8 = "int8"; -const wxString kDatatypeInt16 = "int16"; -const wxString kDatatypeInt32 = "int32"; -const wxString kDatatypeInt64 = "int64"; -const wxString kDataTypeNames[] = {kDatatypeFloat,kDatatypeDouble,/*DATATYPE_STRING,*/kDatatypeInt8,kDatatypeInt16,kDatatypeInt32,kDatatypeInt64}; - -MemorySearcherTool::MemorySearcherTool(wxFrame* parent) - : wxFrame(parent, wxID_ANY, _("Memory Searcher"), wxDefaultPosition, wxSize(600, 540), wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) -{ - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - this->wxTopLevelWindowBase::SetMinSize(wxSize(600, 540)); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - - auto* row1 = new wxFlexGridSizer(0, 4, 0, 0); - row1->AddGrowableCol(1); - - m_cbDataType = new wxComboBox(this, COMBOBOX_DATATYPE, kDatatypeFloat, wxDefaultPosition, wxDefaultSize, std::size(kDataTypeNames), kDataTypeNames, wxCB_READONLY); - m_textValue = new wxTextCtrl(this, TEXT_VALUE); - m_buttonStart = new wxButton(this, BUTTON_START, _("Search")); - m_buttonFilter = new wxButton(this, BUTTON_FILTER, _("Filter")); - m_buttonFilter->Disable(); - - row1->Add(m_cbDataType, 0, wxALL, 5); - row1->Add(m_textValue, 0, wxALL | wxEXPAND, 5); - row1->Add(m_buttonStart, 0, wxALL, 5); - row1->Add(m_buttonFilter, 0, wxALL, 5); - - sizer->Add(row1, 0, wxEXPAND, 5); - - auto* row2 = new wxFlexGridSizer(0, 1, 0, 0); - row2->AddGrowableCol(0); - - m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL); - m_gauge->SetValue(0); - m_gauge->Enable(false); - - m_textEntryTable = new wxStaticText(this, wxID_ANY, _("Results")); - m_listResults = new wxListView(this, LIST_RESULTS, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SORT_ASCENDING); - m_listResults->Bind(wxEVT_LEFT_DCLICK, &MemorySearcherTool::OnResultListClick, this); - { - wxListItem col0; - col0.SetId(0); - col0.SetText(_("Address")); - col0.SetWidth(100); - m_listResults->InsertColumn(0, col0); - wxListItem col1; - col1.SetId(1); - col1.SetText(_("Value")); - col1.SetWidth(250); - m_listResults->InsertColumn(1, col1); - } - - auto textEntryTable = new wxStaticText(this, wxID_ANY, _("Stored Entries")); - m_listEntryTable = new wxDataViewListCtrl(this, LIST_ENTRYTABLE, wxDefaultPosition, wxSize(420, 200), wxDV_HORIZ_RULES); - m_listEntryTable->Bind(wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, &MemorySearcherTool::OnEntryListRightClick, this); - m_listEntryTable->Bind(wxEVT_COMMAND_DATAVIEW_ITEM_EDITING_DONE, &MemorySearcherTool::OnItemEdited, this); - { - m_listEntryTable->AppendTextColumn(_("Description"), wxDATAVIEW_CELL_EDITABLE, 150, wxALIGN_LEFT, wxDATAVIEW_COL_SORTABLE); - m_listEntryTable->AppendTextColumn(_("Address"), wxDATAVIEW_CELL_INERT, 100, wxALIGN_LEFT, wxDATAVIEW_COL_SORTABLE); - m_listEntryTable->AppendTextColumn(_("Type")); - m_listEntryTable->AppendTextColumn(_("Value"), wxDATAVIEW_CELL_EDITABLE); - m_listEntryTable->AppendToggleColumn(_("Freeze"), wxDATAVIEW_CELL_ACTIVATABLE, 50, wxALIGN_LEFT, 0); - } - - row2->AddGrowableRow(3); - row2->AddGrowableRow(5); - - row2->Add(m_gauge, 0, wxALL | wxEXPAND, 5); - row2->Add(0, 10, 1, wxEXPAND, 5); - row2->Add(m_textEntryTable, 0, wxALL, 5); - row2->Add(m_listResults, 1, wxALL | wxEXPAND, 5); - row2->Add(textEntryTable, 0, wxALL, 5); - row2->Add(m_listEntryTable, 1, wxALL | wxEXPAND, 5); - - sizer->Add(row2, 1, wxEXPAND, 5); - - // load stored entries - Load(); - - this->Bind(wxEVT_SEARCH_FINISHED, &MemorySearcherTool::OnSearchFinished, this); - this->Bind(wxEVT_SET_GAUGE_VALUE, &MemorySearcherTool::OnUpdateGauge, this); - - m_refresh_timer = new wxTimer(this, TIMER_REFRESH); - m_refresh_timer->Start(250); - - this->SetSizer(sizer); - this->wxWindowBase::Layout(); - - this->Centre(wxBOTH); -} - -MemorySearcherTool::~MemorySearcherTool() -{ - m_refresh_timer->Stop(); - - m_running = false; - if (m_worker.joinable()) - m_worker.join(); -} - -void MemorySearcherTool::OnTimerTick(wxTimerEvent& event) -{ - RefreshResultList(); - RefreshStashList(); -} - -void MemorySearcherTool::OnClose(wxCloseEvent& event) -{ - Save(); - event.Skip(); -} - -void MemorySearcherTool::OnSearchFinished(wxCommandEvent&) -{ - FillResultList(); - m_search_running = false; - m_buttonStart->Enable(); - m_buttonFilter->Enable(); -} - -void MemorySearcherTool::OnUpdateGauge(wxSetGaugeValue& event) -{ - auto* gauge = event.GetGauge(); - const auto value = event.GetValue(); - if (event.GetRange() != 0) - { - gauge->SetRange(event.GetRange()); - gauge->SetValue(value); - return; - } - - debug_printf("update gauge: %d + %d = %d (/%d)\n", gauge->GetValue(), value, gauge->GetValue() + value, gauge->GetRange()); - gauge->SetValue(gauge->GetValue() + value); -} - -void MemorySearcherTool::OnSearch(wxCommandEvent&) -{ - if (m_clear_state) - { - Reset(); - return; - } - - if (m_search_running) - return; - - if (m_textValue->IsEmpty()) - return; - - SetSearchDataType(); - - if (!VerifySearchValue()) - { - wxMessageBox(_("Your entered value is not valid for the selected datatype."), _("Error"), wxICON_ERROR); - return; - } - m_buttonStart->Disable(); - m_buttonFilter->Disable(); - m_cbDataType->Disable(); - m_buttonStart->SetLabelText(_("Clear")); - m_clear_state = true; - - if (m_worker.joinable()) - m_worker.join(); - - m_worker = std::thread([this]() - { - m_search_jobs.clear(); - uint32 total_size = 0; - for (const auto& itr : memory_getMMURanges()) - { - if (!m_running) - return; - - if (!itr->isMapped()) - continue; - - void* ptr = itr->getPtr(); - const uint32 size = itr->getSize(); - - total_size += (size / kGaugeStep); - m_search_jobs.emplace_back(std::async(std::launch::async, [this, ptr, size]() { return SearchValues(m_searchDataType, ptr, size); })); - } - - wxQueueEvent(this, new wxSetGaugeValue(0, total_size, m_gauge)); - - ListType_t tmp; - for (auto& it : m_search_jobs) - { - const auto result = it.get(); - tmp.insert(tmp.end(), result.cbegin(), result.cend()); - } - - std::unique_lock lock(m_mutex); - m_searchBuffer.swap(tmp); - lock.unlock(); - - wxQueueEvent(this, new wxCommandEvent(wxEVT_SEARCH_FINISHED)); - }); -} - -void MemorySearcherTool::OnFilter(wxCommandEvent& event) -{ - m_buttonStart->Disable(); - m_buttonFilter->Disable(); - - if (m_worker.joinable()) - m_worker.join(); - - m_worker = std::thread([this]() - { - const auto count = (uint32)m_searchBuffer.size(); - wxQueueEvent(this, new wxSetGaugeValue(0, count, m_gauge)); - - auto tmp = FilterValues(m_searchDataType); - - std::unique_lock lock(m_mutex); - m_searchBuffer.swap(tmp); - lock.unlock(); - - wxQueueEvent(this, new wxCommandEvent(wxEVT_SEARCH_FINISHED)); - }); - - m_gauge->SetValue(0); -} - -void MemorySearcherTool::Load() -{ - const auto memorySearcherPath = ActiveSettings::GetUserDataPath("memorySearcher/{:016x}.ini", CafeSystem::GetForegroundTitleId()); - auto memSearcherIniContents = FileStream::LoadIntoMemory(memorySearcherPath); - if (!memSearcherIniContents) - return; - - IniParser iniParser(*memSearcherIniContents, _pathToUtf8(memorySearcherPath)); - while (iniParser.NextSection()) - { - auto option_description = iniParser.FindOption("description"); - auto option_address = iniParser.FindOption("address"); - auto option_type = iniParser.FindOption("type"); - auto option_value = iniParser.FindOption("value"); - - if (!option_description || !option_address || !option_type || !option_value) - continue; - - try - { - const auto addr = StringHelpers::ToInt64(*option_address); - if (!IsAddressValid(addr)) - continue; - } - catch (const std::invalid_argument&) - { - continue; - } - - bool found = false; - for (const auto& entry : kDataTypeNames) - { - if (boost::iequals(entry.ToStdString(), *option_type)) - { - found = true; - break; - } - } - - if (!found && !boost::iequals(kDatatypeString.ToStdString(), *option_type)) - continue; - - wxVector data; - data.push_back(std::string(*option_description).c_str()); - data.push_back(std::string(*option_address).c_str()); - data.push_back(std::string(*option_type).c_str()); - data.push_back(std::string(*option_value).c_str()); - data.push_back(!option_value->empty()); - m_listEntryTable->AppendItem(data); - } -} - -void MemorySearcherTool::Save() -{ - const auto memorySearcherPath = ActiveSettings::GetUserDataPath("memorySearcher/{:016x}.ini", CafeSystem::GetForegroundTitleId()); - FileStream* fs = FileStream::createFile2(memorySearcherPath); - if (fs) - { - for (int i = 0; i < m_listEntryTable->GetItemCount(); ++i) - { - fs->writeLine("[Entry]"); - - std::string tmp = "description=" + m_listEntryTable->GetTextValue(i, 0).ToStdString(); - fs->writeLine(tmp.c_str()); - - tmp = "address=" + m_listEntryTable->GetTextValue(i, 1).ToStdString(); - fs->writeLine(tmp.c_str()); - - tmp = "type=" + m_listEntryTable->GetTextValue(i, 2).ToStdString(); - fs->writeLine(tmp.c_str()); - - // only save value when FREEZE is active - if (m_listEntryTable->GetToggleValue(i, 4)) - { - tmp = "value=" + m_listEntryTable->GetTextValue(i, 3).ToStdString(); - } - else - { - tmp = "value="; - } - fs->writeLine(tmp.c_str()); - - fs->writeLine(""); - } - delete fs; - } -} - -bool MemorySearcherTool::IsAddressValid(uint32 addr) -{ - for (const auto& itr : memory_getMMURanges()) - { - if (!itr->isMapped()) - continue; - - void* ptr = itr->getPtr(); - const uint32 size = itr->getSize(); - - MEMPTR start((uint8*)ptr); - MEMPTR end((uint8*)ptr + size); - if (start.GetMPTR() <= addr && addr < end.GetMPTR()) - return true; - } - - return false; -} - -void MemorySearcherTool::OnEntryListRightClick(wxDataViewEvent& event) -{ - //void *data = reinterpret_cast(event.GetItem().GetData()); - wxMenu mnu; - //mnu.SetClientData(data); - mnu.Append(LIST_ENTRY_ADD, _("&Add new entry"))->Enable(false); - mnu.Append(LIST_ENTRY_REMOVE, _("&Remove entry")); - mnu.Bind(wxEVT_COMMAND_MENU_SELECTED, &MemorySearcherTool::OnPopupClick, this); - PopupMenu(&mnu); -} - -void MemorySearcherTool::OnResultListClick(wxMouseEvent& event) -{ - for (long selectedIndex = m_listResults->GetFirstSelected(); selectedIndex != wxNOT_FOUND; selectedIndex = m_listResults->GetNextSelected(selectedIndex)) - { - long address = m_listResults->GetItemData(selectedIndex); - auto currValue = m_listResults->GetItemText(selectedIndex, 1); - - wxString addressString = wxString::Format("0x%08lx", address); - - // description, address, type, value, freeze - wxVector data; - data.push_back(""); - data.push_back(addressString); - data.push_back(m_cbDataType->GetValue()); - data.push_back(currValue); - data.push_back(false); - m_listEntryTable->AppendItem(data); - } -} - -void MemorySearcherTool::Reset() -{ - m_searchBuffer.clear(); - m_buttonStart->SetLabelText(_("Search")); - m_textEntryTable->SetLabelText(_("Results")); - m_buttonFilter->Disable(); - m_cbDataType->Enable(); - m_textValue->SetValue(""); - m_listResults->DeleteAllItems(); - m_clear_state = false; -} - -bool MemorySearcherTool::VerifySearchValue() const -{ - const auto inputString = m_textValue->GetValue().ToStdString(); - - switch (m_searchDataType) - { - case SearchDataType_String: - return true; - case SearchDataType_Float: - { - float value; - return ConvertStringToType(inputString, value); - } - case SearchDataType_Double: - { - double value; - return ConvertStringToType(inputString, value); - } - case SearchDataType_Int8: - { - sint8 value; - return ConvertStringToType(inputString, value); - } - case SearchDataType_Int16: - { - sint16 value; - return ConvertStringToType(inputString, value); - } - case SearchDataType_Int32: - { - sint32 value; - return ConvertStringToType(inputString, value); - } - case SearchDataType_Int64: - { - sint64 value; - return ConvertStringToType(inputString, value); - } - default: - return false; - } -} - -void MemorySearcherTool::FillResultList() -{ - auto text = formatWxString(_("Results ({0})"), m_searchBuffer.size()); - m_textEntryTable->SetLabelText(text); - - m_listResults->DeleteAllItems(); - - if (m_searchBuffer.empty() || m_searchBuffer.size() > kMaxResultCount) - return; - - for (const auto& address : m_searchBuffer) - { - const auto index = m_listResults->InsertItem(0, fmt::format("{:#08x}", address.GetMPTR())); - m_listResults->SetItemData(index, address.GetMPTR()); - m_listResults->SetItem(index, 1, m_textValue->GetValue()); - } -} - -void MemorySearcherTool::RefreshResultList() -{ - std::unique_lock lock(m_mutex); - if (m_searchBuffer.empty() || m_searchBuffer.size() > kMaxResultCount) - return; - - for (int i = 0; i < m_listResults->GetItemCount(); ++i) - { - const auto addr = m_listResults->GetItemData(i); - switch (m_searchDataType) - { - case SearchDataType_String: - { - // TODO Peter - break; - } - case SearchDataType_Float: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - case SearchDataType_Double: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - case SearchDataType_Int8: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - case SearchDataType_Int16: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - case SearchDataType_Int32: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - case SearchDataType_Int64: - { - const auto value = memory_read(addr); - m_listResults->SetItem(i, 1, fmt::format("{}", value)); - break; - } - } - } -} - -void MemorySearcherTool::RefreshStashList() -{ - for (int i = 0; i < m_listEntryTable->GetItemCount(); ++i) - { - auto freeze = m_listEntryTable->GetToggleValue(i, 4); - - auto addressText = m_listEntryTable->GetTextValue(i, 1).ToStdString(); - auto type = m_listEntryTable->GetTextValue(i, 2); - - auto address = std::stol(addressText, nullptr, 16); - - // if freeze is activated, set the value instead of refreshing it - if (freeze) - { - wxVariant var; - m_listEntryTable->GetValue(var, i, 3); - if (type == kDatatypeFloat) - { - auto value = (float)var.GetDouble(); - memory_writeFloat(address, value); - } - else if (type == kDatatypeDouble) - { - auto value = var.GetDouble(); - memory_writeDouble(address, value); - } - else if (type == kDatatypeInt8) - { - auto value = var.GetInteger(); - memory_writeU8(address, value); - } - else if (type == kDatatypeInt16) - { - auto value = var.GetInteger(); - memory_writeU16(address, value); - } - else if (type == kDatatypeInt32) - { - auto value = var.GetInteger(); - memory_writeU32(address, value); - } - else if (type == kDatatypeInt64) - { - auto valueText = var.GetString().ToStdString(); - auto value = std::stoull(valueText); - memory_writeU64(address, value); - } - else if (type == kDatatypeString) - { - auto valueText = var.GetString().ToStdString(); - for (int i = 0; i < valueText.size(); ++i) - { - memory_writeU8(address + i, valueText[i]); - } - memory_writeU8(address + valueText.size(), 0x00); - } - } - else - { - if (type == kDatatypeFloat) - { - auto value = memory_readFloat(address); - m_listEntryTable->SetValue(fmt::format("{}", value), i, 3); - } - else if (type == kDatatypeDouble) - { - auto value = memory_readDouble(address); - m_listEntryTable->SetValue(fmt::format("{}", value), i, 3); - } - else if (type == kDatatypeInt8) - { - auto value = (sint8)memory_readU8(address); - m_listEntryTable->SetValue(fmt::format("{}", (int)value), i, 3); - } - else if (type == kDatatypeInt16) - { - auto value = (sint16)memory_readU16(address); - m_listEntryTable->SetValue(fmt::format("{}", value), i, 3); - } - else if (type == kDatatypeInt32) - { - auto value = (sint32)memory_readU32(address); - m_listEntryTable->SetValue(fmt::format("{}", value), i, 3); - } - else if (type == kDatatypeInt64) - { - auto value = (sint64)memory_readU64(address); - m_listEntryTable->SetValue(fmt::format("{}", value), i, 3); - } - else if (type == kDatatypeString) - { - // TODO Peter - } - } - } -} - -void MemorySearcherTool::SetSearchDataType() -{ - const auto type = m_cbDataType->GetStringSelection(); - if (type == kDatatypeFloat) - m_searchDataType = SearchDataType_Float; - else if (type == kDatatypeDouble) - m_searchDataType = SearchDataType_Double; - else if (type == kDatatypeInt8) - m_searchDataType = SearchDataType_Int8; - else if (type == kDatatypeInt16) - m_searchDataType = SearchDataType_Int16; - else if (type == kDatatypeInt32) - m_searchDataType = SearchDataType_Int32; - else if (type == kDatatypeInt64) - m_searchDataType = SearchDataType_Int64; - else if (type == kDatatypeString) - m_searchDataType = SearchDataType_String; - else - m_searchDataType = SearchDataType_None; -} - -template<> -bool MemorySearcherTool::ConvertStringToType(const std::string& inValue, sint8& outValue) const -{ - sint16 tmp; - std::istringstream iss(inValue); - iss >> std::noskipws >> tmp; - if (iss && iss.eof()) - { - if (SCHAR_MIN <= tmp && tmp <= SCHAR_MAX) - { - outValue = tmp; - return true; - } - } - return false; -} - -void MemorySearcherTool::OnPopupClick(wxCommandEvent& event) -{ - if (event.GetId() == LIST_ENTRY_REMOVE) - { - const int row = m_listEntryTable->GetSelectedRow(); - if (row == wxNOT_FOUND) - return; - - m_listEntryTable->DeleteItem(row); - } -} - -void MemorySearcherTool::OnItemEdited(wxDataViewEvent& event) -{ - auto column = event.GetColumn(); - // Edit description - if (column == 0) { } - // Edit value - else if (column == 3) - { - auto row = m_listEntryTable->GetSelectedRow(); - if (row == wxNOT_FOUND) - return; - - auto addressText = m_listEntryTable->GetTextValue(row, 1).ToStdString(); - uint32 address = std::stoul(addressText, nullptr, 16); - - auto type = m_listEntryTable->GetTextValue(row, 2); - if (type == kDatatypeFloat) - { - auto value = (float)event.GetValue().GetDouble(); - memory_writeFloat(address, value); - } - else if (type == kDatatypeDouble) - { - auto value = event.GetValue().GetDouble(); - memory_writeDouble(address, value); - } - else if (type == kDatatypeInt8) - { - auto value = event.GetValue().GetInteger(); - memory_writeU8(address, value); - } - else if (type == kDatatypeInt16) - { - auto value = event.GetValue().GetInteger(); - memory_writeU16(address, value); - } - else if (type == kDatatypeInt32) - { - auto value = event.GetValue().GetInteger(); - memory_writeU32(address, value); - } - else if (type == kDatatypeInt64) - { - auto valueText = event.GetValue().GetString().ToStdString(); - auto value = std::stoull(valueText); - memory_writeU64(address, value); - } - else if (type == kDatatypeString) - { - auto valueText = event.GetValue().GetString().ToStdString(); - for (int i = 0; i < valueText.size(); ++i) - { - memory_writeU8(address + i, valueText[i]); - } - memory_writeU8(address + valueText.size(), 0x00); - } - } -} diff --git a/src/gui/wxgui/MemorySearcherTool.h b/src/gui/wxgui/MemorySearcherTool.h deleted file mode 100644 index cb669d1c..00000000 --- a/src/gui/wxgui/MemorySearcherTool.h +++ /dev/null @@ -1,264 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include - -#include "Cafe/HW/MMU/MMU.h" -#include "util/helpers/helpers.h" -#include "wxgui/helpers/wxCustomEvents.h" - -class wxComboBox; -class wxDataViewEvent; -class wxDataViewListCtrl; -class wxStaticText; -class wxTimer; -class wxTimerEvent; - -enum SearchDataType -{ - SearchDataType_None, - SearchDataType_String, - SearchDataType_Float, - SearchDataType_Double, - SearchDataType_Int8, - SearchDataType_Int16, - SearchDataType_Int32, - SearchDataType_Int64, -}; - -struct TableEntry_t -{ - uint32 address; - const char description[32]; - SearchDataType type; - bool freeze; - uint16 dataLen; - uint8* data; -}; - -class MemorySearcherTool : public wxFrame -{ -public: - MemorySearcherTool(wxFrame* parent); - virtual ~MemorySearcherTool(); - - void OnTimerTick(wxTimerEvent& event); - void OnClose(wxCloseEvent&); - void OnSearch(wxCommandEvent&); - void OnSearchFinished(wxCommandEvent& event); - void OnUpdateGauge(wxSetGaugeValue& event); - void OnFilter(wxCommandEvent& event); - void OnResultListClick(wxMouseEvent& event); - void OnEntryListRightClick(wxDataViewEvent& event); - //void OnEntryListRightClick2(wxContextMenuEvent& event); - void OnPopupClick(wxCommandEvent& event); - - void OnItemEdited(wxDataViewEvent& event); - -private: - void Reset(); - bool VerifySearchValue() const; - void FillResultList(); - void RefreshResultList(); - void RefreshStashList(); - void SetSearchDataType(); - - void Load(); - void Save(); - - static bool IsAddressValid(uint32 addr); - - template - bool ConvertStringToType(const std::string& inValue, T& outValue) const - { - std::istringstream iss(inValue); - iss >> std::noskipws >> outValue; - return iss && iss.eof(); - } - - using ListType_t = std::vector>; - ListType_t SearchValues(SearchDataType type, void* ptr, uint32 size) - { - /*if (type == SearchDataType_String) - return SearchValues((const char* )ptr, size); - else */if (type == SearchDataType_Float) - return SearchValues((float*)ptr, size); - else if (type == SearchDataType_Double) - return SearchValues((double*)ptr, size); - else if (type == SearchDataType_Int8) - return SearchValues((sint8*)ptr, size); - else if (type == SearchDataType_Int16) - return SearchValues((sint16*)ptr, size); - else if (type == SearchDataType_Int32) - return SearchValues((sint32*)ptr, size); - else if (type == SearchDataType_Int64) - return SearchValues((sint64*)ptr, size); - return {}; - } - - ListType_t FilterValues(SearchDataType type) - { - /*if (type == SearchDataType_String) - return FilterValues(); - else */if (type == SearchDataType_Float) - return FilterValues(); - else if (type == SearchDataType_Double) - return FilterValues(); - else if (type == SearchDataType_Int8) - return FilterValues(); - else if (type == SearchDataType_Int16) - return FilterValues(); - else if (type == SearchDataType_Int32) - return FilterValues(); - else if (type == SearchDataType_Int64) - return FilterValues(); - return {}; - } - - constexpr static int kGaugeStep = 0x10000; - - template - ListType_t SearchValues(T* ptr, uint32 size) - { - const auto search_value = ConvertString(m_textValue->GetValue().ToStdString()); - - const auto* end = (T*)((uint8*)ptr + size - sizeof(T)); - - uint32 counter = 0; - std::vector> result; - for (; ptr < end; ++ptr) - { - if (!m_running) - return result; - - const auto tmp = (betype*)ptr; - if (equals(search_value, tmp->value())) - result.emplace_back(ptr); - - counter += sizeof(T); - if(counter >= kGaugeStep) - { - wxQueueEvent(this, new wxSetGaugeValue(1, m_gauge)); - counter -= kGaugeStep; - } - } - - return result; - } - - template - ListType_t FilterValues() - { - const auto search_value = ConvertString(m_textValue->GetValue().ToStdString()); - - ListType_t newSearchBuffer; - newSearchBuffer.reserve(m_searchBuffer.size()); - - for (const auto& it : m_searchBuffer) - { - if (!m_running) - return newSearchBuffer; - - const auto tmp = (betype*)it.GetPtr(); - if (equals(search_value , tmp->value())) - newSearchBuffer.emplace_back(it); - - wxQueueEvent(this, new wxSetGaugeValue(1, m_gauge)); - } - - return newSearchBuffer; - } - -wxDECLARE_EVENT_TABLE(); - - SearchDataType m_searchDataType = SearchDataType_None; - wxComboBox* m_cbDataType; - wxTextCtrl* m_textValue; - wxButton *m_buttonStart, *m_buttonFilter; - wxListView* m_listResults; - wxDataViewListCtrl* m_listEntryTable; - wxStaticText* m_textEntryTable; - wxGauge* m_gauge; - wxTimer* m_refresh_timer; - - ListType_t m_searchBuffer; - std::vector m_tableEntries; - - std::mutex m_mutex; - std::thread m_worker; - - std::atomic_bool m_running = true; - std::atomic_bool m_search_running = false; - std::vector> m_search_jobs; - - bool m_clear_state = false; -}; - -template<> -bool MemorySearcherTool::ConvertStringToType(const std::string& inValue, sint8& outValue) const; - -// -//template -//void MemorySearcherTool::FilterValues() -//{ -// auto s1 = m_textValue->GetValue(); -// auto s2 = s1.c_str(); -// auto stringValue = s2.AsChar(); -// -// T filterValue; -// ConvertStringToType(stringValue, filterValue); -// -// std::vector newSearchBuffer; -// newSearchBuffer.reserve(m_searchBuffer.size()); -// -// uint32 gaugeValue = 0; -// uint32 count = m_searchBuffer.size(); -// uint32 counter = 0; -// -// for (auto it = m_searchBuffer.begin(); it != m_searchBuffer.end(); ++it) -// { -// if (m_running) -// return; -// -// T value = memory_read(*it); -// if constexpr (std::is_same::value) -// { -// //float value1 = filterValue * 0.95f; -// float diff = filterValue - value; -// diff = fabs(diff); -// if(diff < value*0.05f) -// { -// newSearchBuffer.push_back(*it); -// } -// -// } -// else -// { -// if (value == filterValue) -// { -// newSearchBuffer.push_back(*it); -// } -// } -// -// ++counter; -// uint32 newValue = counter * 100 / count; -// if (newValue > gaugeValue + GAUGE_STEP_SIZE) -// { -// gaugeValue = newValue; -// m_gaugeValue = newValue; -// } -// } -// -// m_mutex.lock(); -// m_searchBuffer = newSearchBuffer; -// m_gaugeValue = 100; -// m_mutex.unlock(); -// m_isSearchFinished = true; -//} -// - diff --git a/src/gui/wxgui/PadViewFrame.cpp b/src/gui/wxgui/PadViewFrame.cpp deleted file mode 100644 index ca213849..00000000 --- a/src/gui/wxgui/PadViewFrame.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include "interface/WindowSystem.h" -#include "wxgui/wxgui.h" -#include "wxgui/PadViewFrame.h" - -#include - -#include "config/ActiveSettings.h" -#include "Cafe/OS/libs/swkbd/swkbd.h" -#ifdef ENABLE_OPENGL -#include "wxgui/canvas/OpenGLCanvas.h" -#endif -#ifdef ENABLE_VULKAN -#include "wxgui/canvas/VulkanCanvas.h" -#endif -#ifdef ENABLE_METAL -#include "wxgui/canvas/MetalCanvas.h" -#endif -#include "config/CemuConfig.h" -#include "wxgui/MainWindow.h" -#include "wxgui/helpers/wxHelpers.h" -#include "input/InputManager.h" - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif -#include "wxHelper.h" - -extern WindowSystem::WindowInfo g_window_info; - -#define PAD_MIN_WIDTH 320 -#define PAD_MIN_HEIGHT 180 - -PadViewFrame::PadViewFrame(wxFrame* parent) - : wxFrame(nullptr, wxID_ANY, _("GamePad View"), wxDefaultPosition, wxDefaultSize, wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxCLOSE_BOX | wxWANTS_CHARS) -{ - g_window_info.window_pad = initHandleContextFromWxWidgetsWindow(this); - - SetIcon(wxICON(M_WND_ICON128)); - wxWindow::EnableTouchEvents(wxTOUCH_PAN_GESTURES); - - SetMinClientSize({ PAD_MIN_WIDTH, PAD_MIN_HEIGHT }); - - SetPosition({ g_window_info.restored_pad_x, g_window_info.restored_pad_y }); - if (g_window_info.restored_pad_width >= PAD_MIN_WIDTH && g_window_info.restored_pad_height >= PAD_MIN_HEIGHT) - SetClientSize({ g_window_info.restored_pad_width, g_window_info.restored_pad_height }); - else - SetClientSize(wxSize(854, 480)); - - if (g_window_info.pad_maximized) - Maximize(); - - Bind(wxEVT_SIZE, &PadViewFrame::OnSizeEvent, this); - Bind(wxEVT_DPI_CHANGED, &PadViewFrame::OnDPIChangedEvent, this); - Bind(wxEVT_MOVE, &PadViewFrame::OnMoveEvent, this); - Bind(wxEVT_MOTION, &PadViewFrame::OnMouseMove, this); - - Bind(wxEVT_SET_WINDOW_TITLE, &PadViewFrame::OnSetWindowTitle, this); - - g_window_info.pad_open = true; -} - -PadViewFrame::~PadViewFrame() -{ - g_window_info.pad_open = false; -} - -bool PadViewFrame::Initialize() -{ - const wxSize client_size = GetClientSize(); - g_window_info.pad_width = client_size.GetWidth(); - g_window_info.pad_height = client_size.GetHeight(); - g_window_info.phys_pad_width = ToPhys(client_size.GetWidth()); - g_window_info.phys_pad_height = ToPhys(client_size.GetHeight()); - - return true; -} - -void PadViewFrame::InitializeRenderCanvas() -{ - auto sizer = new wxBoxSizer(wxVERTICAL); - { - #ifdef ENABLE_VULKAN - if (ActiveSettings::GetGraphicsAPI() == kVulkan) - m_render_canvas = new VulkanCanvas(this, wxSize(854, 480), false); - #endif - #ifdef ENABLE_OPENGL - if (ActiveSettings::GetGraphicsAPI() == kOpenGL) - m_render_canvas = GLCanvas_Create(this, wxSize(854, 480), false); - #endif - #ifdef ENABLE_METAL - if (ActiveSettings::GetGraphicsAPI() == kMetal) - m_render_canvas = new MetalCanvas(this, wxSize(854, 480), false); - #endif - sizer->Add(m_render_canvas, 1, wxEXPAND, 0, nullptr); - } - cemu_assert(m_render_canvas != nullptr); - SetSizer(sizer); - Layout(); - - m_render_canvas->Bind(wxEVT_KEY_UP, &PadViewFrame::OnKeyUp, this); - m_render_canvas->Bind(wxEVT_CHAR, &PadViewFrame::OnChar, this); - - m_render_canvas->Bind(wxEVT_MOTION, &PadViewFrame::OnMouseMove, this); - m_render_canvas->Bind(wxEVT_LEFT_DOWN, &PadViewFrame::OnMouseLeft, this); - m_render_canvas->Bind(wxEVT_LEFT_UP, &PadViewFrame::OnMouseLeft, this); - m_render_canvas->Bind(wxEVT_RIGHT_DOWN, &PadViewFrame::OnMouseRight, this); - m_render_canvas->Bind(wxEVT_RIGHT_UP, &PadViewFrame::OnMouseRight, this); - - m_render_canvas->Bind(wxEVT_GESTURE_PAN, &PadViewFrame::OnGesturePan, this); - - m_render_canvas->SetFocus(); - SendSizeEvent(); -} - -void PadViewFrame::DestroyCanvas() -{ - if(!m_render_canvas) - return; - m_render_canvas->Destroy(); - m_render_canvas = nullptr; -} - -void PadViewFrame::OnSizeEvent(wxSizeEvent& event) -{ - if (!IsMaximized() && !IsFullScreen()) - { - g_window_info.restored_pad_width = GetSize().x; - g_window_info.restored_pad_height = GetSize().y; - } - g_window_info.pad_maximized = IsMaximized() && !IsFullScreen(); - - const wxSize client_size = GetClientSize(); - g_window_info.pad_width = client_size.GetWidth(); - g_window_info.pad_height = client_size.GetHeight(); - g_window_info.phys_pad_width = ToPhys(client_size.GetWidth()); - g_window_info.phys_pad_height = ToPhys(client_size.GetHeight()); - g_window_info.pad_dpi_scale = GetDPIScaleFactor(); - - event.Skip(); -} - -void PadViewFrame::OnDPIChangedEvent(wxDPIChangedEvent& event) -{ - event.Skip(); - const wxSize client_size = GetClientSize(); - g_window_info.pad_width = client_size.GetWidth(); - g_window_info.pad_height = client_size.GetHeight(); - g_window_info.phys_pad_width = ToPhys(client_size.GetWidth()); - g_window_info.phys_pad_height = ToPhys(client_size.GetHeight()); - g_window_info.pad_dpi_scale = GetDPIScaleFactor(); -} - -void PadViewFrame::OnMoveEvent(wxMoveEvent& event) -{ - if (!IsMaximized() && !IsFullScreen()) - { - g_window_info.restored_pad_x = GetPosition().x; - g_window_info.restored_pad_y = GetPosition().y; - } -} - -void PadViewFrame::OnKeyUp(wxKeyEvent& event) -{ - event.Skip(); - - if (swkbd_hasKeyboardInputHook()) - return; - - const auto code = event.GetKeyCode(); - if (code == WXK_ESCAPE) - ShowFullScreen(false); - else if (code == WXK_RETURN && event.AltDown() || code == WXK_F11) - ShowFullScreen(!IsFullScreen()); -} - -void PadViewFrame::OnGesturePan(wxPanGestureEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_pad_touch.m_mutex); - auto physPos = ToPhys(event.GetPosition()); - instance.m_pad_touch.position = { physPos.x, physPos.y }; - instance.m_pad_touch.left_down = event.IsGestureStart() || !event.IsGestureEnd(); - if (event.IsGestureStart() || !event.IsGestureEnd()) - instance.m_pad_touch.left_down_toggle = true; -} - -void PadViewFrame::OnChar(wxKeyEvent& event) -{ - if (swkbd_hasKeyboardInputHook()) - swkbd_keyInput(event.GetUnicodeKey()); - - event.Skip(); -} - -void PadViewFrame::OnMouseMove(wxMouseEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_pad_touch.m_mutex); - auto physPos = ToPhys(event.GetPosition()); - instance.m_pad_mouse.position = { physPos.x, physPos.y }; - - event.Skip(); -} - -void PadViewFrame::OnMouseLeft(wxMouseEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_pad_mouse.m_mutex); - instance.m_pad_mouse.left_down = event.ButtonDown(wxMOUSE_BTN_LEFT); - auto physPos = ToPhys(event.GetPosition()); - instance.m_pad_mouse.position = { physPos.x, physPos.y }; - if (event.ButtonDown(wxMOUSE_BTN_LEFT)) - instance.m_pad_mouse.left_down_toggle = true; - -} - -void PadViewFrame::OnMouseRight(wxMouseEvent& event) -{ - auto& instance = InputManager::instance(); - - std::scoped_lock lock(instance.m_pad_mouse.m_mutex); - instance.m_pad_mouse.right_down = event.ButtonDown(wxMOUSE_BTN_LEFT); - auto physPos = ToPhys(event.GetPosition()); - instance.m_pad_mouse.position = { physPos.x, physPos.y }; - if (event.ButtonDown(wxMOUSE_BTN_RIGHT)) - instance.m_pad_mouse.right_down_toggle = true; -} - -void PadViewFrame::OnSetWindowTitle(wxCommandEvent& event) -{ - this->SetTitle(event.GetString()); -} - -void PadViewFrame::AsyncSetTitle(std::string_view windowTitle) -{ - wxCommandEvent set_title_event(wxEVT_SET_WINDOW_TITLE); - set_title_event.SetString(wxString::FromUTF8(windowTitle)); - QueueEvent(set_title_event.Clone()); -} diff --git a/src/gui/wxgui/PadViewFrame.h b/src/gui/wxgui/PadViewFrame.h deleted file mode 100644 index 7e343d7e..00000000 --- a/src/gui/wxgui/PadViewFrame.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -#define WM_CREATE_PAD (WM_USER+1) -#define WM_DESTROY_PAD (WM_USER+2) - -wxDECLARE_EVENT(EVT_PAD_CLOSE, wxCommandEvent); -wxDECLARE_EVENT(EVT_SET_WINDOW_TITLE, wxCommandEvent); - -class PadViewFrame : public wxFrame -{ -public: - PadViewFrame(wxFrame* parent); - ~PadViewFrame(); - - bool Initialize(); - void InitializeRenderCanvas(); - void DestroyCanvas(); - - void OnKeyUp(wxKeyEvent& event); - void OnChar(wxKeyEvent& event); - - void AsyncSetTitle(std::string_view windowTitle); - -private: - - void OnMouseMove(wxMouseEvent& event); - void OnMouseLeft(wxMouseEvent& event); - void OnMouseRight(wxMouseEvent& event); - void OnSizeEvent(wxSizeEvent& event); - void OnDPIChangedEvent(wxDPIChangedEvent& event); - void OnMoveEvent(wxMoveEvent& event); - void OnGesturePan(wxPanGestureEvent& event); - void OnSetWindowTitle(wxCommandEvent& event); - - wxWindow* m_render_canvas = nullptr; -}; diff --git a/src/gui/wxgui/TitleManager.cpp b/src/gui/wxgui/TitleManager.cpp deleted file mode 100644 index a4e191c9..00000000 --- a/src/gui/wxgui/TitleManager.cpp +++ /dev/null @@ -1,907 +0,0 @@ -#include "wxgui/TitleManager.h" - -#include "wxgui/helpers/wxCustomEvents.h" -#include "wxgui/helpers/wxCustomData.h" -#include "Cafe/TitleList/GameInfo.h" -#include "util/helpers/helpers.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/wxHelper.h" -#include "wxgui/components/wxTitleManagerList.h" -#include "wxgui/components/wxDownloadManagerList.h" -#include "wxgui/GameUpdateWindow.h" -#include "wxgui/dialogs/SaveImport/SaveTransfer.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "Cafe/IOSU/legacy/iosu_crypto.h" -#include "config/ActiveSettings.h" -#include "wxgui/dialogs/SaveImport/SaveImportWindow.h" -#include "Cafe/Account/Account.h" -#include "Cemu/Tools/DownloadManager/DownloadManager.h" -#include "wxgui/CemuApp.h" -#include "Cafe/TitleList/TitleList.h" -#include "Cafe/TitleList/SaveList.h" -#include "resource/embedded/resources.h" - -wxDEFINE_EVENT(wxEVT_TITLE_FOUND, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_TITLE_SEARCH_COMPLETE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_DL_TITLE_UPDATE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_DL_DISCONNECT_COMPLETE, wxCommandEvent); - -wxPanel* TitleManager::CreateTitleManagerPage() -{ - auto* panel = new wxPanel(m_notebook); - auto* sizer = new wxBoxSizer(wxVERTICAL); - { - auto* row = new wxFlexGridSizer(0, 4, 0, 0); - row->AddGrowableCol(1); - - row->Add(new wxStaticText(panel, wxID_ANY, _("Filter")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_filter = new wxTextCtrl(panel, wxID_ANY); - m_filter->Bind(wxEVT_TEXT, &TitleManager::OnFilterChanged, this); - row->Add(m_filter, 1, wxALL | wxEXPAND, 5); - - const wxImage refresh = wxHelper::LoadThemedBitmapFromPNG(PNG_REFRESH_png, sizeof(PNG_REFRESH_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)).ConvertToImage(); - m_refresh_button = new wxBitmapButton(panel, wxID_ANY, refresh.Scale(16, 16)); - m_refresh_button->Disable(); - m_refresh_button->Bind(wxEVT_BUTTON, &TitleManager::OnRefreshButton, this); - m_refresh_button->SetToolTip(_("Refresh")); - row->Add(m_refresh_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - const wxBitmap help_bitmap = wxHelper::LoadThemedBitmapFromPNG(PNG_HELP_png, sizeof(PNG_HELP_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - auto* help_button = new wxStaticBitmap(panel, wxID_ANY, help_bitmap); - help_button->SetToolTip(formatWxString(_("The following prefixes are supported:\n{0}\n{1}\n{2}\n{3}\n{4}"), - "titleid:", "name:", "type:", "version:", "region:")); - row->Add(help_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - sizer->Add(row, 0, wxEXPAND, 5); - } - - sizer->Add(new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), 0, wxEXPAND | wxALL, 5); - - m_title_list = new wxTitleManagerList(panel); - m_title_list->SetSizeHints(800, 600); - m_title_list->Bind(wxEVT_LIST_ITEM_SELECTED, &TitleManager::OnTitleSelected, this); - sizer->Add(m_title_list, 1, wxALL | wxEXPAND, 5); - - sizer->Add(new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), 0, wxEXPAND | wxALL, 5); - - { - auto* row = new wxFlexGridSizer(0, 3, 0, 0); - row->AddGrowableCol(2); - - auto* install_button = new wxButton(panel, wxID_ANY, _("Install title")); - install_button->Bind(wxEVT_BUTTON, &TitleManager::OnInstallTitle, this); - row->Add(install_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - row->Add(new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL), 0, wxEXPAND | wxALL, 5); - - { - m_save_panel = new wxPanel(panel); - auto* save_sizer = new wxFlexGridSizer(0, 7, 0, 0); - - save_sizer->Add(new wxStaticText(m_save_panel, wxID_ANY, _("Account")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_save_account_list = new wxChoice(m_save_panel, wxID_ANY); - m_save_account_list->SetMinSize({ 170, -1 }); - m_save_account_list->Bind(wxEVT_CHOICE, &TitleManager::OnSaveAccountSelected, this); - save_sizer->Add(m_save_account_list, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* save_open = new wxButton(m_save_panel, wxID_ANY, _("Open directory")); - save_open->Bind(wxEVT_BUTTON, &TitleManager::OnSaveOpenDirectory, this); - save_open->SetToolTip(_("Open the directory of the save entry")); - save_open->Disable(); - save_sizer->Add(save_open, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* save_transfer = new wxButton(m_save_panel, wxID_ANY, _("Transfer")); - save_transfer->Bind(wxEVT_BUTTON, &TitleManager::OnSaveTransfer, this); - save_transfer->SetToolTip(_("Transfers the save entry to another persistent account id")); - save_transfer->Disable(); - save_sizer->Add(save_transfer, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* save_delete = new wxButton(m_save_panel, wxID_ANY, _("Delete")); - save_delete->Bind(wxEVT_BUTTON, &TitleManager::OnSaveDelete, this); - save_delete->SetToolTip(_("Permanently delete the save entry")); - save_delete->Disable(); - save_sizer->Add(save_delete, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_save_import = new wxButton(m_save_panel, wxID_ANY, _("Import")); - m_save_import->Bind(wxEVT_BUTTON, &TitleManager::OnSaveImport, this); - m_save_import->SetToolTip(_("Imports a zipped save entry")); - save_sizer->Add(m_save_import, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxTOP | wxBOTTOM, 5); - - auto* export_bttn = new wxButton(m_save_panel, wxID_ANY, _("Export")); - export_bttn->Bind(wxEVT_BUTTON, &TitleManager::OnSaveExport, this); - export_bttn->SetToolTip(_("Exports the selected save entry as zip file")); - export_bttn->Disable(); - save_sizer->Add(export_bttn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxTOP | wxBOTTOM, 5); - - m_save_panel->SetSizerAndFit(save_sizer); - row->Add(m_save_panel, 1, wxRESERVE_SPACE_EVEN_IF_HIDDEN | wxALIGN_CENTER_VERTICAL, 0); - m_save_panel->Hide(); // hide by default - } - - sizer->Add(row, 0, wxEXPAND, 5); - } - - panel->SetSizerAndFit(sizer); - return panel; -} - -wxPanel* TitleManager::CreateDownloadManagerPage() -{ - auto* panel = new wxPanel(m_notebook); - auto* sizer = new wxBoxSizer(wxVERTICAL); - { - auto* row = new wxBoxSizer(wxHORIZONTAL); - -#if DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN - m_account = new wxChoice(panel, wxID_ANY); - m_account->SetMinSize({ 250,-1 }); - auto accounts = Account::GetAccounts(); - if (!accounts.empty()) - { - const auto id = GetConfig().account.m_persistent_id.GetValue(); - for (const auto& a : accounts) - { - m_account->Append(a.ToString(), (void*)static_cast(a.GetPersistentId())); - if(a.GetPersistentId() == id) - { - m_account->SetSelection(m_account->GetCount() - 1); - } - } - } - - row->Add(m_account, 0, wxALL, 5); -#endif - - m_connect = new wxButton(panel, wxID_ANY, _("Connect")); - m_connect->Bind(wxEVT_BUTTON, &TitleManager::OnConnect, this); - row->Add(m_connect, 0, wxALL, 5); - - sizer->Add(row, 0, wxEXPAND, 5); - } - -#if DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN - m_status_text = new wxStaticText(panel, wxID_ANY, _("Select an account and press Connect")); -#else - if(!NCrypto::HasDataForConsoleCert()) - { - m_status_text = new wxStaticText(panel, wxID_ANY, _("Valid online files are required to download eShop titles. For more information, go to the Account tab in the General Settings.")); - m_connect->Enable(false); - } - else - m_status_text = new wxStaticText(panel, wxID_ANY, _("Click on Connect to load the list of downloadable titles")); -#endif - this->Bind(wxEVT_SET_TEXT, &TitleManager::OnSetStatusText, this); - sizer->Add(m_status_text, 0, wxALL, 5); - - sizer->Add(new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL), 0, wxEXPAND | wxALL, 5); - - { - auto* row = new wxFlexGridSizer(0, 3, 0, 0); - m_show_titles = new wxCheckBox(panel, wxID_ANY, _("Show available titles")); - m_show_titles->SetValue(true); - m_show_titles->Enable(false); - row->Add(m_show_titles, 0, wxALL, 5); - m_show_titles->Bind(wxEVT_CHECKBOX, &TitleManager::OnDlFilterCheckbox, this); - - m_show_updates = new wxCheckBox(panel, wxID_ANY, _("Show available updates")); - m_show_updates->SetValue(true); - m_show_updates->Enable(false); - row->Add(m_show_updates, 0, wxALL, 5); - m_show_updates->Bind(wxEVT_CHECKBOX, &TitleManager::OnDlFilterCheckbox, this); - - m_show_installed = new wxCheckBox(panel, wxID_ANY, _("Show installed")); - m_show_installed->SetValue(true); - m_show_installed->Enable(false); - row->Add(m_show_installed, 0, wxALL, 5); - m_show_installed->Bind(wxEVT_CHECKBOX, &TitleManager::OnDlFilterCheckbox, this); - - sizer->Add(row, 0, wxEXPAND, 5); - } - - m_download_list = new wxDownloadManagerList(panel); - m_download_list->SetSizeHints(800, 600); - m_download_list->Bind(wxEVT_LIST_ITEM_SELECTED, &TitleManager::OnTitleSelected, this); - sizer->Add(m_download_list, 1, wxALL | wxEXPAND, 5); - - panel->SetSizerAndFit(sizer); - return panel; -} - -TitleManager::TitleManager(wxWindow* parent, TitleManagerPage default_page) - : wxFrame(parent, wxID_ANY, _("Title Manager"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL) -{ - SetIcon(wxICON(X_BOX)); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_notebook = new wxNotebook(this, wxID_ANY); - - m_notebook->AddPage(CreateTitleManagerPage(), _("Title Manager"), default_page == TitleManagerPage::TitleManager); - m_notebook->AddPage(CreateDownloadManagerPage(), _("Download Manager"), default_page == TitleManagerPage::DownloadManager); - - sizer->Add(m_notebook, 1, wxEXPAND); - - m_status_bar = CreateStatusBar(2, wxSTB_SIZEGRIP); - m_status_bar->SetStatusText(_("Searching for titles...")); - - this->SetSizerAndFit(sizer); - this->Centre(wxBOTH); - - this->Bind(wxEVT_SET_STATUS_BAR_TEXT, &TitleManager::OnSetStatusBarText, this); - this->Bind(wxEVT_TITLE_FOUND, &TitleManager::OnTitleFound, this); - this->Bind(wxEVT_TITLE_SEARCH_COMPLETE, &TitleManager::OnTitleSearchComplete, this); - this->Bind(wxEVT_DL_TITLE_UPDATE, &TitleManager::OnDownloadableTitleUpdate, this); - this->Bind(wxEVT_DL_DISCONNECT_COMPLETE, &TitleManager::OnDisconnect, this); - - // TODO typing on title list should change filter text and filter! - - // if download manager is already active then restore state - DownloadManager* dlMgr = DownloadManager::GetInstance(false); - if (dlMgr && dlMgr->IsConnected()) - { - dlMgr->setUserData(this); - dlMgr->registerCallbacks( - TitleManager::Callback_ConnectStatusUpdate, - TitleManager::Callback_AddDownloadableTitle, - TitleManager::Callback_RemoveDownloadableTitle); - SetConnected(true); - } - - m_callbackId = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { ((TitleManager*)ctx)->HandleTitleListCallback(evt); }, this); -} - -TitleManager::~TitleManager() -{ - CafeTitleList::UnregisterCallback(m_callbackId); - - // unregister callbacks for download manager - DownloadManager* dlMgr = DownloadManager::GetInstance(false); - if (dlMgr) - { - dlMgr->setUserData(nullptr); - dlMgr->registerCallbacks( - nullptr, - nullptr, - nullptr); - // if download manager is still downloading / installing then show a warning - if (dlMgr->hasActiveDownloads()) - { - static bool s_showedBGDownloadWarning = false; - if (!s_showedBGDownloadWarning) - { - wxMessageBox(_("Currently active downloads will continue in the background."), _("Information"), wxOK | wxCENTRE, this); - s_showedBGDownloadWarning = true; - } - } - } - - m_running = false; -} - -void TitleManager::SetFocusAndTab(TitleManagerPage page) -{ - m_notebook->SetSelection((int)page); - this->SetFocus(); -} - -void TitleManager::SetDownloadStatusText(const wxString& text) -{ - auto* evt = new wxCommandEvent(wxEVT_SET_TEXT); - evt->SetEventObject(m_status_text); - evt->SetString(text); - wxQueueEvent(this, evt); -} - -void TitleManager::HandleTitleListCallback(CafeTitleListCallbackEvent* evt) -{ - if (evt->eventType == CafeTitleListCallbackEvent::TYPE::SCAN_FINISHED) - { - auto* evt = new wxCommandEvent(wxEVT_TITLE_SEARCH_COMPLETE); - wxQueueEvent(this, evt); - } -} - -void TitleManager::OnTitleFound(wxCommandEvent& event) -{ - auto* obj = dynamic_cast(event.GetClientObject()); - wxASSERT(obj); - m_title_list->AddTitle(obj); -} - -void TitleManager::OnTitleSearchComplete(wxCommandEvent& event) -{ - m_isScanning = false; - if (m_connectRequested) - { - InitiateConnect(); - m_connectRequested = false; - } - // update status bar text - m_title_list->SortEntries(-1); - m_status_bar->SetStatusText(formatWxString(_("Found {0} games, {1} updates, {2} DLCs and {3} save entries"), - m_title_list->GetCountByType(wxTitleManagerList::EntryType::Base) + m_title_list->GetCountByType(wxTitleManagerList::EntryType::System), - m_title_list->GetCountByType(wxTitleManagerList::EntryType::Update), - m_title_list->GetCountByType(wxTitleManagerList::EntryType::Dlc), - m_title_list->GetCountByType(wxTitleManagerList::EntryType::Save) - )); - - // re-filter if any filter is set - const auto filter = m_filter->GetValue(); - if (!filter.IsEmpty()) - m_title_list->Filter(m_filter->GetValue()); - - m_title_list->AutosizeColumns(); - m_refresh_button->Enable(); -} - -void TitleManager::OnSetStatusBarText(wxSetStatusBarTextEvent& event) -{ - m_status_bar->SetStatusText(event.GetText(), event.GetNumber()); -} - -void TitleManager::OnFilterChanged(wxCommandEvent& event) -{ - event.Skip(); - m_title_list->Filter(m_filter->GetValue()); -} - -void TitleManager::OnRefreshButton(wxCommandEvent& event) -{ - m_refresh_button->Disable(); - m_isScanning = true; - // m_title_list->ClearItems(); -> Dont clear. Refresh() triggers incremental updates via notifications - m_status_bar->SetStatusText(_("Searching for titles...")); - CafeTitleList::Refresh(); -} - -void TitleManager::OnInstallTitle(wxCommandEvent& event) -{ - wxFileDialog openFileDialog(this, _("Select title to install"), "", "", "meta.xml|meta.xml", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (openFileDialog.ShowModal() == wxID_CANCEL || openFileDialog.GetPath().IsEmpty()) - return; - - fs::path filePath(openFileDialog.GetPath().wc_string()); - try - { - filePath = filePath.parent_path(); - filePath = filePath.parent_path(); - GameUpdateWindow frame(*this, filePath); - const int updateResult = frame.ShowModal(); - - if (updateResult == wxID_OK) - { - CafeTitleList::AddTitleFromPath(frame.GetTargetPath()); - } - else - { - if (frame.GetExceptionMessage().empty()) - wxMessageBox(_("Update installation has been canceled!")); - else - { - throw std::runtime_error(frame.GetExceptionMessage()); - } - } - } - catch (const AbortException&) - { - // ignored - } - catch (const std::exception& ex) - { - wxMessageBox(ex.what(), _("Update error")); - } -} - -static void PopulateSavePersistentIds(wxTitleManagerList::TitleEntry& entry) -{ - if (!entry.persistent_ids.empty()) - return; - cemu_assert(entry.type == wxTitleManagerList::EntryType::Save); - SaveInfo saveInfo = CafeSaveList::GetSaveByTitleId(entry.title_id); - if (!saveInfo.IsValid()) - return; - fs::path savePath = saveInfo.GetPath(); - savePath /= "user"; - std::error_code ec; - for (auto it : fs::directory_iterator(savePath, ec)) - { - if(!it.is_directory(ec)) - continue; - if(fs::is_empty(it.path())) - continue; - std::string dirName = it.path().filename().string(); - if(!std::regex_match(dirName, std::regex("[0-9a-fA-F]{8}"))) - continue; - entry.persistent_ids.emplace_back(ConvertString(dirName, 16)); - } -} - -void TitleManager::OnTitleSelected(wxListEvent& event) -{ - event.Skip(); - - const auto entry = m_title_list->GetSelectedTitleEntry(); - if(entry.has_value() && entry->type == wxTitleManagerList::EntryType::Save) - { - m_save_panel->Show(); - m_save_account_list->Clear(); - - PopulateSavePersistentIds(*entry); - - // an account must be selected before any of the control buttons can be used - for(auto&& v : m_save_panel->GetChildren()) - { - if (dynamic_cast(v) && v->GetId() != m_save_import->GetId()) // import is always enabled - v->Disable(); - } - - const auto& accounts = Account::GetAccounts(); - for (const auto& id : entry->persistent_ids) - { - const auto it = std::find_if(accounts.cbegin(), accounts.cend(), [id](const auto& acc) { return acc.GetPersistentId() == id; }); - if(it != accounts.cend()) - { - m_save_account_list->Append(fmt::format("{:x} ({})", id, - boost::nowide::narrow(it->GetMiiName().data(), it->GetMiiName().size())), - (void*)(uintptr_t)id); - } - else - m_save_account_list->Append(fmt::format("{:x}", id), (void*)(uintptr_t)id); - } - } - else - { - m_save_panel->Hide(); - } -} - -void TitleManager::OnSaveOpenDirectory(wxCommandEvent& event) -{ - const auto selection= m_save_account_list->GetSelection(); - if (selection == wxNOT_FOUND) - return; - - const auto entry = m_title_list->GetSelectedTitleEntry(); - if (!entry.has_value()) - return; - - const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection); - - const fs::path target = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/user/{:08x}", (uint32)(entry->title_id >> 32), (uint32)(entry->title_id & 0xFFFFFFFF), persistent_id); - if (!fs::exists(target) || !fs::is_directory(target)) - return; - - wxLaunchDefaultApplication(wxHelper::FromPath(target)); -} - -void TitleManager::OnSaveDelete(wxCommandEvent& event) -{ - const auto selection_index = m_save_account_list->GetSelection(); - if (selection_index == wxNOT_FOUND) - return; - - const auto selection = m_save_account_list->GetStringSelection(); - if (selection.IsEmpty()) - return; - - const auto msg = formatWxString(_("Are you really sure that you want to delete the save entry for {}"), selection); - const auto result = wxMessageBox(msg, _("Warning"), wxYES_NO | wxCENTRE | wxICON_EXCLAMATION, this); - if (result == wxNO) - return; - - auto entry = m_title_list->GetSelectedTitleEntry(); - if (!entry.has_value()) - return; - - const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection_index); - - const fs::path target = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/user/{:08x}", (uint32)(entry->title_id >> 32), (uint32)(entry->title_id & 0xFFFFFFFF), persistent_id); - if (!fs::exists(target) || !fs::is_directory(target)) - return; - - // edit meta saveinfo.xml - bool meta_file_edited = false; - const fs::path saveinfo = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/meta/saveinfo.xml", (uint32)(entry->title_id >> 32), (uint32)(entry->title_id & 0xFFFFFFFF)); - if (fs::exists(saveinfo) || fs::is_regular_file(saveinfo)) - { - pugi::xml_document doc; - if (doc.load_file(saveinfo.c_str())) - { - auto info_node = doc.child("info"); - if(info_node) - { - auto persistend_id_string = fmt::format(L"{:08x}", persistent_id); - const auto delete_entry = info_node.find_child([&persistend_id_string](const pugi::xml_node& node) - { - return boost::iequals(node.attribute("persistentId").as_string(), persistend_id_string); - }); - if (delete_entry) - { - info_node.remove_child(delete_entry); - meta_file_edited = doc.save_file(saveinfo.c_str()); - } - } - } - } - - if (!meta_file_edited) - cemuLog_log(LogType::Force, "TitleManager::OnSaveDelete: couldn't delete save entry in saveinfo.xml: {}", _pathToUtf8(saveinfo)); - - // remove from title entry - auto& persistent_ids = entry->persistent_ids; - persistent_ids.erase(std::remove(persistent_ids.begin(), persistent_ids.end(), persistent_id), persistent_ids.end()); - - std::error_code ec; - fs::remove_all(target, ec); - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to delete the save directory:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return; - } - - m_save_account_list->Delete(selection_index); -} - - -void TitleManager::OnSaveTransfer(wxCommandEvent& event) -{ - const auto selection_index = m_save_account_list->GetSelection(); - if (selection_index == wxNOT_FOUND) - return; - - const auto selection = m_save_account_list->GetStringSelection(); - if (selection.IsEmpty()) - return; - - auto entry = m_title_list->GetSelectedTitleEntry(); - if (!entry.has_value()) - return; - - const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection_index); - - SaveTransfer transfer(this, entry->title_id, selection, persistent_id); - if (transfer.ShowModal() == wxCANCEL) - return; - - // remove old id entry - auto& persistent_ids = entry->persistent_ids; - persistent_ids.erase(std::remove(persistent_ids.begin(), persistent_ids.end(), persistent_id), persistent_ids.end()); - - // add new id if not added yet - const auto new_id = transfer.GetTargetPersistentId(); - if (new_id != 0 && std::find(persistent_ids.cbegin(), persistent_ids.cend(), new_id) == persistent_ids.cend()) - { - persistent_ids.emplace_back(new_id); - - const auto& account = Account::GetAccount(new_id); - if(account.GetPersistentId() == new_id) - m_save_account_list->Append(fmt::format("{:x} ({})", new_id, - boost::nowide::narrow(account.GetMiiName().data(), account.GetMiiName().size())), - (void*)(uintptr_t)new_id); - else - m_save_account_list->Append(fmt::format("{:x}", new_id), (void*)(uintptr_t)new_id); - } - - m_save_account_list->Delete(selection_index); -} - -void TitleManager::OnSaveAccountSelected(wxCommandEvent& event) -{ - event.Skip(); - for (auto&& v : m_save_panel->GetChildren()) - { - if (!v->IsEnabled()) - v->Enable(); - } -} - -void TitleManager::OnSaveExport(wxCommandEvent& event) -{ - const auto selection_index = m_save_account_list->GetSelection(); - if (selection_index == wxNOT_FOUND) - return; - - const auto selection = m_save_account_list->GetStringSelection(); - if (selection.IsEmpty()) - return; - - const auto entry = m_title_list->GetSelectedTitleEntry(); - if (!entry.has_value()) - return; - - const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection_index); - - wxFileDialog path_dialog(this, _("Select a target file to export the save entry"), wxHelper::FromPath(entry->path), wxEmptyString, - fmt::format("{}|*.zip", _("Exported save entry (*.zip)")), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().IsEmpty()) - return; - - const auto path = path_dialog.GetPath(); - int ze; - auto* zip = zip_open(path.ToUTF8().data(), ZIP_CREATE | ZIP_TRUNCATE, &ze); - if (!zip) - { - zip_error_t ziperror; - zip_error_init_with_code(&ziperror, ze); - const auto error_msg = formatWxString(_("Error when creating the zip for the save entry:\n{}"), zip_error_strerror(&ziperror)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - // grab all files to zip - const auto savedir = fs::path(entry->path).append(fmt::format("user/{:08x}", persistent_id)); - const auto savedir_str = savedir.generic_u8string(); - for(const auto& it : fs::recursive_directory_iterator(savedir)) - { - if (it.path() == "." || it.path() == "..") - continue; - - const auto entryname = it.path().generic_u8string(); - if(fs::is_directory(it.path())) - { - if(zip_dir_add(zip, (const char*)entryname.substr(savedir_str.size() + 1).c_str(), ZIP_FL_ENC_UTF_8) < 0 ) - { - const auto error_msg = formatWxString(_("Error when trying to add a directory to the zip:\n{}"), zip_strerror(zip)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } - } - else - { - auto* source = zip_source_file(zip, (const char*)entryname.c_str(), 0, 0); - if(!source) - { - const auto error_msg = formatWxString(_("Error when trying to add a file to the zip:\n{}"), zip_strerror(zip)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } - - if (zip_file_add(zip, (const char*)entryname.substr(savedir_str.size() + 1).c_str(), source, ZIP_FL_ENC_UTF_8) < 0) - { - const auto error_msg = formatWxString(_("Error when trying to add a file to the zip:\n{}"), zip_strerror(zip)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - - zip_source_free(source); - } - } - } - - // add own metainfo like file and store title id for verification later - std::string metacontent = fmt::format("titleId = {:#016x}", entry->title_id); - auto* metabuff = zip_source_buffer(zip, metacontent.data(), metacontent.size(), 0); - if(zip_file_add(zip, "cemu_meta", metabuff, ZIP_FL_ENC_UTF_8) < 0) - { - const auto error_msg = formatWxString(_("Error when trying to add cemu_meta file to the zip:\n{}"), zip_strerror(zip)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - - zip_source_free(metabuff); - } - - zip_close(zip); -} - -void TitleManager::OnSaveImport(wxCommandEvent& event) -{ - auto entry = m_title_list->GetSelectedTitleEntry(); - if (!entry.has_value()) - return; - - SaveImportWindow save_import(this, entry->title_id); - if (save_import.ShowModal() == wxCANCEL) - return; - - // add new id if not added yet - auto& persistent_ids = entry->persistent_ids; - const auto new_id = save_import.GetTargetPersistentId(); - if (new_id != 0 && std::find(persistent_ids.cbegin(), persistent_ids.cend(), new_id) == persistent_ids.cend()) - { - persistent_ids.emplace_back(new_id); - - const auto& account = Account::GetAccount(new_id); - if (account.GetPersistentId() == new_id) - m_save_account_list->Append(fmt::format("{:x} ({})", new_id, - boost::nowide::narrow(account.GetMiiName().data(), account.GetMiiName().size())), - (void*)(uintptr_t)new_id); - else - m_save_account_list->Append(fmt::format("{:x}", new_id), (void*)(uintptr_t)new_id); - } - -} - -void TitleManager::InitiateConnect() -{ - // init connection to download manager if queued -#if DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN - uint32 persistentId = (uint32)(uintptr_t)m_account->GetClientData(m_account->GetSelection()); - auto& account = Account::GetAccount(persistentId); -#endif - DownloadManager* dlMgr = DownloadManager::GetInstance(); - dlMgr->reset(); - m_download_list->SetCurrentDownloadMgr(dlMgr); - - std::string deviceCertBase64 = NCrypto::CertECC::GetDeviceCertificate().encodeToBase64(); - - if (!NCrypto::SEEPROM_IsPresent()) - { - SetDownloadStatusText(_("Dumped online files not found")); - return; - } - - SetDownloadStatusText(_("Connecting...")); - // begin async connect - dlMgr->setUserData(this); - dlMgr->registerCallbacks( - TitleManager::Callback_ConnectStatusUpdate, - TitleManager::Callback_AddDownloadableTitle, - TitleManager::Callback_RemoveDownloadableTitle); - std::string accountName; - std::array accountPassword; - std::string accountCountry; -#if DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN - accountName = account.GetAccountId(); - accountPassword = account.GetAccountPasswordCache(); - accountCountry.assign(NCrypto::GetCountryAsString(account.GetCountry())); -#endif - dlMgr->connect(accountName, accountPassword, NCrypto::SEEPROM_GetRegion(), accountCountry, NCrypto::GetDeviceId(), NCrypto::GetSerial(), deviceCertBase64); -} - -void TitleManager::OnConnect(wxCommandEvent& event) -{ - if (!m_isScanning) - { - InitiateConnect(); - SetConnected(true); - } - else - { - SetDownloadStatusText(_("Getting installed title information...")); - SetConnected(true); - m_connectRequested = true; - } -} - -void TitleManager::OnDlFilterCheckbox(wxCommandEvent& event) -{ - m_download_list->Filter2(m_show_titles->GetValue(), m_show_updates->GetValue(), m_show_installed->GetValue()); - m_download_list->SortEntries(); -} - -void TitleManager::OnSetStatusText(wxCommandEvent& event) -{ - auto* text = wxDynamicCast(event.GetEventObject(), wxControl); - wxASSERT(text); - text->SetLabel(event.GetString()); -} - -void TitleManager::OnDownloadableTitleUpdate(wxCommandEvent& event) -{ - auto* obj = dynamic_cast(event.GetClientObject()); - auto entry = obj->GetData(); - m_download_list->AddOrUpdateTitle(obj); -} - -void TitleManager::OnDisconnect(wxCommandEvent& event) -{ - SetConnected(false); -} - -void TitleManager::SetConnected(bool state) -{ -#if DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN - m_account->Enable(!state); -#endif - m_connect->Enable(!state); - - m_show_titles->Enable(state); - m_show_updates->Enable(state); - m_show_installed->Enable(state); - m_download_list->Enable(state); -} - -void TitleManager::Callback_ConnectStatusUpdate(std::string statusText, DLMGR_STATUS_CODE statusCode) -{ - TitleManager* titleManager = static_cast(DownloadManager::GetInstance()->getUserData()); - titleManager->SetDownloadStatusText(wxString::FromUTF8(statusText)); - if (statusCode == DLMGR_STATUS_CODE::FAILED) - { - auto* evt = new wxCommandEvent(wxEVT_DL_DISCONNECT_COMPLETE); - wxQueueEvent(titleManager, evt); // this will set SetConnected() to false - return; - } -} - -void TitleManager::Callback_AddDownloadableTitle(const DlMgrTitleReport& titleInfo) -{ - TitleManager* titleManager = static_cast(DownloadManager::GetInstance()->getUserData()); - wxDownloadManagerList::EntryType type = wxDownloadManagerList::EntryType::Base; - if (((titleInfo.titleId >> 32) & 0xF) == 0xE) - type = wxDownloadManagerList::EntryType::Update; - else if (((titleInfo.titleId >> 32) & 0xF) == 0xC) - type = wxDownloadManagerList::EntryType::DLC; - if (titleInfo.status == DlMgrTitleReport::STATUS::INSTALLABLE || titleInfo.status == DlMgrTitleReport::STATUS::INSTALLABLE_UNFINISHED || titleInfo.status == DlMgrTitleReport::STATUS::INSTALLABLE_UPDATE) - { - // installable title - wxDownloadManagerList::TitleEntry titleEntry(type, false, titleInfo.titleId, titleInfo.version, titleInfo.isPaused); - titleEntry.name = wxString::FromUTF8(titleInfo.name); - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Available; - titleEntry.progress = 0; - if (titleInfo.status == DlMgrTitleReport::STATUS::INSTALLABLE_UNFINISHED) - titleEntry.progress = 1; - if (titleInfo.status == DlMgrTitleReport::STATUS::INSTALLABLE_UPDATE) - titleEntry.progress = 2; - auto* evt = new wxCommandEvent(wxEVT_DL_TITLE_UPDATE); - evt->SetClientObject(new wxCustomData(titleEntry)); - wxQueueEvent(titleManager, evt); - } - else - { - // package - wxDownloadManagerList::TitleEntry titleEntry(type, true, titleInfo.titleId, titleInfo.version, titleInfo.isPaused); - titleEntry.name = wxString::FromUTF8(titleInfo.name); - titleEntry.progress = titleInfo.progress; - titleEntry.progressMax = titleInfo.progressMax; - switch (titleInfo.status) - { - case DlMgrTitleReport::STATUS::INITIALIZING: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Initializing; - break; - case DlMgrTitleReport::STATUS::QUEUED: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Queued; - break; - case DlMgrTitleReport::STATUS::CHECKING: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Checking; - break; - case DlMgrTitleReport::STATUS::DOWNLOADING: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Downloading; - titleEntry.progress = titleInfo.progress; - break; - case DlMgrTitleReport::STATUS::VERIFYING: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Verifying; - break; - case DlMgrTitleReport::STATUS::INSTALLING: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Installing; - break; - case DlMgrTitleReport::STATUS::INSTALLED: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Installed; - break; - case DlMgrTitleReport::STATUS::HAS_ERROR: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::Error; - titleEntry.errorMsg = titleInfo.errorMsg; - break; - default: - titleEntry.status = wxDownloadManagerList::TitleDownloadStatus::None; - break; - } - - auto* evt = new wxCommandEvent(wxEVT_DL_TITLE_UPDATE); - evt->SetClientObject(new wxCustomData(titleEntry)); - wxQueueEvent(titleManager, evt); - } -} - -void TitleManager::Callback_RemoveDownloadableTitle(uint64 titleId, uint16 version) -{ - TitleManager* titleManager = static_cast(DownloadManager::GetInstance()->getUserData()); - assert_dbg(); -} diff --git a/src/gui/wxgui/TitleManager.h b/src/gui/wxgui/TitleManager.h deleted file mode 100644 index ae284040..00000000 --- a/src/gui/wxgui/TitleManager.h +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "Cemu/Tools/DownloadManager/DownloadManager.h" - -#define DOWNLOADMGR_HAS_ACCOUNT_DROPDOWN 0 - -class wxCheckBox; -class wxStaticText; -class wxListEvent; -class wxSetStatusBarTextEvent; -class wxTitleManagerList; -class wxDownloadManagerList; -class wxTextCtrl; -class wxStatusBar; -class wxImageList; -class wxBitmapButton; -class wxPanel; -class wxChoice; -class wxNotebook; - -enum class TitleManagerPage -{ - TitleManager = 0, - DownloadManager = 1 -}; - -enum class DLMGR_STATUS_CODE; - -class TitleManager : public wxFrame -{ -public: - TitleManager(wxWindow* parent, TitleManagerPage default_page = TitleManagerPage::TitleManager); - ~TitleManager(); - - void SetFocusAndTab(TitleManagerPage page); - - void SetDownloadStatusText(const wxString& text); - -private: - wxPanel* CreateTitleManagerPage(); - wxPanel* CreateDownloadManagerPage(); - - // title manager - void OnTitleFound(wxCommandEvent& event); - void OnTitleSearchComplete(wxCommandEvent& event); - void OnSetStatusBarText(wxSetStatusBarTextEvent& event); - void OnFilterChanged(wxCommandEvent& event); - void OnRefreshButton(wxCommandEvent& event); - void OnInstallTitle(wxCommandEvent& event); - void OnTitleSelected(wxListEvent& event); - void OnSaveOpenDirectory(wxCommandEvent& event); - void OnSaveDelete(wxCommandEvent& event); - void OnSaveTransfer(wxCommandEvent& event); - void OnSaveAccountSelected(wxCommandEvent& event); - void OnSaveExport(wxCommandEvent& event); - void OnSaveImport(wxCommandEvent& event); - - void HandleTitleListCallback(struct CafeTitleListCallbackEvent* evt); - - wxNotebook* m_notebook; - - uint32 m_callbackId; - - // title manager - wxTextCtrl* m_filter; - wxTitleManagerList* m_title_list; - wxStatusBar* m_status_bar; - wxBitmapButton* m_refresh_button; - wxPanel* m_save_panel; - wxChoice* m_save_account_list; - wxButton* m_save_import; - - bool m_isScanning{ true }; // set when CafeTitleList is scanning - - std::atomic_bool m_running = true; - - // download manager - void InitiateConnect(); - void OnConnect(wxCommandEvent& event); - void OnSetStatusText(wxCommandEvent& event); - void OnDownloadableTitleUpdate(wxCommandEvent& event); - void OnDisconnect(wxCommandEvent& event); - - void OnDlFilterCheckbox(wxCommandEvent& event); - - void SetConnected(bool state); - - static void Callback_ConnectStatusUpdate(std::string statusText, DLMGR_STATUS_CODE statusCode); - static void Callback_AddDownloadableTitle(const struct DlMgrTitleReport& titleInfo); - static void Callback_RemoveDownloadableTitle(uint64 titleId, uint16 version); - - wxChoice* m_account; - wxButton* m_connect; - wxStaticText* m_status_text; - wxCheckBox *m_show_titles, *m_show_updates, *m_show_installed; - wxDownloadManagerList* m_download_list; - bool m_connectRequested{false}; // connect was clicked before m_foundTitles was available -}; diff --git a/src/gui/wxgui/canvas/IRenderCanvas.h b/src/gui/wxgui/canvas/IRenderCanvas.h deleted file mode 100644 index 125ae954..00000000 --- a/src/gui/wxgui/canvas/IRenderCanvas.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -// base class for all render interfaces -class IRenderCanvas -{ -public: - IRenderCanvas(bool is_main_window) - : m_is_main_window(is_main_window) {} - -protected: - bool m_is_main_window; -}; \ No newline at end of file diff --git a/src/gui/wxgui/canvas/MetalCanvas.cpp b/src/gui/wxgui/canvas/MetalCanvas.cpp deleted file mode 100644 index ee58abcd..00000000 --- a/src/gui/wxgui/canvas/MetalCanvas.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "wxgui/canvas/MetalCanvas.h" -#include "Cafe/HW/Latte/Renderer/Metal/MetalRenderer.h" - -#include -#include - -MetalCanvas::MetalCanvas(wxWindow* parent, const wxSize& size, bool is_main_window) - : IRenderCanvas(is_main_window), wxWindow(parent, wxID_ANY, wxDefaultPosition, size, wxNO_FULL_REPAINT_ON_RESIZE | wxWANTS_CHARS) -{ - Bind(wxEVT_PAINT, &MetalCanvas::OnPaint, this); - Bind(wxEVT_SIZE, &MetalCanvas::OnResize, this); - - auto& canvas = is_main_window ? WindowSystem::GetWindowInfo().canvas_main : WindowSystem::GetWindowInfo().canvas_pad; - canvas = initHandleContextFromWxWidgetsWindow(this); - - try - { - if (is_main_window) - g_renderer = std::make_unique(); - - auto metal_renderer = MetalRenderer::GetInstance(); - metal_renderer->InitializeLayer({size.x, size.y}, is_main_window); - } - catch(const std::exception& ex) - { - cemuLog_log(LogType::Force, "Error when initializing Metal renderer: {}", ex.what()); - auto msg = formatWxString(_("Error when initializing Metal renderer:\n{}"), ex.what()); - wxMessageDialog dialog(this, msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - dialog.ShowModal(); - exit(0); - } - - wxWindow::EnableTouchEvents(wxTOUCH_PAN_GESTURES); -} - -MetalCanvas::~MetalCanvas() -{ - Unbind(wxEVT_PAINT, &MetalCanvas::OnPaint, this); - Unbind(wxEVT_SIZE, &MetalCanvas::OnResize, this); - - MetalRenderer* mtlr = (MetalRenderer*)g_renderer.get(); - if (mtlr) - mtlr->ShutdownLayer(m_is_main_window); -} - -void MetalCanvas::OnPaint(wxPaintEvent& event) -{ -} - -void MetalCanvas::OnResize(wxSizeEvent& event) -{ - const wxSize size = GetSize(); - if (size.GetWidth() == 0 || size.GetHeight() == 0) - return; - - const wxRect refreshRect(size); - RefreshRect(refreshRect, false); - - auto metal_renderer = MetalRenderer::GetInstance(); - metal_renderer->ResizeLayer({size.x, size.y}, m_is_main_window); -} diff --git a/src/gui/wxgui/canvas/MetalCanvas.h b/src/gui/wxgui/canvas/MetalCanvas.h deleted file mode 100644 index a0a3a616..00000000 --- a/src/gui/wxgui/canvas/MetalCanvas.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "wxgui/canvas/IRenderCanvas.h" - -#include - -#include - -class MetalCanvas : public IRenderCanvas, public wxWindow -{ -public: - MetalCanvas(wxWindow* parent, const wxSize& size, bool is_main_window); - ~MetalCanvas(); - -private: - - void OnPaint(wxPaintEvent& event); - void OnResize(wxSizeEvent& event); -}; diff --git a/src/gui/wxgui/canvas/OpenGLCanvas.cpp b/src/gui/wxgui/canvas/OpenGLCanvas.cpp deleted file mode 100644 index d80d696e..00000000 --- a/src/gui/wxgui/canvas/OpenGLCanvas.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include "wxgui/canvas/OpenGLCanvas.h" - -#include "wxgui/canvas/IRenderCanvas.h" - -#include "Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.h" - -#include "config/CemuConfig.h" - -#include "Common/GLInclude/GLInclude.h" -#include // this includes GL/gl.h, avoid using this in a header because it would contaminate our own OpenGL definitions (GLInclude) - -static const int g_gl_attribute_list[] = -{ - WX_GL_RGBA, - WX_GL_DOUBLEBUFFER, - WX_GL_DEPTH_SIZE, 16, - - WX_GL_MIN_RED, 8, - WX_GL_MIN_GREEN, 8, - WX_GL_MIN_BLUE, 8, - WX_GL_MIN_ALPHA, 8, - - WX_GL_STENCIL_SIZE, 8, - - //WX_GL_MAJOR_VERSION, 4, - //WX_GL_MINOR_VERSION, 1, - //wx_GL_COMPAT_PROFILE, - - 0, // end of list -}; - -class OpenGLCanvas; - -class GLCanvasManager : public OpenGLCanvasCallbacks -{ - public: - GLCanvasManager() - { - SetOpenGLCanvasCallbacks(this); - } - - ~GLCanvasManager() - { - ClearOpenGLCanvasCallbacks(); - } - - void SetTVView(OpenGLCanvas* canvas) - { - m_tvView = canvas; - } - - void SetPadView(OpenGLCanvas* canvas) - { - m_padView = canvas; - } - - void SetGLContext(wxGLContext* context) - { - m_glContext = context; - } - - void DeleteGLContext() - { - if (m_tvView == nullptr && m_padView == nullptr && m_glContext) - { - delete m_glContext; - m_glContext = nullptr; - } - } - - bool HasPadViewOpen() const - { - return m_padView != nullptr; - } - - bool MakeCurrent(bool padView); - - void SwapBuffers(bool swapTV, bool swapDRC); - - private: - wxGLContext* m_glContext = nullptr; - OpenGLCanvas* m_tvView = nullptr; - OpenGLCanvas* m_padView = nullptr; -} s_glCanvasManager; - -class OpenGLCanvas : public IRenderCanvas, public wxGLCanvas -{ -public: - OpenGLCanvas(wxWindow* parent, const wxSize& size, bool is_main_window) - : IRenderCanvas(is_main_window), wxGLCanvas(parent, wxID_ANY, g_gl_attribute_list, wxDefaultPosition, size, wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS) - { - if (m_is_main_window) - { - s_glCanvasManager.SetTVView(this); - s_glCanvasManager.SetGLContext(new wxGLContext(this)); - - g_renderer = std::make_unique(); - } - else - { - s_glCanvasManager.SetPadView(this); - } - - wxWindow::EnableTouchEvents(wxTOUCH_PAN_GESTURES); - } - - ~OpenGLCanvas() override - { - // todo - if this is the main window, make sure the renderer has been shut down - - if (m_is_main_window) - s_glCanvasManager.SetTVView(nullptr); - else - s_glCanvasManager.SetPadView(nullptr); - - s_glCanvasManager.DeleteGLContext(); - } - - void UpdateVSyncState() - { - int configValue = GetConfig().vsync.GetValue(); - configValue = configValue > 0 ? 1 : 0; - if(m_activeVSyncState != configValue) - { -#if BOOST_OS_WINDOWS - if(wglSwapIntervalEXT) - wglSwapIntervalEXT(configValue); // 1 = enabled, 0 = disabled -#elif BOOST_OS_LINUX || BOOST_OS_BSD - if (eglSwapInterval) - { - if (eglSwapInterval(eglGetCurrentDisplay(), configValue) == EGL_FALSE) - { - cemuLog_log(LogType::Force, "Failed to set vsync using EGL"); - } - } -#else - cemuLog_log(LogType::Force, "OpenGL vsync not implemented"); -#endif - m_activeVSyncState = configValue; - } - } - -private: - int m_activeVSyncState = -1; - //wxGLContext* m_context = nullptr; -}; - -wxWindow* GLCanvas_Create(wxWindow* parent, const wxSize& size, bool is_main_window) -{ - return new OpenGLCanvas(parent, size, is_main_window); -} - -void GLCanvasManager::SwapBuffers(bool swapTV, bool swapDRC) -{ - if (swapTV && m_tvView) - { - MakeCurrent(false); - m_tvView->SwapBuffers(); - m_tvView->UpdateVSyncState(); - } - - if (swapDRC && m_padView) - { - MakeCurrent(true); - m_padView->SwapBuffers(); - m_padView->UpdateVSyncState(); - } - - MakeCurrent(false); -} - -bool GLCanvasManager::MakeCurrent(bool padView) -{ - OpenGLCanvas* canvas = padView ? m_padView : m_tvView; - if (!canvas) - return false; - m_glContext->SetCurrent(*canvas); - return true; -} diff --git a/src/gui/wxgui/canvas/OpenGLCanvas.h b/src/gui/wxgui/canvas/OpenGLCanvas.h deleted file mode 100644 index 167058fe..00000000 --- a/src/gui/wxgui/canvas/OpenGLCanvas.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once -#include - -wxWindow* GLCanvas_Create(wxWindow* parent, const wxSize& size, bool is_main_window); \ No newline at end of file diff --git a/src/gui/wxgui/canvas/VulkanCanvas.cpp b/src/gui/wxgui/canvas/VulkanCanvas.cpp deleted file mode 100644 index 5b0a0a33..00000000 --- a/src/gui/wxgui/canvas/VulkanCanvas.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "wxgui/canvas/VulkanCanvas.h" -#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h" - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND -#include "wxgui/helpers/wxWayland.h" -#endif - -#include -#include - -VulkanCanvas::VulkanCanvas(wxWindow* parent, const wxSize& size, bool is_main_window) - : IRenderCanvas(is_main_window), wxWindow(parent, wxID_ANY, wxDefaultPosition, size, wxNO_FULL_REPAINT_ON_RESIZE | wxWANTS_CHARS) -{ - Bind(wxEVT_PAINT, &VulkanCanvas::OnPaint, this); - Bind(wxEVT_SIZE, &VulkanCanvas::OnResize, this); -#if __WXMSW__ - MSWDisableComposited(); -#endif - - auto& canvas = is_main_window ? WindowSystem::GetWindowInfo().canvas_main : WindowSystem::GetWindowInfo().canvas_pad; - canvas = initHandleContextFromWxWidgetsWindow(this); - #if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - if (canvas.backend == WindowSystem::WindowHandleInfo::Backend::Wayland) - { - m_subsurface = std::make_unique(this); - canvas.surface = m_subsurface->getSurface(); - } - #endif - - cemu_assert(g_vulkan_available); - - try - { - if (is_main_window) - g_renderer = std::make_unique(); - - auto vulkan_renderer = VulkanRenderer::GetInstance(); - vulkan_renderer->InitializeSurface({size.x, size.y}, is_main_window); - } - catch(const std::exception& ex) - { - cemuLog_log(LogType::Force, "Error when initializing Vulkan renderer: {}", ex.what()); - auto msg = formatWxString(_("Error when initializing Vulkan renderer:\n{}"), ex.what()); - wxMessageDialog dialog(this, msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - dialog.ShowModal(); - _exit(0); - } - - wxWindow::EnableTouchEvents(wxTOUCH_PAN_GESTURES); -} - -VulkanCanvas::~VulkanCanvas() -{ - Unbind(wxEVT_PAINT, &VulkanCanvas::OnPaint, this); - Unbind(wxEVT_SIZE, &VulkanCanvas::OnResize, this); - - if(!m_is_main_window) - { - VulkanRenderer* vkr = (VulkanRenderer*)g_renderer.get(); - if(vkr) - vkr->StopUsingPadAndWait(); - } -} - -void VulkanCanvas::OnPaint(wxPaintEvent& event) -{ -} - -void VulkanCanvas::OnResize(wxSizeEvent& event) -{ - const wxSize size = GetSize(); - if (size.GetWidth() == 0 || size.GetHeight() == 0) - return; - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - if(m_subsurface) - { - auto sRect = GetScreenRect(); - m_subsurface->setSize(sRect.GetX(), sRect.GetY(), sRect.GetWidth(), sRect.GetHeight()); - } -#endif - - const wxRect refreshRect(size); - RefreshRect(refreshRect, false); -} diff --git a/src/gui/wxgui/canvas/VulkanCanvas.h b/src/gui/wxgui/canvas/VulkanCanvas.h deleted file mode 100644 index 731638f3..00000000 --- a/src/gui/wxgui/canvas/VulkanCanvas.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "wxgui/canvas/IRenderCanvas.h" - -#include - -#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" -#include - -class VulkanCanvas : public IRenderCanvas, public wxWindow -{ -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - std::unique_ptr m_subsurface; -#endif -public: - VulkanCanvas(wxWindow* parent, const wxSize& size, bool is_main_window); - ~VulkanCanvas(); - -private: - - void OnPaint(wxPaintEvent& event); - void OnResize(wxSizeEvent& event); -}; diff --git a/src/gui/wxgui/components/TextList.cpp b/src/gui/wxgui/components/TextList.cpp deleted file mode 100644 index f5e3f133..00000000 --- a/src/gui/wxgui/components/TextList.cpp +++ /dev/null @@ -1,320 +0,0 @@ -#include "wxgui/wxgui.h" -#include "TextList.h" - -#include "debugger/DisasmCtrl.h" - -#include -#include - -TextList::~TextList() -{ - m_tooltip_timer->Stop(); - - this->Unbind(wxEVT_MOTION, &TextList::OnMouseMoveEvent, this); - this->Unbind(wxEVT_KEY_DOWN, &TextList::OnKeyDownEvent, this); - this->Unbind(wxEVT_KEY_UP, &TextList::OnKeyUpEvent, this); - this->Unbind(wxEVT_PAINT, &TextList::OnPaintEvent, this); - this->Unbind(wxEVT_LEFT_DOWN, &TextList::OnMouseDownEvent, this); - this->Unbind(wxEVT_LEFT_UP, &TextList::OnMouseUpEvent, this); - this->Unbind(wxEVT_LEFT_DCLICK, &TextList::OnMouseDClickEvent, this); - this->Unbind(wxEVT_CONTEXT_MENU, &TextList::OnContextMenu, this); - this->Unbind(wxEVT_ERASE_BACKGROUND, &TextList::OnEraseBackground, this); -} - -TextList::TextList(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style) - : wxControl(parent, id, pos, size, style), wxScrollHelper(this) -{ - m_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - this->wxWindowBase::SetBackgroundStyle(wxBG_STYLE_PAINT); - - wxInfoDC dc(this); - this->DoPrepareReadOnlyDC(dc); - dc.SetFont(m_font); - m_line_height = dc.GetCharHeight(); - m_char_width = dc.GetCharWidth(); - - m_yScrollPixelsPerLine = m_line_height; - - this->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_DEFAULT); - - m_tooltip = new wxToolTip(wxEmptyString); - - this->Bind(wxEVT_MOTION, &TextList::OnMouseMoveEvent, this); - this->Bind(wxEVT_KEY_DOWN, &TextList::OnKeyDownEvent, this); - this->Bind(wxEVT_KEY_UP, &TextList::OnKeyUpEvent, this); - this->Bind(wxEVT_PAINT, &TextList::OnPaintEvent, this); - this->Bind(wxEVT_LEFT_DOWN, &TextList::OnMouseDownEvent, this); - this->Bind(wxEVT_LEFT_UP, &TextList::OnMouseUpEvent, this); - this->Bind(wxEVT_LEFT_DCLICK, &TextList::OnMouseDClickEvent, this); - this->Bind(wxEVT_CONTEXT_MENU, &TextList::OnContextMenu, this); - this->Bind(wxEVT_ERASE_BACKGROUND, &TextList::OnEraseBackground, this); - - m_tooltip_window = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); - m_tooltip_window->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); - m_tooltip_window->Hide(); - - m_tooltip_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &TextList::OnTooltipTimer, this); -} - -void TextList::RefreshControl(const wxRect* update_region) -{ - if(update_region) - Refresh(true, update_region); - else - { - wxRect region = GetClientRect(); - update_region = ®ion; - Refresh(true, update_region); - } -} - -void TextList::RefreshLine(uint32 line) -{ - wxRect update_region = GetClientRect(); - update_region.y = (sint32)line * m_line_height; - update_region.height = m_line_height; - CalcScrolledPosition(0, update_region.y, nullptr, &update_region.y); - // debug_printf("update: <%x, %x>\n", update_region.y, update_region.height); - Refresh(true, &update_region); -} - -wxSize TextList::DoGetVirtualSize() const -{ - return {wxDefaultCoord, (int)m_element_count * m_line_height}; -} - -void TextList::DoSetSize(int x, int y, int width, int height, int sizeFlags) -{ - wxControl::DoSetSize(x, y, width, height, sizeFlags); - - m_elements_visible = (height + m_line_height - 1) / m_line_height; - Refresh(); -} - -void TextList::SetScrollPos(int orient, int pos, bool refresh) -{ - wxControl::SetScrollPos(orient, pos, refresh); -} - -void TextList::DrawLineBackground(wxDC& dc, uint32 line, const wxColour& colour, uint32 lines) const -{ - wxRect rect; - rect.x = GetPosition().x; - rect.y = line * m_line_height; - rect.width = GetSize().x; - rect.height = m_line_height * lines; - - dc.SetBrush(colour); - dc.DrawRectangle(rect); -} - -void TextList::DrawLineBackground(wxDC& dc, const wxPoint& position, const wxColour& colour, uint32 lines) const -{ - wxRect rect; - rect.x = position.x; - rect.y = position.y; - rect.width = this->GetSize().x; - rect.height = m_line_height * lines; - - dc.SetBrush(colour); - dc.DrawRectangle(rect); -} - -bool TextList::SetElementCount(size_t element_count) -{ - if (m_element_count == element_count) - return false; - - if (element_count > 0x7FFFFFFF) - element_count = 0x7FFFFFFF; - - m_element_count = element_count; - this->AdjustScrollbars(); - return true; -} - -uint32 TextList::GetElementCount() const -{ - return m_element_count; -} - -bool TextList::IsKeyDown(sint32 keycode) -{ - return m_key_states[keycode]; -} - -void TextList::WriteText(wxDC& dc, const wxString& text, wxPoint& position) const -{ - dc.ResetBoundingBox(); - dc.DrawText(text, position); - position.x += dc.MaxX() - dc.MinX(); -} - -void TextList::WriteText(wxDC& dc, const wxString& text, wxPoint& position, const wxColour& color) const -{ - dc.SetTextForeground(color); - WriteText(dc, text, position); -} - -void TextList::NextLine(wxPoint& position, const wxPoint* start_position) const -{ - position.y += m_line_height; - - if (start_position) - position.x = start_position->x; -} - -void TextList::OnMouseDown() {} -void TextList::OnMouseUp() {} - -bool TextList::OnShowTooltip(const wxPoint& position, uint32 line) -{ - return false; -} - -void TextList::OnMouseMoveEvent(wxMouseEvent& event) -{ - m_tooltip_timer->Stop(); - m_tooltip_timer->StartOnce(250); - - wxPoint position = event.GetPosition(); - CalcUnscrolledPosition(position.x, position.y, &position.x, &position.y); - - m_mouse_position = position; - - if(m_mouse_down) - m_selection.SetBottomRight(position); - - const sint32 line = position.y / m_line_height; - OnMouseMove(position, line); -} - -void TextList::OnKeyDownEvent(wxKeyEvent& event) -{ - const auto key_code = event.GetKeyCode(); - const auto it = m_key_states.find(key_code); - if(it == m_key_states.end() || !it->second) - { - m_key_states[key_code] = true; - OnKeyPressed(key_code, event.GetPosition()); - } - - event.Skip(); -} - -void TextList::OnKeyUpEvent(wxKeyEvent& event) -{ - m_key_states[event.GetKeyCode()] = false; -} - -void TextList::OnMouseDownEvent(wxMouseEvent& event) -{ - m_mouse_down = true; - - wxPoint position = event.GetPosition(); - CalcUnscrolledPosition(position.x, position.y, &position.x, &position.y); - m_selection.SetPosition(position); - m_selection.SetBottomRight(position); - - OnMouseDown(); - - event.Skip(); -} - -void TextList::OnMouseUpEvent(wxMouseEvent& event) -{ - m_mouse_down = false; - OnMouseUp(); - - event.Skip(); -} - -void TextList::OnMouseDClickEvent(wxMouseEvent& event) -{ - wxPoint position = event.GetPosition(); - CalcUnscrolledPosition(position.x, position.y, &position.x, &position.y); - m_selection.SetPosition(position); - m_selection.SetBottomRight(position); - - const uint32 line = position.y / m_line_height; - OnMouseDClick(position, line); -} - -void TextList::OnContextMenu(wxContextMenuEvent& event) -{ - wxPoint position = event.GetPosition(); - if (position == wxDefaultPosition) - return; - - wxPoint clientPosition = ScreenToClient(position); - - CalcUnscrolledPosition(clientPosition.x, clientPosition.y, &clientPosition.x, &clientPosition.y); - m_selection.SetPosition(clientPosition); - m_selection.SetBottomRight(clientPosition); - - const uint32 line = clientPosition.y / m_line_height; - OnContextMenu(clientPosition, line); -} - -void TextList::OnTooltipTimer(wxTimerEvent& event) -{ - m_tooltip_window->Hide(); - - const auto cursor_position = wxGetMousePosition(); - - const auto position = GetScreenPosition(); - if (cursor_position.x < position.x || cursor_position.y < position.y) - return; - - const auto size = GetSize(); - if (position.x + size.x < cursor_position.x || position.y + size.y < cursor_position.y) - return; - - const sint32 line = position.y / m_line_height; - if(OnShowTooltip(m_mouse_position, line)) - { - m_tooltip_window->SetPosition(wxPoint(m_mouse_position.x + 15, m_mouse_position.y + 15)); - m_tooltip_window->SendSizeEvent(); - m_tooltip_window->Show(); - } -} - -void TextList::OnPaintEvent(wxPaintEvent& event) -{ - wxAutoBufferedPaintDC dc(m_targetWindow); - - // get window position - auto position = GetPosition(); - - // get current real position - wxRect rect_update = GetUpdateRegion().GetBox(); - const auto count = (uint32)std::ceil((float)rect_update.GetHeight() / m_line_height); - - position.y = (rect_update.y / m_line_height) * m_line_height; - - // paint background - dc.SetFont(m_font); - const wxColour window_colour = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - dc.SetBrush(GetBackgroundColour()); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.DrawRectangle(rect_update); - - //// paint selection - //if (!m_selected_text.eof()) - //{ - // dc.SetBrush(*wxBLUE_BRUSH); - // dc.SetPen(*wxBLUE_PEN); - // dc.DrawRectangle(m_selection); - //} - - sint32 start; - CalcUnscrolledPosition(rect_update.x, rect_update.y, nullptr, &start); - - start /= m_line_height; - m_scrolled_to_end = (start + count) >= m_element_count; - - OnDraw(dc, start, count, position); - - // removed Update() here since all text is white -} \ No newline at end of file diff --git a/src/gui/wxgui/components/TextList.h b/src/gui/wxgui/components/TextList.h deleted file mode 100644 index 9a722a21..00000000 --- a/src/gui/wxgui/components/TextList.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include - -#include -#include - - -class TextList : public wxControl, public wxScrollHelper -{ -public: - virtual ~TextList(); - TextList(wxWindow* parent, wxWindowID id, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, long style = 0); - - void RefreshControl(const wxRect* update_region = nullptr); - void RefreshLine(uint32 line); - - wxSize DoGetVirtualSize() const override; - void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; - void SetScrollPos(int orient, int pos, bool refresh) override; - void DrawLineBackground(wxDC& dc, uint32 line, const wxColour& colour, uint32 lines = 1) const; - void DrawLineBackground(wxDC& dc, const wxPoint& position, const wxColour& colour, uint32 lines = 1) const; - - bool SetElementCount(size_t element_count); - uint32 GetElementCount() const; - bool IsKeyDown(sint32 keycode); - -protected: - void WriteText(wxDC& dc, const wxString& text, wxPoint& position) const; - void WriteText(wxDC& dc, const wxString& text, wxPoint& position, const wxColour& color) const; - void NextLine(wxPoint& position, const wxPoint* start_position = nullptr) const; - - virtual void OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) {}; - virtual void OnMouseMove(const wxPoint& position, uint32 line) {} - virtual void OnKeyPressed(sint32 key_code, const wxPoint& position) {} - virtual void OnMouseDown(); - virtual void OnMouseUp(); - virtual void OnMouseDClick(const wxPoint& position, uint32 line) {} - virtual void OnContextMenu(const wxPoint& position, uint32 line) {} - virtual bool OnShowTooltip(const wxPoint& position, uint32 line); - void OnEraseBackground(wxEraseEvent&) {} - - sint32 m_line_height; - sint32 m_char_width; - size_t m_elements_visible = 0; - size_t m_element_count = 0; - bool m_scrolled_to_end = true; - - std::wstringstream m_selected_text; - bool m_mouse_down = false; - wxRect m_selection; - wxPanel* m_tooltip_window; - -private: - void OnPaintEvent(wxPaintEvent& event); - void OnMouseMoveEvent(wxMouseEvent& event); - void OnKeyDownEvent(wxKeyEvent& event); - void OnKeyUpEvent(wxKeyEvent& event); - void OnMouseDownEvent(wxMouseEvent& event); - void OnMouseUpEvent(wxMouseEvent& event); - void OnMouseDClickEvent(wxMouseEvent& event); - void OnContextMenu(wxContextMenuEvent& event); - void OnTooltipTimer(wxTimerEvent& event); - - std::unordered_map m_key_states; - - wxFont m_font; - - wxTimer* m_tooltip_timer; - wxPoint m_mouse_position; -}; diff --git a/src/gui/wxgui/components/wxDownloadManagerList.cpp b/src/gui/wxgui/components/wxDownloadManagerList.cpp deleted file mode 100644 index e3919cff..00000000 --- a/src/gui/wxgui/components/wxDownloadManagerList.cpp +++ /dev/null @@ -1,813 +0,0 @@ -#include "wxgui/components/wxDownloadManagerList.h" - -#include "wxHelper.h" -#include "wxgui/helpers/wxHelpers.h" -#include "util/helpers/SystemException.h" -#include "Cafe/TitleList/GameInfo.h" -#include "wxgui/components/wxGameList.h" -#include "wxgui/helpers/wxCustomEvents.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "config/ActiveSettings.h" -#include "wxgui/ChecksumTool.h" -#include "Cemu/Tools/DownloadManager/DownloadManager.h" -#include "Cafe/TitleList/TitleId.h" -#include "wxgui/MainWindow.h" - -wxDEFINE_EVENT(wxEVT_REMOVE_ENTRY, wxCommandEvent); - -wxDownloadManagerList::wxDownloadManagerList(wxWindow* parent, wxWindowID id) - : wxListView(parent, id, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL) -{ - AddColumns(); - - // tooltip TODO: extract class mb wxPanelTooltip - m_tooltip_window = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); - auto* tooltip_sizer = new wxBoxSizer(wxVERTICAL); - m_tooltip_text = new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString); - tooltip_sizer->Add(m_tooltip_text , 0, wxALL, 5); - m_tooltip_window->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); - m_tooltip_window->SetSizerAndFit(tooltip_sizer); - m_tooltip_window->Hide(); - m_tooltip_timer = new wxTimer(this); - - Bind(wxEVT_LIST_COL_CLICK, &wxDownloadManagerList::OnColumnClick, this); - Bind(wxEVT_CONTEXT_MENU, &wxDownloadManagerList::OnContextMenu, this); - Bind(wxEVT_LIST_ITEM_SELECTED, &wxDownloadManagerList::OnItemSelected, this); - Bind(wxEVT_TIMER, &wxDownloadManagerList::OnTimer, this); - Bind(wxEVT_REMOVE_ITEM, &wxDownloadManagerList::OnRemoveItem, this); - Bind(wxEVT_REMOVE_ENTRY, &wxDownloadManagerList::OnRemoveEntry, this); - Bind(wxEVT_CLOSE_WINDOW, &wxDownloadManagerList::OnClose, this); - - ShowSortIndicator(ColumnName); -} - -boost::optional wxDownloadManagerList::GetSelectedTitleEntry() const -{ - const auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - const auto tmp = GetTitleEntry(selection); - if (tmp.has_value()) - return tmp.value(); - } - - return {}; -} - -boost::optional wxDownloadManagerList::GetSelectedTitleEntry() -{ - const auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - const auto tmp = GetTitleEntry(selection); - if (tmp.has_value()) - return tmp.value(); - } - - return {}; -} - -boost::optional wxDownloadManagerList::GetTitleEntry(uint64 titleId, uint16 titleVersion) -{ - for(const auto& v : m_data) - { - if (v->entry.titleId == titleId && v->entry.version == titleVersion) - return v->entry; - } - - return {}; -} - -void wxDownloadManagerList::AddColumns() -{ - wxListItem col0; - col0.SetId(ColumnTitleId); - col0.SetText(_("Title ID")); - col0.SetWidth(120); - InsertColumn(ColumnTitleId, col0); - - wxListItem col1; - col1.SetId(ColumnName); - col1.SetText(_("Name")); - col1.SetWidth(260); - InsertColumn(ColumnName, col1); - - wxListItem col2; - col2.SetId(ColumnVersion); - col2.SetText(_("Version")); - col2.SetWidth(55); - InsertColumn(ColumnVersion, col2); - - wxListItem col3; - col3.SetId(ColumnType); - col3.SetText(_("Type")); - col3.SetWidth(60); - InsertColumn(ColumnType, col3); - - wxListItem col4; - col4.SetId(ColumnProgress); - col4.SetText(_("Progress")); - col4.SetWidth(wxLIST_AUTOSIZE_USEHEADER); - InsertColumn(ColumnProgress, col4); - - wxListItem col5; - col5.SetId(ColumnStatus); - col5.SetText(_("Status")); - col5.SetWidth(240); - InsertColumn(ColumnStatus, col5); -} - -wxString wxDownloadManagerList::OnGetItemText(long item, long column) const -{ - if (item >= GetItemCount()) - return wxEmptyString; - - const auto entry = GetTitleEntry(item); - if (entry.has_value()) - return GetTitleEntryText(entry.value(), (ItemColumn)column); - - return wxEmptyString; -} - -wxItemAttr* wxDownloadManagerList::OnGetItemAttr(long item) const -{ - const auto entry = GetTitleEntry(item); - - const wxColour bgColour = GetBackgroundColour(); - const bool isDarkTheme = wxSystemSettings::GetAppearance().IsDark(); - - if (entry.has_value()) - { - auto& entryData = entry.value(); - if (entryData.status == TitleDownloadStatus::Downloading || - entryData.status == TitleDownloadStatus::Verifying || - entryData.status == TitleDownloadStatus::Installing) - { - const wxColour kActiveColor = isDarkTheme ? wxColour(80, 40, 40) : wxColour(0xFFE0E0); - static wxListItemAttr s_active_attr; - s_active_attr.SetBackgroundColour(kActiveColor); - s_active_attr.SetTextColour(GetTextColour()); - s_active_attr.SetFont(GetFont()); - return &s_active_attr; - } - else if (entryData.status == TitleDownloadStatus::Installed && entryData.isPackage) - { - const wxColour kActiveColor = isDarkTheme ? wxColour(40, 80, 40) : wxColour(0xE0FFE0); - static wxListItemAttr s_installed_attr; - s_installed_attr.SetBackgroundColour(kActiveColor); - s_installed_attr.SetTextColour(GetTextColour()); - s_installed_attr.SetFont(GetFont()); - return &s_installed_attr; - } - else if (entryData.status == TitleDownloadStatus::Error) - { - const wxColour kActiveColor = isDarkTheme ? wxColour(40, 40, 80) : wxColour(0xCCCCF2); - static wxListItemAttr s_error_attr; - s_error_attr.SetBackgroundColour(kActiveColor); - s_error_attr.SetTextColour(GetTextColour()); - s_error_attr.SetFont(GetFont()); - return &s_error_attr; - } - } - - wxColour bgColourSecondary = wxHelper::CalculateAccentColour(bgColour); - static wxListItemAttr s_coloured_attr; - s_coloured_attr.SetBackgroundColour(bgColourSecondary); - s_coloured_attr.SetTextColour(GetTextColour()); - s_coloured_attr.SetFont(GetFont()); - return item % 2 == 0 ? nullptr : &s_coloured_attr; -} - -boost::optional wxDownloadManagerList::GetTitleEntry(long item) -{ - long counter = 0; - for (const auto& data : m_sorted_data) - { - if (!data.get().visible) - continue; - - if (item != counter++) - continue; - - return data.get().entry; - } - - return {}; -} - -boost::optional wxDownloadManagerList::GetTitleEntry(long item) const -{ - long counter = 0; - for (const auto& data : m_sorted_data) - { - if (!data.get().visible) - continue; - - if (item != counter++) - continue; - - return data.get().entry; - } - - return {}; -} - -void wxDownloadManagerList::OnClose(wxCloseEvent& event) -{ - event.Skip(); - // wait until all tasks are complete - if (m_context_worker.valid()) - m_context_worker.get(); - g_mainFrame->RequestGameListRefresh(); // todo: add games instead of fully refreshing game list if a game is downloaded -} - -void wxDownloadManagerList::OnColumnClick(wxListEvent& event) -{ - const int column = event.GetColumn(); - - SortEntries(column); - event.Skip(); -} - -void wxDownloadManagerList::RemoveItem(long item) -{ - const int item_count = GetItemCount(); - - const ItemData* ref = nullptr; - long counter = 0; - for(auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) - { - if (!it->get().visible) - continue; - - if (item != counter++) - continue; - - ref = &(it->get()); - m_sorted_data.erase(it); - break; - } - - // shouldn't happen - if (ref == nullptr) - return; - - for(auto it = m_data.begin(); it != m_data.end(); ++it) - { - if (ref != (*it).get()) - continue; - - m_data.erase(it); - break; - } - - SetItemCount(std::max(0, item_count - 1)); - RefreshPage(); -} - -void wxDownloadManagerList::RemoveItem(const TitleEntry& entry) -{ - const int item_count = GetItemCount(); - - const TitleEntry* ref = &entry; - for (auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) - { - if (ref != &it->get().entry) - continue; - - m_sorted_data.erase(it); - break; - } - - for (auto it = m_data.begin(); it != m_data.end(); ++it) - { - if (ref != &(*it).get()->entry) - continue; - - m_data.erase(it); - break; - } - - SetItemCount(std::max(0, item_count - 1)); - RefreshPage(); -} - -void wxDownloadManagerList::OnItemSelected(wxListEvent& event) -{ - event.Skip(); - m_tooltip_timer->Stop(); - const auto selection = event.GetIndex(); - - if (selection == wxNOT_FOUND) - { - m_tooltip_window->Hide(); - return; - } - -} - -enum ContextMenuEntries -{ - kContextMenuRetry = wxID_HIGHEST + 1, - kContextMenuDownload, - kContextMenuPause, - kContextMenuResume, -}; -void wxDownloadManagerList::OnContextMenu(wxContextMenuEvent& event) -{ - // still doing work - if (m_context_worker.valid() && !future_is_ready(m_context_worker)) - return; - - wxMenu menu; - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &wxDownloadManagerList::OnContextMenuSelected, this); - - const auto selection = GetFirstSelected(); - if (selection == wxNOT_FOUND) - return; - - const auto entry = GetTitleEntry(selection); - if (!entry.has_value()) - return; - - if (entry->isPaused) - menu.Append(kContextMenuResume, _("&Resume")); - else if (entry->status == TitleDownloadStatus::Error) - menu.Append(kContextMenuRetry, _("&Retry")); - else if(entry->status == TitleDownloadStatus::Available) - menu.Append(kContextMenuDownload, _("&Download")); - //else if(entry->status == TitleDownloadStatus::Downloading || entry->status == TitleDownloadStatus::Initializing) - // menu.Append(kContextMenuPause, _("&Pause")); buggy! - - PopupMenu(&menu); - - // TODO: fix tooltip position -} - -void wxDownloadManagerList::SetCurrentDownloadMgr(DownloadManager* dlMgr) -{ - std::unique_lock _l(m_dlMgrMutex); - m_dlMgr = dlMgr; -} - -bool wxDownloadManagerList::StartDownloadEntry(const TitleEntry& entry) -{ - std::unique_lock _l(m_dlMgrMutex); - if (m_dlMgr) - m_dlMgr->initiateDownload(entry.titleId, entry.version); - return true; -} - -bool wxDownloadManagerList::RetryDownloadEntry(const TitleEntry& entry) -{ - StartDownloadEntry(entry); - return true; -} - -bool wxDownloadManagerList::PauseDownloadEntry(const TitleEntry& entry) -{ - std::unique_lock _l(m_dlMgrMutex); - if (m_dlMgr) - m_dlMgr->pauseDownload(entry.titleId, entry.version); - return true; -} - -void wxDownloadManagerList::OnContextMenuSelected(wxCommandEvent& event) -{ - // still doing work - if (m_context_worker.valid() && !future_is_ready(m_context_worker)) - return; - - const auto selection = GetFirstSelected(); - if (selection == wxNOT_FOUND) - return; - - const auto entry = GetTitleEntry(selection); - if (!entry.has_value()) - return; - - switch (event.GetId()) - { - case kContextMenuDownload: - StartDownloadEntry(entry.value()); - break; - case kContextMenuRetry: - RetryDownloadEntry(entry.value()); - break; - case kContextMenuResume: - StartDownloadEntry(entry.value()); - break; - case kContextMenuPause: - PauseDownloadEntry(entry.value()); - break; - } -} - -void wxDownloadManagerList::OnTimer(wxTimerEvent& event) -{ - if(event.GetTimer().GetId() != m_tooltip_timer->GetId()) - { - event.Skip(); - return; - } - - m_tooltip_window->Show(); -} - -void wxDownloadManagerList::OnRemoveItem(wxCommandEvent& event) -{ - RemoveItem(event.GetInt()); -} - -void wxDownloadManagerList::OnRemoveEntry(wxCommandEvent& event) -{ - wxASSERT(event.GetClientData() != nullptr); - RemoveItem(*(TitleEntry*)event.GetClientData()); -} - -wxString wxDownloadManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColumn column) -{ - switch (column) - { - case ColumnTitleId: - return formatWxString("{:08x}-{:08x}", (uint32) (entry.titleId >> 32), (uint32) (entry.titleId & 0xFFFFFFFF)); - case ColumnName: - return entry.name; - case ColumnType: - return GetTranslatedTitleEntryType(entry.type); - case ColumnVersion: - { - // dont show version for base game unless it is not v0 - if (entry.type == EntryType::Base && entry.version == 0) - return ""; - if (entry.type == EntryType::DLC && entry.version == 0) - return ""; - return formatWxString("v{}", entry.version); - } - case ColumnProgress: - { - if (entry.status == TitleDownloadStatus::Downloading) - { - if (entry.progress >= 1000) - return "100%"; - return formatWxString("{:.1f}%", (float) entry.progress / 10.0f); // one decimal - } - else if (entry.status == TitleDownloadStatus::Installing || entry.status == TitleDownloadStatus::Checking || entry.status == TitleDownloadStatus::Verifying) - { - return formatWxString("{0}/{1}", entry.progress, entry.progressMax); // number of processed files/content files - } - return ""; - } - case ColumnStatus: - { - if (entry.isPaused) - return _("Paused"); - else if (entry.status == TitleDownloadStatus::Available) - { - if (entry.progress == 1) - return _("Not installed (Partially downloaded)"); - if (entry.progress == 2) - return _("Update available"); - return _("Not installed"); - } - else if (entry.status == TitleDownloadStatus::Initializing) - return _("Initializing"); - else if (entry.status == TitleDownloadStatus::Checking) - return _("Checking"); - else if (entry.status == TitleDownloadStatus::Queued) - return _("Queued"); - else if (entry.status == TitleDownloadStatus::Downloading) - return _("Downloading"); - else if (entry.status == TitleDownloadStatus::Verifying) - return _("Verifying"); - else if (entry.status == TitleDownloadStatus::Installing) - return _("Installing"); - else if (entry.status == TitleDownloadStatus::Installed) - return _("Installed"); - else if (entry.status == TitleDownloadStatus::Error) - { - wxString errorStatusMsg = _("Error:"); - errorStatusMsg.append(" "); - errorStatusMsg.append(entry.errorMsg); - return errorStatusMsg; - } - else - return "Unknown status"; - } - } - - return wxEmptyString; -} - -wxString wxDownloadManagerList::GetTranslatedTitleEntryType(EntryType type) -{ - switch (type) - { - case EntryType::Base: - return _("base"); - case EntryType::Update: - return _("update"); - case EntryType::DLC: - return _("DLC"); - default: - return std::to_string(static_cast>(type)); - } -} - -void wxDownloadManagerList::AddOrUpdateTitle(TitleEntryData_t* obj) -{ - const auto& data = obj->GetData(); - // if already in list only update - auto entry = GetTitleEntry(data.titleId, data.version); - if (entry.has_value()) - { - // update item - entry.value() = data; - RefreshPage(); // more efficient way to do this? - return; - } - - m_data.emplace_back(std::make_unique(true, data)); - m_sorted_data.emplace_back(*m_data[m_data.size() - 1]); - SetItemCount(m_data.size()); - - // reapply sort - Filter2(m_filterShowTitles, m_filterShowUpdates, m_filterShowInstalled); - SortEntries(); -} - -void wxDownloadManagerList::UpdateTitleStatusDepr(TitleEntryData_t* obj, const wxString& text) -{ - const auto& data = obj->GetData(); - const auto entry = GetTitleEntry(data.titleId, data.version); - // check if already added to list - if (!entry.has_value()) - return; - - // update gamelist text - for(size_t i = 0; i < m_sorted_data.size(); ++i) - { - if (m_sorted_data[i].get().entry == data) - { - SetItem(i, ColumnStatus, text); - return; - } - } - - cemuLog_logDebug(LogType::Force, "cant update title status of {:x}", data.titleId); -} - -int wxDownloadManagerList::AddImage(const wxImage& image) const -{ - return -1; // m_image_list->Add(image.Scale(kListIconWidth, kListIconWidth, wxIMAGE_QUALITY_BICUBIC)); -} - -// return < -bool wxDownloadManagerList::SortFunc(std::span sortColumnOrder, const Type_t& v1, const Type_t& v2) -{ - cemu_assert_debug(sortColumnOrder.size() == 5); - - // visible have always priority - if (!v1.get().visible && v2.get().visible) - return false; - else if (v1.get().visible && !v2.get().visible) - return true; - - const auto& entry1 = v1.get().entry; - const auto& entry2 = v2.get().entry; - - for (size_t i = 0; i < sortColumnOrder.size(); i++) - { - int sortByColumn = sortColumnOrder[i]; - if (sortByColumn == ColumnTitleId) - { - // ensure strong ordering -> use type since only one entry should be now (should be changed if every save for every user is displayed spearately?) - if (entry1.titleId != entry2.titleId) - return entry1.titleId < entry2.titleId; - } - else if (sortByColumn == ColumnName) - { - const int tmp = entry1.name.CmpNoCase(entry2.name); - if (tmp != 0) - return tmp < 0; - } - else if (sortByColumn == ColumnType) - { - if (std::underlying_type_t(entry1.type) != std::underlying_type_t(entry2.type)) - return std::underlying_type_t(entry1.type) < std::underlying_type_t(entry2.type); - } - else if (sortByColumn == ColumnVersion) - { - if (entry1.version != entry2.version) - return entry1.version < entry2.version; - } - else if (sortByColumn == ColumnProgress) - { - if (entry1.progress != entry2.progress) - return entry1.progress < entry2.progress; - } - else - { - cemu_assert_debug(false); - return (uintptr_t)&entry1 < (uintptr_t)&entry2; - } - - } - - return false; -} - -#include - -void wxDownloadManagerList::SortEntries(int column) -{ - boost::container::small_vector s_SortColumnOrder{ ColumnName, ColumnType, ColumnVersion, ColumnTitleId, ColumnProgress }; - - bool ascending; - if (column == -1) - { - column = GetSortIndicator(); - if (column == -1) - column = ColumnName; - ascending = IsAscendingSortIndicator(); - } - else - ascending = GetUpdatedAscendingSortIndicator(column); - - // prioritize column by moving it to first position in the column sort order list - s_SortColumnOrder.erase(std::remove(s_SortColumnOrder.begin(), s_SortColumnOrder.end(), column), s_SortColumnOrder.end()); - s_SortColumnOrder.insert(s_SortColumnOrder.begin(), column); - - std::sort(m_sorted_data.begin(), m_sorted_data.end(), - [this, &s_SortColumnOrder, ascending](const Type_t& v1, const Type_t& v2) -> bool { - return ascending ? SortFunc(s_SortColumnOrder, v1, v2) : SortFunc(s_SortColumnOrder, v2, v1); - }); - - ShowSortIndicator(column, ascending); - RefreshPage(); -} - -void wxDownloadManagerList::RefreshPage() -{ - long item_count = GetItemCount(); - - if (item_count > 0) - RefreshItems(GetTopItem(), std::min(item_count - 1, GetTopItem() + GetCountPerPage() + 1)); -} - -int wxDownloadManagerList::Filter(const wxString& filter, const wxString& prefix, ItemColumn column) -{ - if (prefix.empty()) - return -1; - - if (!filter.StartsWith(prefix)) - return -1; - - int counter = 0; - const auto tmp_filter = filter.substr(prefix.size() - 1).Trim(false); - for (auto&& data : m_data) - { - if (GetTitleEntryText(data->entry, column).Upper().Contains(tmp_filter)) - { - data->visible = true; - ++counter; - } - else - data->visible = false; - } - return counter; -} - -void wxDownloadManagerList::Filter(const wxString& filter) -{ - if(filter.empty()) - { - std::for_each(m_data.begin(), m_data.end(), [](ItemDataPtr& data) { data->visible = true; }); - SetItemCount(m_data.size()); - RefreshPage(); - return; - } - - const auto filter_upper = filter.Upper().Trim(false).Trim(true); - int counter = 0; - - if (const auto result = Filter(filter_upper, "TITLEID:", ColumnTitleId) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "NAME:", ColumnName) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "TYPE:", ColumnType) != -1) - counter = result; - //else if (const auto result = Filter(filter_upper, "REGION:", ColumnRegion) != -1) - // counter = result; - else if (const auto result = Filter(filter_upper, "VERSION:", ColumnVersion) != -1) - counter = result; - else if(filter_upper == "ERROR") - { - for (auto&& data : m_data) - { - bool visible = false; - //if (data->entry.error != TitleError::None) - // visible = true; - - data->visible = visible; - if (visible) - ++counter; - } - } - else - { - for (auto&& data : m_data) - { - bool visible = false; - if (data->entry.name.Upper().Contains(filter_upper)) - visible = true; - else if (GetTitleEntryText(data->entry, ColumnTitleId).Upper().Contains(filter_upper)) - visible = true; - else if (GetTitleEntryText(data->entry, ColumnType).Upper().Contains(filter_upper)) - visible = true; - - data->visible = visible; - if (visible) - ++counter; - } - } - - SetItemCount(counter); - RefreshPage(); -} - -void wxDownloadManagerList::Filter2(bool showTitles, bool showUpdates, bool showInstalled) -{ - m_filterShowTitles = showTitles; - m_filterShowUpdates = showUpdates; - m_filterShowInstalled = showInstalled; - if (showTitles && showUpdates && showInstalled) - { - std::for_each(m_data.begin(), m_data.end(), [](ItemDataPtr& data) { data->visible = true; }); - SetItemCount(m_data.size()); - RefreshPage(); - return; - } - - size_t counter = 0; - - for (auto&& data : m_data) - { - bool visible = false; - - TitleIdParser tParser(data->entry.titleId); - bool isInstalled = data->entry.status == TitleDownloadStatus::Installed; - if (tParser.IsBaseTitleUpdate()) - { - if (showUpdates && (showInstalled || !isInstalled)) - visible = true; - } - else - { - if (showTitles && (showInstalled || !isInstalled)) - visible = true; - } - - data->visible = visible; - if (visible) - ++counter; - } - - SetItemCount(counter); - RefreshPage(); -} - -size_t wxDownloadManagerList::GetCountByType(EntryType type) const -{ - size_t result = 0; - for(const auto& data : m_data) - { - if (data->entry.type == type) - ++result; - } - return result; -} - -void wxDownloadManagerList::ClearItems() -{ - m_sorted_data.clear(); - m_data.clear(); - SetItemCount(0); - RefreshPage(); -} - -void wxDownloadManagerList::AutosizeColumns() -{ - wxAutosizeColumns(this, ColumnTitleId, ColumnMAX - 1); -} - diff --git a/src/gui/wxgui/components/wxDownloadManagerList.h b/src/gui/wxgui/components/wxDownloadManagerList.h deleted file mode 100644 index e3988e13..00000000 --- a/src/gui/wxgui/components/wxDownloadManagerList.h +++ /dev/null @@ -1,152 +0,0 @@ -#pragma once - -#include "wxgui/helpers/wxCustomData.h" -#include "config/CemuConfig.h" - -#include - -#include // std::optional doesn't support optional reference inner types yet -#include -#include - -class wxDownloadManagerList : public wxListView -{ - friend class TitleManager; -public: - wxDownloadManagerList(wxWindow* parent, wxWindowID id = wxID_ANY); - - enum ItemColumn - { - ColumnTitleId = 0, - ColumnName, - ColumnVersion, - ColumnType, - ColumnProgress, - ColumnStatus, - - ColumnMAX, - }; - - enum class EntryType - { - Base, - Update, - DLC, - }; - - enum class TitleDownloadStatus - { - None, - Available, // available for download - Error, // error state - Queued, // queued for download - Initializing, // downloading/parsing TMD - Checking, // checking for previously downloaded files - Downloading, - Verifying, // verifying downloaded files - Installing, - Installed, - // error state? - }; - - void SortEntries(int column = -1); - void RefreshPage(); - void Filter(const wxString& filter); - void Filter2(bool showTitles, bool showUpdates, bool showInstalled); - [[nodiscard]] size_t GetCountByType(EntryType type) const; - void ClearItems(); - void AutosizeColumns(); - - struct TitleEntry - { - TitleEntry(const EntryType& type, bool isPackage, uint64 titleId, uint16 version, bool isPaused) - : type(type), isPackage(isPackage), titleId(titleId), version(version), isPaused(isPaused) {} - - EntryType type; - - bool isPaused{}; - int icon = -1; - bool isPackage; - uint64 titleId; - wxString name; - uint32_t version{ 0 }; - uint32 progress; // downloading: in 1/10th of a percent, installing: number of files - uint32 progressMax{ 0 }; - CafeConsoleRegion region; - - TitleDownloadStatus status = TitleDownloadStatus::None; - std::string errorMsg; - - bool operator==(const TitleEntry& e) const - { - return type == e.type && titleId == e.titleId && version == e.version; - } - bool operator!=(const TitleEntry& e) const { return !(*this == e); } - }; - boost::optional GetSelectedTitleEntry() const; - -private: - void AddColumns(); - int Filter(const wxString& filter, const wxString& prefix, ItemColumn column); - boost::optional GetSelectedTitleEntry(); - boost::optional GetTitleEntry(uint64 titleId, uint16 titleVersion); - - class wxPanel* m_tooltip_window; - class wxStaticText* m_tooltip_text; - class wxTimer* m_tooltip_timer; - - void OnClose(wxCloseEvent& event); - void OnColumnClick(wxListEvent& event); - void OnContextMenu(wxContextMenuEvent& event); - void OnItemSelected(wxListEvent& event); - void OnContextMenuSelected(wxCommandEvent& event); - void OnTimer(class wxTimerEvent& event); - void OnRemoveItem(wxCommandEvent& event); - void OnRemoveEntry(wxCommandEvent& event); - - using TitleEntryData_t = wxCustomData; - void AddOrUpdateTitle(TitleEntryData_t* obj); - void UpdateTitleStatusDepr(TitleEntryData_t* obj, const wxString& text); - int AddImage(const wxImage& image) const; - wxString OnGetItemText(long item, long column) const override; - wxItemAttr* OnGetItemAttr(long item) const override; - [[nodiscard]] boost::optional GetTitleEntry(long item) const; - [[nodiscard]] boost::optional GetTitleEntry(long item); - //[[nodiscard]] boost::optional GetTitleEntry(const fs::path& path) const; - //[[nodiscard]] boost::optional GetTitleEntry(const fs::path& path); - - void SetCurrentDownloadMgr(class DownloadManager* dlMgr); - - //bool FixEntry(TitleEntry& entry); - //bool VerifyEntryFiles(TitleEntry& entry); - bool StartDownloadEntry(const TitleEntry& entry); - bool RetryDownloadEntry(const TitleEntry& entry); - bool PauseDownloadEntry(const TitleEntry& entry); - - void RemoveItem(long item); - void RemoveItem(const TitleEntry& entry); - - struct ItemData - { - ItemData(bool visible, const TitleEntry& entry) - : visible(visible), entry(entry) {} - - bool visible; - TitleEntry entry; - }; - using ItemDataPtr = std::unique_ptr; - std::vector m_data; - std::vector> m_sorted_data; - - bool m_filterShowTitles = true; - bool m_filterShowUpdates = true; - bool m_filterShowInstalled = true; - DownloadManager* m_dlMgr{}; - std::mutex m_dlMgrMutex; - using Type_t = std::reference_wrapper; - bool SortFunc(std::span sortColumnOrder, const Type_t& v1, const Type_t& v2); - - static wxString GetTitleEntryText(const TitleEntry& entry, ItemColumn column); - static wxString GetTranslatedTitleEntryType(EntryType entryType); - std::future m_context_worker; -}; diff --git a/src/gui/wxgui/components/wxGameList.cpp b/src/gui/wxgui/components/wxGameList.cpp deleted file mode 100644 index 68116026..00000000 --- a/src/gui/wxgui/components/wxGameList.cpp +++ /dev/null @@ -1,1696 +0,0 @@ -#include "wxgui/components/wxGameList.h" - -#include "wxgui/helpers/wxCustomData.h" -#include "wxCemuConfig.h" -#include "util/helpers/helpers.h" -#include "wxgui/GameProfileWindow.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "config/ActiveSettings.h" -#include "config/LaunchSettings.h" -#include "Cafe/TitleList/GameInfo.h" -#include "Cafe/TitleList/TitleList.h" - -#include "wxgui/CemuApp.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/MainWindow.h" - -#include "../wxHelper.h" - -#include "Cafe/IOSU/PDM/iosu_pdm.h" // for last played and play time - -#if BOOST_OS_WINDOWS -// for shortcut creation -#include -#include -#include -#include -#include -#include -#include -#include -#endif - -// public events -wxDEFINE_EVENT(wxEVT_OPEN_SETTINGS, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_GAMELIST_BEGIN_UPDATE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_GAMELIST_END_UPDATE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_OPEN_GRAPHIC_PACK, wxTitleIdEvent); - -// internal events -wxDEFINE_EVENT(wxEVT_GAME_ENTRY_ADDED_OR_REMOVED, wxTitleIdEvent); - -constexpr uint64 kDefaultEntryData = 0x1337; - -void _stripPathFilename(fs::path& path) -{ - if (path.has_extension()) - path = path.parent_path(); -} - -std::vector _getCachesPaths(const TitleId& titleId) -{ - std::vector cachePaths{ - ActiveSettings::GetCachePath(L"shaderCache/driver/vk/{:016x}.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/precompiled/{:016x}_spirv.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/precompiled/{:016x}_gl.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/precompiled/{:016x}_air.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/transferable/{:016x}_shaders.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/transferable/{:016x}_mtlshaders.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/transferable/{:016x}_vkpipeline.bin", titleId), - ActiveSettings::GetCachePath(L"shaderCache/transferable/{:016x}_mtlpipeline.bin", titleId)}; - - cachePaths.erase(std::remove_if(cachePaths.begin(), cachePaths.end(), - [](const fs::path& cachePath) { - std::error_code ec; - return !fs::exists(cachePath, ec); - }), - cachePaths.end()); - - return cachePaths; -} - -// Convert PNG to Apple icon image format -bool writeICNS(const fs::path& pngPath, const fs::path& icnsPath) { - // Read PNG file - std::ifstream pngFile(pngPath, std::ios::binary); - if (!pngFile) - return false; - - // Get PNG size - pngFile.seekg(0, std::ios::end); - uint32 pngSize = static_cast(pngFile.tellg()); - pngFile.seekg(0, std::ios::beg); - - // Calculate total file size (header + size + type + data) - uint32 totalSize = 8 + 8 + pngSize; - - // Create output file - std::ofstream icnsFile(icnsPath, std::ios::binary); - if (!icnsFile) - return false; - - // Write ICNS header - icnsFile.put(0x69); // 'i' - icnsFile.put(0x63); // 'c' - icnsFile.put(0x6e); // 'n' - icnsFile.put(0x73); // 's' - - // Write total file size (big endian) - icnsFile.put((totalSize >> 24) & 0xFF); - icnsFile.put((totalSize >> 16) & 0xFF); - icnsFile.put((totalSize >> 8) & 0xFF); - icnsFile.put(totalSize & 0xFF); - - // Write icon type (ic07 = 128x128 PNG) - icnsFile.put(0x69); // 'i' - icnsFile.put(0x63); // 'c' - icnsFile.put(0x30); // '0' - icnsFile.put(0x37); // '7' - - // Write PNG size (big endian) - icnsFile.put((pngSize >> 24) & 0xFF); - icnsFile.put((pngSize >> 16) & 0xFF); - icnsFile.put((pngSize >> 8) & 0xFF); - icnsFile.put(pngSize & 0xFF); - - // Copy PNG data - icnsFile << pngFile.rdbuf(); - - return true; -} - -wxGameList::wxGameList(wxWindow* parent, wxWindowID id) - : wxListView(parent, id, wxDefaultPosition, wxDefaultSize, GetStyleFlags(Style::kList)), m_style(Style::kList) -{ - const auto& config = GetWxGUIConfig(); - - char transparent_bitmap[kIconWidth * kIconWidth * 4] = {}; - memset(transparent_bitmap, wxSystemSettings::GetAppearance().IsDark() ? 0xFF : 0x00, sizeof(transparent_bitmap)); - wxBitmap blank(transparent_bitmap, kIconWidth, kIconWidth); - - m_image_list_data.Add(blank); - wxListCtrl::SetImageList(&m_image_list_data, wxIMAGE_LIST_NORMAL); - - wxBitmap::Rescale(blank, {kListIconWidth, kListIconWidth}); - m_image_list_small_data.Add(blank); - wxListCtrl::SetImageList(&m_image_list_small_data, wxIMAGE_LIST_SMALL); - - InsertColumn(ColumnHiddenName, "", wxLIST_FORMAT_LEFT, 0); - if(config.show_icon_column) - InsertColumn(ColumnIcon, _("Icon"), wxLIST_FORMAT_LEFT, GetColumnDefaultWidth(ColumnIcon)); - else - InsertColumn(ColumnIcon, _("Icon"), wxLIST_FORMAT_LEFT, 0); - InsertColumn(ColumnName, _("Game"), wxLIST_FORMAT_LEFT, config.column_width.name); - InsertColumn(ColumnVersion, _("Version"), wxLIST_FORMAT_LEFT, config.column_width.version); - InsertColumn(ColumnDLC, _("DLC"), wxLIST_FORMAT_LEFT, config.column_width.dlc); - InsertColumn(ColumnGameTime, _("You've played"), wxLIST_FORMAT_LEFT, config.column_width.game_time); - InsertColumn(ColumnGameStarted, _("Last played"), wxLIST_FORMAT_LEFT, config.column_width.game_started); - InsertColumn(ColumnRegion, _("Region"), wxLIST_FORMAT_LEFT, config.column_width.region); - InsertColumn(ColumnTitleID, _("Title ID"), wxLIST_FORMAT_LEFT, config.column_width.title_id); - - m_tooltip_window = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); - auto* tooltip_sizer = new wxBoxSizer(wxVERTICAL); - tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, _("This game entry seems to be either an update or the base game was merged with update data\nBroken game dumps cause various problems during emulation and may even stop working at all in future Cemu versions\nPlease make sure the base game is intact and install updates only with the File->Install Update/DLC option")), 0, wxALL, 5); - m_tooltip_window->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); - m_tooltip_window->SetSizerAndFit(tooltip_sizer); - m_tooltip_window->Hide(); - - m_tooltip_timer = new wxTimer(this); - - Bind(wxEVT_CLOSE_WINDOW, &wxGameList::OnClose, this); - Bind(wxEVT_MOTION, &wxGameList::OnMouseMove, this); - Bind(wxEVT_LIST_KEY_DOWN, &wxGameList::OnKeyDown, this); - Bind(wxEVT_CONTEXT_MENU, &wxGameList::OnContextMenu, this); - Bind(wxEVT_LIST_ITEM_ACTIVATED, &wxGameList::OnItemActivated, this); - Bind(wxEVT_GAME_ENTRY_ADDED_OR_REMOVED, &wxGameList::OnGameEntryUpdatedByTitleId, this); - Bind(wxEVT_TIMER, &wxGameList::OnTimer, this); - Bind(wxEVT_LEAVE_WINDOW, &wxGameList::OnLeaveWindow, this); - - Bind(wxEVT_LIST_COL_CLICK, &wxGameList::OnColumnClick, this); - Bind(wxEVT_LIST_COL_BEGIN_DRAG, &wxGameList::OnColumnBeginResize, this); - Bind(wxEVT_LIST_COL_END_DRAG, &wxGameList::OnColumnResize, this); - Bind(wxEVT_LIST_COL_RIGHT_CLICK, &wxGameList::OnColumnRightClick, this); - Bind(wxEVT_SIZE, &wxGameList::OnGameListSize, this); - m_bulkUpdateTimer.Bind(wxEVT_TIMER, &wxGameList::OnTimerBulkAddEntriesToGameList, this); - - m_callbackIdTitleList = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { ((wxGameList*)ctx)->HandleTitleListCallback(evt); }, this); - - // start async worker (for icon loading) - m_async_worker_active = true; - m_async_worker_thread = std::thread(&wxGameList::AsyncWorkerThread, this); - - ShowSortIndicator(ColumnName); -} - -wxGameList::~wxGameList() -{ - CafeTitleList::UnregisterCallback(m_callbackIdTitleList); - - m_tooltip_timer->Stop(); - - // shut down async worker - m_async_worker_active.store(false); - m_async_task_count.increment(); - m_async_worker_thread.join(); - - // clear image cache - m_icon_cache_mtx.lock(); - m_icon_cache.clear(); - m_icon_cache_mtx.unlock(); -} - -void wxGameList::LoadConfig() -{ - const auto& config = GetWxGUIConfig(); - SetStyle((Style)config.game_list_style, false); - - if (!config.game_list_column_order.empty()) - { - wxArrayInt order; - order.reserve(ColumnCounts); - - const auto order_string = std::string_view(config.game_list_column_order).substr(1); - - const boost::char_separator sep(","); - boost::tokenizer tokens(order_string.begin(), order_string.end(), sep); - for(const auto& token : tokens) - { - order.push_back(ConvertString(token, 10)); - } - -#ifdef wxHAS_LISTCTRL_COLUMN_ORDER - if(order.GetCount() == ColumnCounts) - SetColumnsOrder(order); -#endif - } -} - -void wxGameList::OnGameListSize(wxSizeEvent &event) -{ - event.Skip(); - - // when using a sizer-based layout, do not change the size of the wxComponent in its own wxSizeEvent handler to avoid some UI issues. - int last_col_index = 0; - for(int i = GetColumnCount() - 1; i > 0; i--) - { -#ifdef wxHAS_LISTCTRL_COLUMN_ORDER - if(GetColumnWidth(GetColumnIndexFromOrder(i)) > 0) - { - last_col_index = GetColumnIndexFromOrder(i); - break; - } -#else - if(GetColumnWidth(i) > 0) - { - last_col_index = i; - break; - } -#endif - } - wxListEvent column_resize_event(wxEVT_LIST_COL_END_DRAG); - column_resize_event.SetColumn(last_col_index); - wxPostEvent(this, column_resize_event); -} - -void wxGameList::AdjustLastColumnWidth() -{ - wxWindowUpdateLocker windowlock(this); - int last_col_index = 0; - int last_col_width = GetClientSize().GetWidth(); - for (int i = 1; i < GetColumnCount(); i++) - { -#ifdef wxHAS_LISTCTRL_COLUMN_ORDER - if (GetColumnWidth(GetColumnIndexFromOrder(i)) > 0) - { - last_col_index = GetColumnIndexFromOrder(i); - last_col_width -= GetColumnWidth(last_col_index); - } -#else - if (GetColumnWidth(i) > 0) - { - last_col_index = i; - last_col_width -= GetColumnWidth(i); - } -#endif - } - last_col_width += GetColumnWidth(last_col_index); - if (last_col_width < GetColumnDefaultWidth(last_col_index)) // keep a minimum width - last_col_width = GetColumnDefaultWidth(last_col_index); - SetColumnWidth(last_col_index, last_col_width); -} - -// todo: scale all columns using a ratio instead of hardcoding exact widths -int wxGameList::GetColumnDefaultWidth(int column) -{ - switch (column) - { - case ColumnIcon: - return kListIconWidth+2; - case ColumnName: - return DefaultColumnSize::name; - case ColumnVersion: - return DefaultColumnSize::version; - case ColumnDLC: - return DefaultColumnSize::dlc; - case ColumnGameTime: - return DefaultColumnSize::game_time; - case ColumnGameStarted: - return DefaultColumnSize::game_started; - case ColumnRegion: - return DefaultColumnSize::region; - case ColumnTitleID: - return DefaultColumnSize::title_id; - default: - return 80; - } -} - -void wxGameList::SaveConfig(bool flush) -{ - auto& config = GetWxGUIConfig(); - - config.game_list_style = (int)m_style; - #ifdef wxHAS_LISTCTRL_COLUMN_ORDER - config.game_list_column_order = fmt::format("{}", GetColumnsOrder()); - #endif - - if (flush) - GetConfigHandle().Save(); -} - -bool wxGameList::IsVisible(long item) const -{ - wxRect itemRect; - GetItemRect(item, itemRect); - const wxRect clientRect = GetClientRect(); - bool visible = clientRect.Intersects(itemRect); - return visible; -} - -void wxGameList::ReloadGameEntries() -{ - wxWindowUpdateLocker windowlock(this); - DeleteAllItems(); - // tell the game list to rescan - CafeTitleList::Refresh(); - // resend notifications for all known titles by re-registering the callback - CafeTitleList::UnregisterCallback(m_callbackIdTitleList); - m_callbackIdTitleList = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { ((wxGameList*)ctx)->HandleTitleListCallback(evt); }, this); -} - -long wxGameList::FindListItemByTitleId(uint64 title_id) const -{ - for (int i = 0; i < GetItemCount(); ++i) - { - const auto id = (uint64)GetItemData(i); - if (id == title_id) - return i; - } - - return wxNOT_FOUND; -} - -// get title name with cache -std::string wxGameList::GetNameByTitleId(uint64 titleId) -{ - auto it = m_name_cache.find(titleId); - if (it != m_name_cache.end()) - return it->second; - TitleInfo titleInfo; - if (!CafeTitleList::GetFirstByTitleId(titleId, titleInfo)) - return "Unknown title"; - std::string name; - if (!GetConfig().GetGameListCustomName(titleId, name)) - name = titleInfo.GetMetaTitleName(); - m_name_cache.emplace(titleId, name); - return name; -} - -void wxGameList::SetStyle(Style style, bool save) -{ - if (m_style == style) - return; - - wxWindowUpdateLocker updatelock(this); - - m_style = style; - SetWindowStyleFlag(GetStyleFlags(m_style)); - - uint64 selected_title_id = 0; - auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - selected_title_id = (uint64)GetItemData(selection); - selection = wxNOT_FOUND; - } - - switch(style) - { - case Style::kIcons: - wxListCtrl::SetImageList(&m_image_list_data, wxIMAGE_LIST_NORMAL); - break; - case Style::kSmallIcons: - wxListCtrl::SetImageList(&m_image_list_small_data, wxIMAGE_LIST_NORMAL); - break; - case Style::kList: - wxListCtrl::SetImageList(&m_image_list_small_data, wxIMAGE_LIST_SMALL); - break; - } - - ReloadGameEntries(); - SortEntries(); - UpdateItemColors(); - - if(selection != wxNOT_FOUND) - { - Select(selection); - Focus(selection); - } - - if(save) - { - GetWxGUIConfig().game_list_style = (int)m_style; - GetConfigHandle().Save(); - } - - if (style == Style::kList) - ApplyGameListColumnWidths(); -} - -long wxGameList::GetStyleFlags(Style style) const -{ - switch (style) - { - case Style::kList: - return (wxLC_SINGLE_SEL | wxLC_VRULES | wxLC_REPORT); - case Style::kIcons: - return (wxLC_SINGLE_SEL | wxLC_ICON); - case Style::kSmallIcons: - return (wxLC_SINGLE_SEL | wxLC_ICON); - default: - wxASSERT(false); - return (wxLC_SINGLE_SEL | wxLC_REPORT); - } -} - -void wxGameList::UpdateItemColors(sint32 startIndex) -{ - wxWindowUpdateLocker lock(this); - - for (int i = startIndex; i < GetItemCount(); ++i) - { - const uint64 titleId = GetItemData(i); - if (GetConfig().IsGameListFavorite(titleId)) - { - SetItemBackgroundColour(i, kFavoriteColor); - SetItemTextColour(i, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - } - else if ((i % 2) != 0) - { - SetItemBackgroundColour(i, kPrimaryColor); - SetItemTextColour(i, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - } - else - { - SetItemBackgroundColour(i, kAlternateColor); - SetItemTextColour(i, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - } - } -} - -static inline int order_to_int(const std::weak_ordering &wo) -{ - // no easy conversion seems to exist in C++20 - if (wo == std::weak_ordering::less) - return -1; - else if (wo == std::weak_ordering::greater) - return 1; - return 0; -} - -std::weak_ordering wxGameList::SortComparator(uint64 titleId1, uint64 titleId2, SortData* sortData) -{ - auto titleLastPlayed = [](uint64_t id) - { - iosu::pdm::GameListStat playTimeStat{}; - iosu::pdm::GetStatForGamelist(id, playTimeStat); - return playTimeStat; - }; - - auto titlePlayMinutes = [](uint64_t id) - { - iosu::pdm::GameListStat playTimeStat; - if (!iosu::pdm::GetStatForGamelist(id, playTimeStat)) - return 0u; - return playTimeStat.numMinutesPlayed; - }; - - auto titleRegion = [](uint64_t id) - { - return CafeTitleList::GetGameInfo(id).GetRegion(); - }; - - switch(sortData->column) - { - default: - case ColumnName: - { - const auto isFavoriteA = GetConfig().IsGameListFavorite(titleId1); - const auto isFavoriteB = GetConfig().IsGameListFavorite(titleId2); - const auto nameA = GetNameByTitleId(titleId1); - const auto nameB = GetNameByTitleId(titleId2); - return std::tie(isFavoriteB, nameA) <=> std::tie(isFavoriteA, nameB); - } - case ColumnGameStarted: - return titleLastPlayed(titleId1).last_played <=> titleLastPlayed(titleId2).last_played; - case ColumnGameTime: - return titlePlayMinutes(titleId1) <=> titlePlayMinutes(titleId2); - case ColumnRegion: - return titleRegion(titleId1) <=> titleRegion(titleId2); - case ColumnTitleID: - return titleId1 <=> titleId2; - } - // unreachable - cemu_assert_debug(false); - return std::weak_ordering::less; -} - -int wxGameList::SortFunction(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) -{ - const auto sort_data = (SortData*)sortData; - return sort_data->dir * order_to_int(sort_data->thisptr->SortComparator((uint64)item1, (uint64)item2, sort_data)); -} - -void wxGameList::SortEntries(int column) -{ - bool ascending; - if (column == -1) - { - column = GetSortIndicator(); - if (column == -1) - column = ColumnName; - ascending = IsAscendingSortIndicator(); - } - else - ascending = GetUpdatedAscendingSortIndicator(column); - - switch (column) - { - case ColumnName: - case ColumnGameTime: - case ColumnGameStarted: - case ColumnRegion: - case ColumnTitleID: - { - SortData data{this, ItemColumns{column}, ascending ? 1 : -1}; - SortItems(SortFunction, (wxIntPtr)&data); - ShowSortIndicator(column, ascending); - break; - } - } -} - -void wxGameList::OnKeyDown(wxListEvent& event) -{ - event.Skip(); - if (m_style != Style::kList) - return; - - const auto keycode = event.GetKeyCode(); - if (keycode == WXK_LEFT) - { - const auto item_count = GetItemCount(); - if (item_count > 0) - { - auto selection = (int)GetFirstSelected(); - if (selection == wxNOT_FOUND) - selection = 0; - else - selection = std::max(0, selection - GetCountPerPage()); - - Select(selection); - Focus(selection); - } - } - else if (keycode == WXK_RIGHT) - { - const auto item_count = GetItemCount(); - if (item_count > 0) - { - auto selection = (int)GetFirstSelected(); - if (selection == wxNOT_FOUND) - selection = 0; - - selection = std::min(item_count - 1, selection + GetCountPerPage()); - - Select(selection); - Focus(selection); - } - } -} - -enum ContextMenuEntries -{ - kContextMenuRefreshGames = wxID_HIGHEST + 1, - - kContextMenuStart, - kWikiPage, - kContextMenuFavorite, - kContextMenuEditName, - - kContextMenuGameFolder, - kContextMenuSaveFolder, - kContextMenuUpdateFolder, - kContextMenuDLCFolder, - kContextMenuEditGraphicPacks, - kContextMenuEditGameProfile, - - kContextMenuRemoveCache, - - kContextMenuStyleList, - kContextMenuStyleIcon, - kContextMenuStyleIconSmall, - - kContextMenuCreateShortcut, - - kContextMenuCopyTitleName, - kContextMenuCopyTitleId, - kContextMenuCopyTitleImage -}; - -void wxGameList::OnContextMenu(wxContextMenuEvent& event) -{ - auto& config = GetConfig(); - - wxMenu menu; - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &wxGameList::OnContextMenuSelected, this); - - const auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - const auto title_id = (uint64)GetItemData(selection); - GameInfo2 gameInfo = CafeTitleList::GetGameInfo(title_id); - if (gameInfo.IsValid()) - { - menu.SetClientData((void*)title_id); - - menu.Append(kContextMenuStart, _("&Start")); - - bool isFavorite = GetConfig().IsGameListFavorite(title_id); - std::error_code ec; - - menu.AppendSeparator(); - menu.AppendCheckItem(kContextMenuFavorite, _("&Favorite"))->Check(isFavorite); - menu.Append(kContextMenuEditName, _("&Edit name")); - - menu.AppendSeparator(); - menu.Append(kWikiPage, _("&Wiki page")); - menu.Append(kContextMenuGameFolder, _("&Game directory")); - menu.Append(kContextMenuSaveFolder, _("&Save directory"))->Enable(fs::is_directory(gameInfo.GetSaveFolder(), ec)); - menu.Append(kContextMenuUpdateFolder, _("&Update directory"))->Enable(gameInfo.HasUpdate()); - menu.Append(kContextMenuDLCFolder, _("&DLC directory"))->Enable(gameInfo.HasAOC()); - - menu.AppendSeparator(); - menu.Append(kContextMenuRemoveCache, _("&Remove shader caches"))->Enable(!_getCachesPaths(gameInfo.GetBaseTitleId()).empty()); - - menu.AppendSeparator(); - menu.Append(kContextMenuEditGraphicPacks, _("&Edit graphic packs")); - menu.Append(kContextMenuEditGameProfile, _("&Edit game profile")); - - menu.AppendSeparator(); - menu.Append(kContextMenuCreateShortcut, _("&Create shortcut")); - menu.AppendSeparator(); - menu.Append(kContextMenuCopyTitleName, _("&Copy Title Name")); - menu.Append(kContextMenuCopyTitleId, _("&Copy Title ID")); - menu.Append(kContextMenuCopyTitleImage, _("&Copy Title Image")); - menu.AppendSeparator(); - } - } - - menu.Append(kContextMenuRefreshGames, _("&Refresh game list"))->Enable(!CafeTitleList::IsScanning()); - menu.AppendSeparator(); - menu.AppendRadioItem(kContextMenuStyleList, _("Style: &List"))->Check(m_style == Style::kList); - menu.AppendRadioItem(kContextMenuStyleIcon, _("Style: &Icons"))->Check(m_style == Style::kIcons); - menu.AppendRadioItem(kContextMenuStyleIconSmall, _("Style: &Small Icons"))->Check(m_style == Style::kSmallIcons); - PopupMenu(&menu); -} - -void wxGameList::OnContextMenuSelected(wxCommandEvent& event) -{ - const auto title_id = (uint64)((wxMenu*)event.GetEventObject())->GetClientData(); - if (title_id) - { - GameInfo2 gameInfo = CafeTitleList::GetGameInfo(title_id); - if (gameInfo.IsValid()) - { - switch (event.GetId()) - { - case kContextMenuStart: - { - MainWindow::RequestLaunchGame(gameInfo.GetBase().GetPath(), wxLaunchGameEvent::INITIATED_BY::GAME_LIST); - break; - } - case kContextMenuFavorite: - GetConfig().SetGameListFavorite(title_id, !GetConfig().IsGameListFavorite(title_id)); - SortEntries(); - UpdateItemColors(); - SaveConfig(true); - break; - case kContextMenuEditName: - { - std::string customName = ""; - if (!GetConfig().GetGameListCustomName(title_id, customName)) - customName.clear(); - wxTextEntryDialog dialog(this, wxEmptyString, _("Enter a custom game title"), wxString::FromUTF8(customName)); - if(dialog.ShowModal() == wxID_OK) - { - const auto custom_name = dialog.GetValue(); - GetConfig().SetGameListCustomName(title_id, custom_name.utf8_string()); - m_name_cache.clear(); - GetConfigHandle().Save(); - // update list entry - for (int i = 0; i < GetItemCount(); ++i) - { - const auto id = (uint64)GetItemData(i); - if (id == title_id) - { - if (m_style == Style::kList) - SetItem(i, ColumnName, wxString::FromUTF8(GetNameByTitleId(title_id))); - break; - } - } - SortEntries(); - UpdateItemColors(); - } - break; - } - case kContextMenuGameFolder: - { - fs::path path(gameInfo.GetBase().GetPath()); - _stripPathFilename(path); - wxLaunchDefaultApplication(wxHelper::FromPath(path)); - break; - } - case kWikiPage: - { - // https://wiki.cemu.info/wiki/GameIDs - // WUP-P-ALZP - if (gameInfo.GetBase().ParseXmlInfo()) - { - std::string productCode = gameInfo.GetBase().GetMetaInfo()->GetProductCode(); - const auto tokens = TokenizeView(productCode, '-'); - wxASSERT(!tokens.empty()); - const std::string company_code = gameInfo.GetBase().GetMetaInfo()->GetCompanyCode(); - wxASSERT(company_code.size() >= 2); - wxLaunchDefaultBrowser(formatWxString("https://wiki.cemu.info/wiki/{}{}", *tokens.rbegin(), company_code.substr(company_code.size() - 2))); - } - break; - } - - case kContextMenuSaveFolder: - { - wxLaunchDefaultApplication(wxHelper::FromPath(gameInfo.GetSaveFolder())); - break; - } - case kContextMenuUpdateFolder: - { - fs::path path(gameInfo.GetUpdate().GetPath()); - _stripPathFilename(path); - wxLaunchDefaultApplication(wxHelper::FromPath(path)); - break; - } - case kContextMenuDLCFolder: - { - fs::path path(gameInfo.GetAOC().front().GetPath()); - _stripPathFilename(path); - wxLaunchDefaultApplication(wxHelper::FromPath(path)); - break; - } - case kContextMenuRemoveCache: - { - RemoveCache(_getCachesPaths(gameInfo.GetBaseTitleId()), gameInfo.GetTitleName()); - break; - } - case kContextMenuEditGraphicPacks: - { - wxTitleIdEvent open_event(wxEVT_OPEN_GRAPHIC_PACK, title_id); - ProcessWindowEvent(open_event); - break; - } - case kContextMenuEditGameProfile: - { - (new GameProfileWindow(GetParent(), title_id))->Show(); - break; - } - case kContextMenuCreateShortcut: - { - CreateShortcut(gameInfo); - break; - } - case kContextMenuCopyTitleName: - { - if (wxClipboard::Get()->Open()) - { - wxClipboard::Get()->SetData(new wxTextDataObject(wxString::FromUTF8(gameInfo.GetTitleName()))); - wxClipboard::Get()->Close(); - } - break; - } - case kContextMenuCopyTitleId: - { - if (wxClipboard::Get()->Open()) - { - wxClipboard::Get()->SetData(new wxTextDataObject(fmt::format("{:016x}", gameInfo.GetBaseTitleId()))); - wxClipboard::Get()->Close(); - } - break; - } - case kContextMenuCopyTitleImage: - { - if (wxClipboard::Get()->Open()) - { - int icon_large; - int icon_small; - if (!QueryIconForTitle(title_id, icon_large, icon_small)) - break; - auto icon = m_image_list_data.GetIcon(icon_large); - wxClipboard::Get()->SetData(new wxBitmapDataObject(icon)); - wxClipboard::Get()->Close(); - } - break; - } - } - } - } - - switch (event.GetId()) - { - case kContextMenuRefreshGames: - ReloadGameEntries(); - break; - case kContextMenuStyleList: - SetStyle(Style::kList); - break; - case kContextMenuStyleIcon: - SetStyle(Style::kIcons); - break; - case kContextMenuStyleIconSmall: - SetStyle(Style::kSmallIcons); - break; - } -} - -void wxGameList::OnColumnClick(wxListEvent& event) -{ - const int column = event.GetColumn(); - SortEntries(column); - UpdateItemColors(); - event.Skip(); -} - -void wxGameList::OnColumnRightClick(wxListEvent& event) -{ - enum ItemIds - { - ResetWidth = wxID_HIGHEST + 1, - ResetOrder, - - ShowIcon, - ShowName, - ShowVersion, - ShowDlc, - ShowGameTime, - ShowLastPlayed, - ShowRegion, - ShowTitleId - }; - const int column = event.GetColumn(); - wxMenu menu; - menu.SetClientObject(new wxCustomData(column)); - - menu.Append(ResetWidth, _("Reset &width")); - menu.Append(ResetOrder, _("Reset &order")) ; - - menu.AppendSeparator(); - menu.AppendCheckItem(ShowIcon, _("Show &icon"))->Check(GetColumnWidth(ColumnIcon) > 0); - menu.AppendCheckItem(ShowName, _("Show &name"))->Check(GetColumnWidth(ColumnName) > 0); - menu.AppendCheckItem(ShowVersion, _("Show &version"))->Check(GetColumnWidth(ColumnVersion) > 0); - menu.AppendCheckItem(ShowDlc, _("Show &dlc"))->Check(GetColumnWidth(ColumnDLC) > 0); - menu.AppendCheckItem(ShowGameTime, _("Show &game time"))->Check(GetColumnWidth(ColumnGameTime) > 0); - menu.AppendCheckItem(ShowLastPlayed, _("Show &last played"))->Check(GetColumnWidth(ColumnGameStarted) > 0); - menu.AppendCheckItem(ShowRegion, _("Show ®ion"))->Check(GetColumnWidth(ColumnRegion) > 0); - menu.AppendCheckItem(ShowTitleId, _("Show &title ID"))->Check(GetColumnWidth(ColumnTitleID) > 0); - - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, - [this](wxCommandEvent& event) { - event.Skip(); - - const auto menu = dynamic_cast(event.GetEventObject()); - const int column = dynamic_cast*>(menu->GetClientObject())->GetData(); - auto& config = GetWxGUIConfig(); - - switch (event.GetId()) - { - case ShowIcon: - config.show_icon_column = menu->IsChecked(ShowIcon); - break; - case ShowName: - config.column_width.name = menu->IsChecked(ShowName) ? DefaultColumnSize::name : 0; - break; - case ShowVersion: - config.column_width.version = menu->IsChecked(ShowVersion) ? DefaultColumnSize::version : 0; - break; - case ShowDlc: - config.column_width.dlc = menu->IsChecked(ShowDlc) ? DefaultColumnSize::dlc : 0; - break; - case ShowGameTime: - config.column_width.game_time = menu->IsChecked(ShowGameTime) ? DefaultColumnSize::game_time : 0; - break; - case ShowLastPlayed: - config.column_width.game_started = menu->IsChecked(ShowLastPlayed) ? DefaultColumnSize::game_started : 0; - break; - case ShowRegion: - config.column_width.region = menu->IsChecked(ShowRegion) ? DefaultColumnSize::region : 0; - break; - case ShowTitleId: - config.column_width.title_id = menu->IsChecked(ShowTitleId) ? DefaultColumnSize::title_id : 0; - break; - case ResetWidth: - { - switch (column) - { - case ColumnIcon: - break; - case ColumnName: - config.column_width.name = DefaultColumnSize::name; - break; - case ColumnVersion: - config.column_width.version = DefaultColumnSize::version; - break; - case ColumnDLC: - config.column_width.dlc = DefaultColumnSize::dlc; - break; - case ColumnGameTime: - config.column_width.game_time = DefaultColumnSize::game_time; - break; - case ColumnGameStarted: - config.column_width.game_started = DefaultColumnSize::game_started; - break; - case ColumnRegion: - config.column_width.region = DefaultColumnSize::region; - break; - case ColumnTitleID: - config.column_width.title_id = DefaultColumnSize::title_id; - default: - return; - } - - break; - } - case ResetOrder: - { - config.game_list_column_order.clear(); - wxArrayInt order(ColumnCounts); - std::iota(order.begin(), order.end(), 0); - #ifdef wxHAS_LISTCTRL_COLUMN_ORDER - SetColumnsOrder(order); - #endif - //ApplyGameListColumnWidths(); - //Refresh(); - //return; - } - } - - GetConfigHandle().Save(); - ApplyGameListColumnWidths(); - }); - - PopupMenu(&menu); - event.Skip(); -} - -void wxGameList::ApplyGameListColumnWidths() -{ - const auto& config = GetWxGUIConfig(); - wxWindowUpdateLocker lock(this); - if(config.show_icon_column) - SetColumnWidth(ColumnIcon, kListIconWidth+2); - else - SetColumnWidth(ColumnIcon, 0); - SetColumnWidth(ColumnName, config.column_width.name); - SetColumnWidth(ColumnVersion, config.column_width.version); - SetColumnWidth(ColumnDLC, config.column_width.dlc); - SetColumnWidth(ColumnGameTime, config.column_width.game_time); - SetColumnWidth(ColumnGameStarted, config.column_width.game_started); - SetColumnWidth(ColumnRegion, config.column_width.region); - SetColumnWidth(ColumnTitleID, config.column_width.title_id); - - AdjustLastColumnWidth(); -} - -void wxGameList::OnColumnBeginResize(wxListEvent& event) -{ - const int column = event.GetColumn(); - const int width = GetColumnWidth(column); - int last_col_index = 0; - for(int i = GetColumnCount() - 1; i > 0; i--) - { -#ifdef wxHAS_LISTCTRL_COLUMN_ORDER - if(GetColumnWidth(GetColumnIndexFromOrder(i)) > 0) - { - last_col_index = GetColumnIndexFromOrder(i); - break; - } -#else - if(GetColumnWidth(i) > 0) - { - last_col_index = i; - break; - } -#endif - } - if (width == 0 || column == ColumnIcon || column == last_col_index) // dont resize hidden name, icon, and last column - event.Veto(); - else - event.Skip(); -} - -void wxGameList::OnColumnResize(wxListEvent& event) -{ - event.Skip(); - - if(m_style != Style::kList) - return; - - const int column = event.GetColumn(); - const int width = GetColumnWidth(column); - - auto& config = GetWxGUIConfig(); - switch (column) - { - case ColumnName: - config.column_width.name = width; - break; - case ColumnVersion: - config.column_width.version = width; - break; - case ColumnDLC: - config.column_width.dlc = width; - break; - case ColumnGameTime: - config.column_width.game_time = width; - break; - case ColumnGameStarted: - config.column_width.game_started = width; - break; - case ColumnRegion: - config.column_width.region = width; - break; - default: - break; - } - - GetConfigHandle().Save(); - AdjustLastColumnWidth(); -} - -void wxGameList::OnClose(wxCloseEvent& event) -{ - event.Skip(); - m_exit = true; -} - -int wxGameList::FindInsertPosition(TitleId titleId, bool& entryAlreadyExists) -{ - entryAlreadyExists = false; - SortData data{this, ItemColumns(GetSortIndicator()), IsAscendingSortIndicator() ? 1 : -1}; - const auto itemCount = GetItemCount(); - - if (itemCount == 0) - return 0; - sint32 low = 0; - sint32 high = itemCount; - while (low < high) - { - sint32 mid = low + (high - low) / 2; - auto cmp = data.dir * order_to_int(SortComparator(titleId, (uint64)GetItemData(mid), &data)); - if (cmp <= 0) - { - if (cmp == 0) - { - entryAlreadyExists = true; - return mid; - } - high = mid; - } - else - { - low = mid + 1; - } - } - return low; -} - -void wxGameList::OnTimerBulkAddEntriesToGameList(wxTimerEvent& event) -{ - std::vector titleIdsToUpdate; - std::swap(titleIdsToUpdate, m_bulkTitlesToAdd); - - wxWindowUpdateLocker lock(this); - bool hasAnyNewEntry = false; - for (auto& titleId : titleIdsToUpdate) - { - GameInfo2 gameInfo = CafeTitleList::GetGameInfo(titleId); - if (!gameInfo.IsValid() || gameInfo.IsSystemDataTitle()) - { - // entry no longer exists or is not a valid game - // we dont need to remove list entries here because all delete operations should trigger a full list refresh - continue; - } - TitleId baseTitleId = gameInfo.GetBaseTitleId(); - bool isNewEntry = false; - - int icon = -1; /* 0 is the default empty icon */ - int icon_small = -1; /* 0 is the default empty icon */ - QueryIconForTitle(baseTitleId, icon, icon_small); - - bool entryAlreadyExists = false; - auto index = FindInsertPosition(baseTitleId, entryAlreadyExists); - if(!entryAlreadyExists) - { - // entry doesn't exist - index = InsertItem(index, wxString::FromUTF8(GetNameByTitleId(baseTitleId))); - SetItemPtrData(index, baseTitleId); - isNewEntry = true; - hasAnyNewEntry = true; - } - - if (m_style == Style::kList) - { - SetItemColumnImage(index, ColumnIcon, icon_small); - - SetItem(index, ColumnName, wxString::FromUTF8(GetNameByTitleId(baseTitleId))); - - SetItem(index, ColumnVersion, fmt::format("{}", gameInfo.GetVersion())); - - if(gameInfo.HasAOC()) - SetItem(index, ColumnDLC, fmt::format("{}", gameInfo.GetAOCVersion())); - else - SetItem(index, ColumnDLC, wxString()); - - if (isNewEntry) - { - iosu::pdm::GameListStat playTimeStat; - if (iosu::pdm::GetStatForGamelist(baseTitleId, playTimeStat)) - { - // time played - uint32 minutesPlayed = playTimeStat.numMinutesPlayed; - if (minutesPlayed == 0) - SetItem(index, ColumnGameTime, wxEmptyString); - else if (minutesPlayed < 60) - SetItem(index, ColumnGameTime, formatWxString(wxPLURAL("{} minute", "{} minutes", minutesPlayed), minutesPlayed)); - else - { - uint32 hours = minutesPlayed / 60; - uint32 minutes = minutesPlayed % 60; - wxString hoursText = formatWxString(wxPLURAL("{} hour", "{} hours", hours), hours); - wxString minutesText = formatWxString(wxPLURAL("{} minute", "{} minutes", minutes), minutes); - SetItem(index, ColumnGameTime, hoursText + " " + minutesText); - } - - // last played - if (playTimeStat.last_played.year != 0) - { - const wxDateTime tmp((wxDateTime::wxDateTime_t)playTimeStat.last_played.day, (wxDateTime::Month)playTimeStat.last_played.month, (wxDateTime::wxDateTime_t)playTimeStat.last_played.year, 0, 0, 0, 0); - SetItem(index, ColumnGameStarted, tmp.FormatDate()); - } - else - SetItem(index, ColumnGameStarted, _("never")); - } - else - { - SetItem(index, ColumnGameTime, wxEmptyString); - SetItem(index, ColumnGameStarted, _("never")); - } - } - const auto region_text = fmt::format("{}", gameInfo.GetRegion()); - SetItem(index, ColumnRegion, wxGetTranslation(region_text)); - SetItem(index, ColumnTitleID, fmt::format("{:016x}", baseTitleId)); - } - else if (m_style == Style::kIcons) - { - SetItemImage(index, icon); - } - else if (m_style == Style::kSmallIcons) - { - SetItemImage(index, icon_small); - } - } - if (hasAnyNewEntry) - UpdateItemColors(); -} - -void wxGameList::OnGameEntryUpdatedByTitleId(wxTitleIdEvent& event) -{ - if (m_bulkTitlesToAdd.size() < 100) - m_bulkUpdateTimer.StartOnce(100); // if timer is started already this will delay it - const auto titleId = event.GetTitleId(); - m_bulkTitlesToAdd.emplace_back(titleId); -} - -void wxGameList::OnItemActivated(wxListEvent& event) -{ - event.Skip(); - - const auto selection = event.GetIndex(); - if (selection == wxNOT_FOUND) - return; - - const auto item_data = (uint64)GetItemData(selection); - if(item_data == kDefaultEntryData) - { - const wxCommandEvent open_settings_event(wxEVT_OPEN_SETTINGS); - wxPostEvent(this, open_settings_event); - return; - } - - TitleInfo titleInfo; - if (!CafeTitleList::GetFirstByTitleId(item_data, titleInfo)) - return; - if (!titleInfo.IsValid()) - return; - - MainWindow::RequestLaunchGame(titleInfo.GetPath(), wxLaunchGameEvent::INITIATED_BY::GAME_LIST); -} - -void wxGameList::OnTimer(wxTimerEvent& event) -{ - const auto& obj = event.GetTimer().GetId(); - if(obj == m_tooltip_timer->GetId()) - { - m_tooltip_window->Hide(); - - auto flag = wxLIST_HITTEST_ONITEM; - const auto item = this->HitTest(m_mouse_position, flag); - if(item != wxNOT_FOUND ) - { - //const auto title_id = (uint64_t)GetItemData(item); - //auto entry = GetGameEntry(title_id); - //if (entry && entry->is_update) - //{ - // m_tooltip_window->SetPosition(wxPoint(m_mouse_position.x + 15, m_mouse_position.y + 15)); - // m_tooltip_window->SendSizeEvent(); - // m_tooltip_window->Show(); - //} - } - } - -} - -void wxGameList::OnMouseMove(wxMouseEvent& event) -{ - m_tooltip_timer->Stop(); - m_tooltip_timer->StartOnce(250); - m_mouse_position = event.GetPosition(); -} - -void wxGameList::OnLeaveWindow(wxMouseEvent& event) -{ - m_tooltip_timer->Stop(); - m_tooltip_window->Hide(); -} - -void wxGameList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt) -{ - if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED || - evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) - { - wxQueueEvent(this, new wxTitleIdEvent(wxEVT_GAME_ENTRY_ADDED_OR_REMOVED, evt->titleInfo->GetAppTitleId())); - } -} - -void wxGameList::RemoveCache(const std::vector& cachePaths, const std::string& titleName) -{ - wxMessageDialog dialog(this, formatWxString(_("Remove the shader caches for {}?"), titleName), _("Remove shader caches"), wxCENTRE | wxYES_NO | wxICON_EXCLAMATION); - dialog.SetYesNoLabels(_("Yes"), _("No")); - - const auto dialogResult = dialog.ShowModal(); - if (dialogResult != wxID_YES) - return; - std::vector errs; - for (const fs::path& cachePath : cachePaths) - { - if (std::error_code ec; !fs::remove(cachePath, ec)) - errs.emplace_back(fmt::format("{} : {}", cachePath.string(), ec.message())); - } - if (errs.empty()) - wxMessageDialog(this, _("The shader caches were removed!"), _("Shader caches removed"), wxCENTRE | wxOK | wxICON_INFORMATION).ShowModal(); - else - wxMessageDialog(this, formatWxString(_("Failed to remove the shader caches:\n{}"), fmt::join(errs, "\n")), _("Error"), wxCENTRE | wxOK | wxICON_ERROR).ShowModal(); -} - -void wxGameList::AsyncWorkerThread() -{ - SetThreadName("GameListWorker"); - while (m_async_worker_active) - { - m_async_task_count.decrementWithWait(); - // get next titleId to load (if any) - m_async_worker_mutex.lock(); - bool hasJob = !m_icon_load_queue.empty(); - TitleId titleId = 0; - if (hasJob) - { - titleId = m_icon_load_queue.front(); - m_icon_load_queue.erase(m_icon_load_queue.begin()); - } - m_async_worker_mutex.unlock(); - if (!hasJob) - continue; - if(m_icon_loaded.find(titleId) != m_icon_loaded.end()) - continue; - m_icon_loaded.emplace(titleId); - // load and process icon - TitleInfo titleInfo; - if( !CafeTitleList::GetFirstByTitleId(titleId, titleInfo) ) - continue; - std::string tempMountPath = TitleInfo::GetUniqueTempMountingPath(); - if(!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE)) - continue; - auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str()); - // try iconTex.tga.gz - if (!tgaData) - { - tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga.gz").c_str()); - if (tgaData) - { - auto decompressed = zlibDecompress(*tgaData, 70*1024); - std::swap(tgaData, decompressed); - } - } - bool iconSuccessfullyLoaded = false; - if (tgaData && tgaData->size() > 16) - { - wxMemoryInputStream tmp_stream(tgaData->data(), tgaData->size()); - const wxImage image(tmp_stream); - // todo - is wxImageList thread safe? - int icon = m_image_list_data.Add(image.Scale(kIconWidth, kIconWidth, wxIMAGE_QUALITY_BICUBIC)); - int icon_small = m_image_list_small_data.Add(image.Scale(kListIconWidth, kListIconWidth, wxIMAGE_QUALITY_BICUBIC)); - // store in cache - m_icon_cache_mtx.lock(); - m_icon_cache.try_emplace(titleId, icon, icon_small); - m_icon_cache_mtx.unlock(); - iconSuccessfullyLoaded = true; - } - else - { - cemuLog_log(LogType::Force, "Failed to load icon for title {:016x}", titleId); - } - titleInfo.Unmount(tempMountPath); - // notify UI about loaded icon - if(iconSuccessfullyLoaded) - wxQueueEvent(this, new wxTitleIdEvent(wxEVT_GAME_ENTRY_ADDED_OR_REMOVED, titleId)); - } -} - -void wxGameList::RequestLoadIconAsync(TitleId titleId) -{ - m_async_worker_mutex.lock(); - m_icon_load_queue.push_back(titleId); - m_async_worker_mutex.unlock(); - m_async_task_count.increment(); -} - -// returns icons if cached, otherwise an async request to load them is made -bool wxGameList::QueryIconForTitle(TitleId titleId, int& icon, int& iconSmall) -{ - m_icon_cache_mtx.lock(); - auto it = m_icon_cache.find(titleId); - if (it == m_icon_cache.end()) - { - m_icon_cache_mtx.unlock(); - RequestLoadIconAsync(titleId); - return false; - } - icon = it->second.first; - iconSmall = it->second.second; - m_icon_cache_mtx.unlock(); - return true; -} - -void wxGameList::DeleteCachedStrings() -{ - m_name_cache.clear(); -} - -#if BOOST_OS_LINUX || BOOST_OS_BSD -void wxGameList::CreateShortcut(GameInfo2& gameInfo) -{ - const auto titleId = gameInfo.GetBaseTitleId(); - const auto titleName = wxString::FromUTF8(gameInfo.GetTitleName()); - auto exePath = ActiveSettings::GetExecutablePath(); - const char* flatpakId = getenv("FLATPAK_ID"); - - const wxString desktopEntryName = wxString::Format("%s.desktop", titleName); - wxFileDialog entryDialog(this, _("Choose desktop entry location"), "~/.local/share/applications", desktopEntryName, - "Desktop file (*.desktop)|*.desktop", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); - const auto result = entryDialog.ShowModal(); - if (result == wxID_CANCEL) - return; - const auto output_path = entryDialog.GetPath(); - - std::optional iconPath; - // Obtain and convert icon - [&]() - { - int iconIdx, smallIconIdx; - - if (!QueryIconForTitle(titleId, iconIdx, smallIconIdx)) - { - cemuLog_log(LogType::Force, "Icon hasn't loaded"); - return; - } - const fs::path outIconDir = ActiveSettings::GetUserDataPath("icons"); - - if (!fs::exists(outIconDir) && !fs::create_directories(outIconDir)) - { - cemuLog_log(LogType::Force, "Failed to create icon directory"); - return; - } - - iconPath = outIconDir / fmt::format("{:016x}.png", gameInfo.GetBaseTitleId()); - wxFileOutputStream pngFileStream(_pathToUtf8(iconPath.value())); - - const auto icon = m_image_list_data.GetIcon(iconIdx); - wxBitmap bitmap{icon}; - wxImage image = bitmap.ConvertToImage(); - wxPNGHandler pngHandler; - if (!pngHandler.SaveFile(&image, pngFileStream, false)) - { - iconPath = std::nullopt; - cemuLog_log(LogType::Force, "Icon failed to save"); - } - }(); - - std::string desktopExecEntry = flatpakId ? fmt::format("/usr/bin/flatpak run {0} --title-id {1:016x}", flatpakId, titleId) - : fmt::format("{0:?} --title-id {1:016x}", _pathToUtf8(exePath), titleId); - - // 'Icon' accepts spaces in file name, does not accept quoted file paths - // 'Exec' does not accept non-escaped spaces, and can accept quoted file paths - auto desktopEntryString = fmt::format( - "[Desktop Entry]\n" - "Name={0}\n" - "Comment=Play {0} on Cemu\n" - "Exec={1}\n" - "Icon={2}\n" - "Terminal=false\n" - "Type=Application\n" - "Categories=Game;\n", - titleName.utf8_string(), - desktopExecEntry, - _pathToUtf8(iconPath.value_or(""))); - - if (flatpakId) - desktopEntryString += fmt::format("X-Flatpak={}\n", flatpakId); - - std::ofstream outputStream(output_path.utf8_string()); - if (!outputStream.good()) - { - auto errorMsg = formatWxString(_("Failed to save desktop entry to {}"), output_path.utf8_string()); - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return; - } - outputStream << desktopEntryString; -} -#elif BOOST_OS_MACOS -void wxGameList::CreateShortcut(GameInfo2& gameInfo) -{ - const auto titleId = gameInfo.GetBaseTitleId(); - const auto titleName = wxString::FromUTF8(gameInfo.GetTitleName()); - auto exePath = ActiveSettings::GetExecutablePath(); - - const wxString appName = wxString::Format("%s.app", titleName); - wxFileDialog entryDialog(this, _("Choose shortcut location"), "~/Applications", appName, - "Application (*.app)|*.app", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); - const auto result = entryDialog.ShowModal(); - if (result == wxID_CANCEL) - return; - const auto output_path = entryDialog.GetPath(); - // Create .app folder - const fs::path appPath = output_path.utf8_string(); - if (!fs::create_directories(appPath)) - { - cemuLog_log(LogType::Force, "Failed to create app directory"); - return; - } - const fs::path infoPath = appPath / "Contents/Info.plist"; - const fs::path scriptPath = appPath / "Contents/MacOS/run.sh"; - const fs::path icnsPath = appPath / "Contents/Resources/shortcut.icns"; - if (!(fs::create_directories(scriptPath.parent_path()) && fs::create_directories(icnsPath.parent_path()))) - { - cemuLog_log(LogType::Force, "Failed to create app shortcut directories"); - return; - } - - std::optional iconPath; - // Obtain and convert icon - [&]() - { - int iconIdx, smallIconIdx; - - if (!QueryIconForTitle(titleId, iconIdx, smallIconIdx)) - { - cemuLog_log(LogType::Force, "Icon hasn't loaded"); - return; - } - const fs::path outIconDir = fs::temp_directory_path(); - - if (!fs::exists(outIconDir) && !fs::create_directories(outIconDir)) - { - cemuLog_log(LogType::Force, "Failed to create icon directory"); - return; - } - - iconPath = outIconDir / fmt::format("{:016x}.png", gameInfo.GetBaseTitleId()); - wxFileOutputStream pngFileStream(_pathToUtf8(iconPath.value())); - - const auto icon = m_image_list_data.GetIcon(iconIdx); - wxBitmap bitmap{icon}; - wxImage image = bitmap.ConvertToImage(); - wxPNGHandler pngHandler; - if (!pngHandler.SaveFile(&image, pngFileStream, false)) - { - iconPath = std::nullopt; - cemuLog_log(LogType::Force, "Icon failed to save"); - } - }(); - - std::string runCommand = fmt::format("#!/bin/zsh\n\n{0:?} --title-id {1:016x}", _pathToUtf8(exePath), titleId); - const std::string infoPlist = fmt::format( - "\n" - "\n" - "\n" - "\n" - " CFBundleDisplayName\n" - " {0}\n" - " CFBundleExecutable\n" - " run.sh\n" - " CFBundleIconFile\n" - " shortcut.icns\n" - " CFBundleName\n" - " {0}\n" - " CFBundlePackageType\n" - " APPL\n" - " CFBundleSignature\n" - " \?\?\?\?\n" - " LSApplicationCategoryType\n" - " public.app-category.games\n" - " CFBundleShortVersionString\n" - " {1}\n" - " CFBundleVersion\n" - " {1}\n" - "\n" - "\n", - gameInfo.GetTitleName(), - std::to_string(gameInfo.GetVersion()) - ); - // write Info.plist to infoPath - std::ofstream infoStream(infoPath); - std::ofstream scriptStream(scriptPath); - if (!infoStream.good() || !scriptStream.good()) - { - auto errorMsg = formatWxString(_("Failed to save app shortcut to {}"), output_path.utf8_string()); - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - return; - } - infoStream << infoPlist; - scriptStream << runCommand; - scriptStream.close(); - - // Set execute permissions for script - fs::permissions( - scriptPath, - fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec, - fs::perm_options::add - ); - - // Return if iconPath is empty - if (!iconPath) - { - cemuLog_log(LogType::Force, "Icon not found"); - return; - } - - // Convert icon to icns, only works for 128x128 PNG - // Alternatively, can run the command "sips -s format icns {iconPath} --out '{icnsPath}'" - // using std::system() to handle images of any size - if (!writeICNS(*iconPath, icnsPath)) - { - cemuLog_log(LogType::Force, "Failed to convert icon to icns"); - return; - } - - // Remove temp file - fs::remove(*iconPath); -} -#elif BOOST_OS_WINDOWS -void wxGameList::CreateShortcut(GameInfo2& gameInfo) -{ - const auto titleId = gameInfo.GetBaseTitleId(); - const auto titleName = wxString::FromUTF8(gameInfo.GetTitleName()); - auto exePath = ActiveSettings::GetExecutablePath(); - - // Get '%APPDATA%\Microsoft\Windows\Start Menu\Programs' path - PWSTR userShortcutFolder; - SHGetKnownFolderPath(FOLDERID_Programs, 0, NULL, &userShortcutFolder); - const wxString shortcutName = wxString::Format("%s.lnk", titleName); - wxFileDialog shortcutDialog(this, _("Choose shortcut location"), userShortcutFolder, shortcutName, - "Shortcut (*.lnk)|*.lnk", wxFD_SAVE | wxFD_CHANGE_DIR | wxFD_OVERWRITE_PROMPT); - - CoTaskMemFree(userShortcutFolder); - - const auto result = shortcutDialog.ShowModal(); - if (result == wxID_CANCEL) - return; - const auto outputPath = shortcutDialog.GetPath(); - - std::optional icon_path = std::nullopt; - { - int iconIdx; - int smallIconIdx; - if (!QueryIconForTitle(titleId, iconIdx, smallIconIdx)) - { - cemuLog_log(LogType::Force, "Icon hasn't loaded"); - return; - } - const auto icon = m_image_list_data.GetIcon(iconIdx); - const auto folder = ActiveSettings::GetUserDataPath("icons"); - if (!fs::exists(folder) && !fs::create_directories(folder)) - { - cemuLog_log(LogType::Force, "Failed to create icon directory"); - return; - } - wxBitmap bitmap{icon}; - - icon_path = folder / fmt::format("{:016x}.ico", titleId); - auto stream = wxFileOutputStream(icon_path->wstring()); - auto image = bitmap.ConvertToImage(); - wxICOHandler icohandler{}; - if (!icohandler.SaveFile(&image, stream, false)) - { - icon_path = std::nullopt; - cemuLog_log(LogType::Force, "Icon failed to save"); - } - } - - Microsoft::WRL::ComPtr shellLink; - HRESULT hres = CoCreateInstance(__uuidof(ShellLink), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); - if (SUCCEEDED(hres)) - { - const auto description = wxString::Format("Play %s on Cemu", titleName); - const auto args = wxString::Format("-t %016llx", titleId); - - shellLink->SetPath(exePath.wstring().c_str()); - shellLink->SetDescription(description.wc_str()); - shellLink->SetArguments(args.wc_str()); - shellLink->SetWorkingDirectory(exePath.parent_path().wstring().c_str()); - - if (icon_path) - shellLink->SetIconLocation(icon_path->wstring().c_str(), 0); - else - shellLink->SetIconLocation(exePath.wstring().c_str(), 0); - - Microsoft::WRL::ComPtr shellLinkFile; - // save the shortcut - hres = shellLink.As(&shellLinkFile); - if (SUCCEEDED(hres)) - { - hres = shellLinkFile->Save(outputPath.wc_str(), TRUE); - } - } - if (FAILED(hres)) - { - auto errorMsg = formatWxString(_("Failed to save shortcut to {}"), outputPath); - wxMessageBox(errorMsg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); - } -} -#endif diff --git a/src/gui/wxgui/components/wxGameList.h b/src/gui/wxgui/components/wxGameList.h deleted file mode 100644 index 388a9df6..00000000 --- a/src/gui/wxgui/components/wxGameList.h +++ /dev/null @@ -1,166 +0,0 @@ -#pragma once - -#include "config/CemuConfig.h" -#include "Cafe/TitleList/TitleId.h" - -#include -#include -#include -#include -#include - -#include "wxHelper.h" -#include "util/helpers/Semaphore.h" - -class wxTitleIdEvent : public wxCommandEvent -{ -public: - wxTitleIdEvent(int type, uint64 title_id) - : wxCommandEvent(type), m_title_id(title_id) {} - - [[nodiscard]] uint64 GetTitleId() const { return m_title_id; } - -private: - uint64 m_title_id; -}; - -wxDECLARE_EVENT(wxEVT_OPEN_SETTINGS, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_OPEN_GRAPHIC_PACK, wxTitleIdEvent); -wxDECLARE_EVENT(wxEVT_GAMELIST_BEGIN_UPDATE, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_GAMELIST_END_UPDATE, wxCommandEvent); - -class wxGameList : public wxListView -{ - friend class MainWindow; -public: - enum class Style - { - kList, - kIcons, - kSmallIcons - }; - - wxGameList(wxWindow* parent, wxWindowID id = wxID_ANY); - ~wxGameList(); - - void SetStyle(Style style, bool save = true); - - void LoadConfig(); - void SaveConfig(bool flush = false); - bool IsVisible(long item) const; // only available in wxwidgets 3.1.3 - - void ReloadGameEntries(); - void DeleteCachedStrings(); - - void CreateShortcut(GameInfo2& gameInfo); - - long FindListItemByTitleId(uint64 title_id) const; - - void OnClose(wxCloseEvent& event); - -private: - std::atomic_bool m_exit = false; - Style m_style; - long GetStyleFlags(Style style) const; - - const wxColour kUpdateColor{ wxSystemSettings::SelectLightDark(wxColour(195, 57, 57), wxColour(84, 29, 29)) }; - const wxColour kFavoriteColor{ wxSystemSettings::SelectLightDark(wxColour(253, 246, 211), wxColour(82, 84, 48)) }; - const wxColour kPrimaryColor = GetBackgroundColour(); - const wxColour kAlternateColor = wxHelper::CalculateAccentColour(kPrimaryColor); - void UpdateItemColors(sint32 startIndex = 0); - - enum ItemColumns : int - { - ColumnHiddenName = 0, - ColumnIcon, - ColumnName, - ColumnVersion, - ColumnDLC, - ColumnGameTime, - ColumnGameStarted, - ColumnRegion, - ColumnTitleID, - //ColumnFavorite, - ColumnCounts, - }; - - void SortEntries(int column = -1); - struct SortData - { - wxGameList* thisptr; - ItemColumns column; - int dir; - }; - - int FindInsertPosition(TitleId titleId, bool& entryAlreadyExists); - std::weak_ordering SortComparator(uint64 titleId1, uint64 titleId2, SortData* sortData); - static int SortFunction(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData); - - wxTimer* m_tooltip_timer; - wxToolTip* m_tool_tip; - wxPanel* m_tooltip_window; - wxPoint m_mouse_position; - - std::mutex m_async_worker_mutex; - std::thread m_async_worker_thread; - CounterSemaphore m_async_task_count; - std::atomic_bool m_async_worker_active{false}; - - std::vector m_icon_load_queue; - - uint64 m_callbackIdTitleList; - - std::string GetNameByTitleId(uint64 titleId); - - void HandleTitleListCallback(struct CafeTitleListCallbackEvent* evt); - - void RemoveCache(const std::vector& cachePath, const std::string& titleName); - - void AsyncWorkerThread(); - void RequestLoadIconAsync(TitleId titleId); - bool QueryIconForTitle(TitleId titleId, int& icon, int& iconSmall); - - inline static constexpr int kListIconWidth = 64; - inline static constexpr int kIconWidth = 128; - wxImageList m_image_list_data = wxImageList(kIconWidth, kIconWidth, false, 1); - wxImageList m_image_list_small_data = wxImageList(kListIconWidth, kListIconWidth, false, 1); - - std::mutex m_icon_cache_mtx; - std::set m_icon_loaded; - std::map> m_icon_cache; // pair contains icon and small icon - - std::map m_name_cache; - - // bulk update handling - std::vector m_bulkTitlesToAdd; - wxTimer m_bulkUpdateTimer; - - // list mode - void CreateListColumns(); - - void OnColumnClick(wxListEvent& event); - void OnColumnRightClick(wxListEvent& event); - void ApplyGameListColumnWidths(); - void OnColumnBeginResize(wxListEvent& event); - void OnColumnResize(wxListEvent& event); - void OnColumnDrag(wxListEvent& event); - - // generic events - void OnKeyDown(wxListEvent& event); - void OnContextMenu(wxContextMenuEvent& event); - void OnContextMenuSelected(wxCommandEvent& event); - void OnGameEntryUpdatedByTitleId(wxTitleIdEvent& event); - void OnItemActivated(wxListEvent& event); - void OnTimer(wxTimerEvent& event); - void OnMouseMove(wxMouseEvent& event); - void OnLeaveWindow(wxMouseEvent& event); - - void OnGameListSize(wxSizeEvent& event); - void AdjustLastColumnWidth(); - int GetColumnDefaultWidth(int column); - - void OnTimerBulkAddEntriesToGameList(wxTimerEvent& event); - - static inline std::once_flag s_launch_file_once; -}; - diff --git a/src/gui/wxgui/components/wxInputDraw.cpp b/src/gui/wxgui/components/wxInputDraw.cpp deleted file mode 100644 index d12237c8..00000000 --- a/src/gui/wxgui/components/wxInputDraw.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include "wxgui/components/wxInputDraw.h" - -#include -#include - -wxInputDraw::wxInputDraw(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size) - : wxWindow(parent, id, pos, size, 0, wxPanelNameStr) -{ - SetBackgroundStyle(wxBG_STYLE_PAINT); - Bind(wxEVT_PAINT, &wxInputDraw::OnPaintEvent, this); -} - -wxInputDraw::~wxInputDraw() { Unbind(wxEVT_PAINT, &wxInputDraw::OnPaintEvent, this); } - -void wxInputDraw::OnPaintEvent(wxPaintEvent& event) -{ - wxAutoBufferedPaintDC dc(this); - OnRender(dc); -} - -void wxInputDraw::OnRender(wxDC& dc) -{ - dc.Clear(); - - glm::vec2 position = m_position; - - wxPen black = wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - wxPen red = *wxRED_PEN; - wxPen grey = wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - wxPen green = wxSystemSettings::SelectLightDark(0x336600, 0x99FF99); - - wxBrush black_brush = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - wxBrush red_brush = *wxRED_BRUSH; - wxBrush grey_brush = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); - wxBrush green_brush = wxSystemSettings::SelectLightDark(0x336600, 0x99FF99); - - if(!IsEnabled()) - { - position = {}; - black.SetColour(black.GetColour()); - red.SetColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - grey.SetColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT).MakeDisabled()); - - black_brush = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); - red_brush = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT).MakeDisabled(); - grey_brush = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT).MakeDisabled(); - } - - dc.SetBackgroundMode(wxSOLID); - dc.SetBackground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - dc.Clear(); - - const auto size = GetSize(); - const auto min_size = (float)std::min(size.GetWidth(), size.GetHeight()) - 1.0f; - const Vector2f middle{min_size / 2.0f, min_size / 2.0f}; - - // border - const wxRect border{0, 0, (int)min_size, (int)min_size}; - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - dc.DrawRectangle(border); - - dc.SetPen(IsEnabled() ? green.GetColour() : grey.GetColour()); - dc.DrawCircle((int)middle.x, (int)middle.y, (int)middle.x); - - if (m_deadzone > 0) - { - dc.SetPen(grey); - dc.SetBrush(grey_brush); - const auto deadzone_size = m_deadzone * min_size / 2.0f; - dc.DrawCircle( - static_cast(middle.x), - static_cast(middle.x), - (int)deadzone_size); - - if (length(position) >= m_deadzone) - { - dc.SetPen(red); - dc.SetBrush(red_brush); - - if (std::abs(1.0f - length(position)) < 0.05f) - { - dc.SetPen(green); - dc.SetBrush(green_brush); - } - } - else - { - dc.SetPen(black); - dc.SetBrush(black_brush); - } - } - else - { - dc.SetPen(red); - dc.SetBrush(red_brush); - } - - // draw axis - const wxPoint pos - { - static_cast(middle.x + position.x * middle.x), - static_cast(middle.y - position.y * middle.y) - }; - dc.DrawCircle(pos.x, pos.y, 2); - - dc.SetBrush(*wxTRANSPARENT_BRUSH); -} - -void wxInputDraw::SetAxisValue(const glm::vec2& position) -{ - m_position = position; - Refresh(); -} - -void wxInputDraw::SetDeadzone(float deadzone) -{ - m_deadzone = deadzone; - Refresh(); -} diff --git a/src/gui/wxgui/components/wxInputDraw.h b/src/gui/wxgui/components/wxInputDraw.h deleted file mode 100644 index 477dd075..00000000 --- a/src/gui/wxgui/components/wxInputDraw.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "input/api/ControllerState.h" -#include "util/math/vector2.h" - -#include - - -class wxInputDraw : public wxWindow -{ -public: - wxInputDraw(wxWindow *parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize); - ~wxInputDraw(); - - virtual void OnRender(wxDC& dc); - void SetAxisValue(const glm::vec2& position); - void SetDeadzone(float deadzone); - -private: - void OnPaintEvent(wxPaintEvent& event); - - glm::vec2 m_position{}; - float m_deadzone{0}; -}; diff --git a/src/gui/wxgui/components/wxLogCtrl.cpp b/src/gui/wxgui/components/wxLogCtrl.cpp deleted file mode 100644 index 46ab9ad5..00000000 --- a/src/gui/wxgui/components/wxLogCtrl.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "wxgui/components/wxLogCtrl.h" - -#include - -wxDEFINE_EVENT(EVT_ON_LIST_UPDATED, wxEvent); - -wxLogCtrl::wxLogCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, bool alternateRowColours) - : TextList(parent, id, pos, size, style) -{ - m_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &wxLogCtrl::OnTimer, this); - this->Bind(EVT_ON_LIST_UPDATED, &wxLogCtrl::OnActiveListUpdated, this); - m_timer->Start(250); - - m_alternateRowColoursEnabled = alternateRowColours; -} - -wxLogCtrl::~wxLogCtrl() -{ - this->Unbind(wxEVT_TIMER, &wxLogCtrl::OnTimer, this); - this->Unbind(EVT_ON_LIST_UPDATED, &wxLogCtrl::OnActiveListUpdated, this); - - if(m_timer) - m_timer->Stop(); - - if(m_update_worker.joinable()) - m_update_worker.join(); -} - -void wxLogCtrl::SetActiveFilter(const std::string& active_filter) -{ - if(m_active_filter == active_filter) - return; - - m_active_filter = active_filter; - - if(m_update_worker.joinable()) - m_update_worker.join(); - - m_update_worker = std::thread(&wxLogCtrl::UpdateActiveEntries, this); -} - -const std::string& wxLogCtrl::GetActiveFilter() const -{ - return m_active_filter; -} - -void wxLogCtrl::SetFilterMessage(bool state) -{ - if(m_filter_messages == state) - return; - - m_filter_messages = state; - - if(m_update_worker.joinable()) - m_update_worker.join(); - - m_update_worker = std::thread(&wxLogCtrl::UpdateActiveEntries, this); -} - -bool wxLogCtrl::GetFilterMessage() const -{ - return m_filter_messages; -} - -void wxLogCtrl::PushEntry(const wxString& filter, const wxString& message) -{ - std::unique_lock lock(m_mutex); - m_log_entries.emplace_back(filter, message); - ListIt_t it = m_log_entries.back(); - lock.unlock(); - - if(m_active_filter.empty() || filter == m_active_filter || (m_filter_messages && boost::icontains(message.ToStdString(), m_active_filter))) - { - std::unique_lock active_lock(m_active_mutex); - m_active_entries.emplace_back(std::cref(it)); - const auto entry_count = m_active_entries.size(); - active_lock.unlock(); - - if(entry_count <= m_elements_visible) - { - RefreshLine(entry_count - 1); - } - } -} - -void wxLogCtrl::OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) -{ - wxPoint position = start_position; - - std::scoped_lock lock(m_active_mutex); - auto it = std::next(m_active_entries.cbegin(), start); - if(it == m_active_entries.cend()) - return; - - for (sint32 i = 0; i <= count && it != m_active_entries.cend(); ++i, ++it) - { - wxColour background_colour = GetBackgroundColour(); - if((start + i) % 2 != 0 && m_alternateRowColoursEnabled) - background_colour = GetAlternateRowColour(); - - DrawLineBackground(dc, position, background_colour); - - dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - dc.DrawText(it->get().second, position); - - NextLine(position, &start_position); - } -} - -void wxLogCtrl::OnTimer(wxTimerEvent& event) -{ - std::unique_lock lock(m_active_mutex); - const auto count = m_active_entries.size(); - if(count == m_element_count) - return; - - lock.unlock(); - SetElementCount(count); - - if(m_scrolled_to_end) - { - Scroll(0, count); - RefreshControl(); - } -} - -void wxLogCtrl::OnActiveListUpdated(wxEvent& event) -{ - std::unique_lock lock(m_active_mutex); - const auto count = m_active_entries.size(); - lock.unlock(); - - SetElementCount(count); - RefreshControl(); -} - -void wxLogCtrl::UpdateActiveEntries() -{ - { - std::scoped_lock lock(m_mutex, m_active_mutex); - m_active_entries.clear(); - if(m_active_filter.empty()) - { - for (const auto& it : m_log_entries) - m_active_entries.emplace_back(it); - } - else - { - for (const auto& it : m_log_entries) - { - if(it.first == m_active_filter || - (m_filter_messages && boost::icontains(it.second.ToStdString(), m_active_filter)) ) - m_active_entries.emplace_back(it); - } - } - } - - wxQueueEvent(this, new wxCommandEvent(EVT_ON_LIST_UPDATED)); -} - diff --git a/src/gui/wxgui/components/wxLogCtrl.h b/src/gui/wxgui/components/wxLogCtrl.h deleted file mode 100644 index e4593b8c..00000000 --- a/src/gui/wxgui/components/wxLogCtrl.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once -#include "TextList.h" - -class wxLogCtrl : public TextList -{ -public: - wxLogCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, bool alternateRowColours = false); - ~wxLogCtrl(); - - void SetActiveFilter(const std::string& active_filter); - [[nodiscard]] const std::string& GetActiveFilter() const; - void SetFilterMessage(bool state); - [[nodiscard]] bool GetFilterMessage() const; - - void PushEntry(const wxString& filter, const wxString& message); - -protected: - void OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position); - -private: - void OnTimer(wxTimerEvent& event); - void OnActiveListUpdated(wxEvent& event); - - wxTimer* m_timer; - - bool m_alternateRowColoursEnabled = false; - wxColour m_alternateRowColour; - - wxColour GetAlternateRowColour() - { - if (m_alternateRowColour.IsOk()) - return m_alternateRowColour; - - // Depending on the background, alternate row colour should be a bit - // darker or brighter. - const wxColour bgColour = GetBackgroundColour(); - int alpha = bgColour.GetRGB() > 0x808080 ? 97 : 110; - m_alternateRowColour = bgColour.ChangeLightness(alpha); - return m_alternateRowColour; - } - - std::string m_active_filter; - std::thread m_update_worker; - bool m_filter_messages = false; - - std::mutex m_mutex, m_active_mutex; - using ListEntry_t = std::pair; - using ListIt_t = std::list::const_reference; - std::list m_log_entries; - std::list> m_active_entries; - void UpdateActiveEntries(); -}; \ No newline at end of file diff --git a/src/gui/wxgui/components/wxProgressDialogManager.h b/src/gui/wxgui/components/wxProgressDialogManager.h deleted file mode 100644 index 93b6c316..00000000 --- a/src/gui/wxgui/components/wxProgressDialogManager.h +++ /dev/null @@ -1,118 +0,0 @@ -#pragma once - -#include -#include -#include -#include "util/helpers/Semaphore.h" - -wxDEFINE_EVENT(wxEVT_CREATE_PROGRESS_DIALOG, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_DESTROY_PROGRESS_DIALOG, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_UPDATE_PROGRESS_DIALOG, wxThreadEvent); - -// wrapper for wxGenericProgressDialog which can be used from any thread -class wxProgressDialogManager : public wxEvtHandler -{ -public: - wxProgressDialogManager(wxWindow* parent) : m_parent(parent), m_dialog(nullptr) - { - Bind(wxEVT_CREATE_PROGRESS_DIALOG, &wxProgressDialogManager::OnCreateProgressDialog, this); - Bind(wxEVT_DESTROY_PROGRESS_DIALOG, &wxProgressDialogManager::OnDestroyProgressDialog, this); - Bind(wxEVT_UPDATE_PROGRESS_DIALOG, &wxProgressDialogManager::OnUpdateProgressDialog, this); - } - - ~wxProgressDialogManager() - { - if (m_dialog) - Destroy(); - } - - void Create(const wxString& title, const wxString& message, int maximum, int style = wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME) - { - m_instanceSemaphore.increment(); - m_isCancelled = false; - m_isSkipped = false; - wxThreadEvent event(wxEVT_CREATE_PROGRESS_DIALOG); - event.SetString(title); - event.SetInt(maximum); - event.SetExtraLong(style); - wxQueueEvent(this, event.Clone()); - } - - void Destroy() - { - wxThreadEvent event(wxEVT_DESTROY_PROGRESS_DIALOG); - wxQueueEvent(this, event.Clone()); - m_instanceSemaphore.waitUntilZero(); // wait until destruction is complete - } - - // this also updates the cancel and skip state - void Update(int value, const wxString& newmsg = wxEmptyString) - { - wxThreadEvent event(wxEVT_UPDATE_PROGRESS_DIALOG); - event.SetInt(value); - event.SetString(newmsg); - wxQueueEvent(this, event.Clone()); - } - - bool IsCancelled() const - { - return m_isCancelled; - } - - bool IsSkipped() const - { - return m_isSkipped; - } - - bool IsCancelledOrSkipped() const - { - return m_isCancelled || m_isSkipped; - } - -private: - void OnCreateProgressDialog(wxThreadEvent& event) - { - if (m_dialog) - { - m_dialog->Destroy(); - m_instanceSemaphore.waitUntilZero(); - } - m_maximum = event.GetInt(); - m_dialog = new wxGenericProgressDialog(event.GetString(), "Please wait...", m_maximum, m_parent, event.GetExtraLong()); - } - - void OnDestroyProgressDialog(wxThreadEvent& event) - { - if (m_dialog) - { - m_dialog->Destroy(); - m_dialog = nullptr; - m_instanceSemaphore.decrement(); - } - } - - void OnUpdateProgressDialog(wxThreadEvent& event) - { - if (m_dialog) - { - // make sure that progress is never >= maximum - // because wxGenericProgressDialog seems to become crashy on destruction otherwise - int progress = event.GetInt(); - if(progress >= m_maximum) - progress = m_maximum - 1; - bool wasSkipped = false; - bool r = m_dialog->Update(progress, event.GetString(), &wasSkipped); - if(!r) - m_isCancelled = true; - if(wasSkipped) - m_isSkipped = true; - } - } - - wxWindow* m_parent; - wxGenericProgressDialog* m_dialog; - bool m_isCancelled{false}; - bool m_isSkipped{false}; - int m_maximum{0}; - CounterSemaphore m_instanceSemaphore; // used to synchronize destruction of the dialog -}; \ No newline at end of file diff --git a/src/gui/wxgui/components/wxTitleManagerList.cpp b/src/gui/wxgui/components/wxTitleManagerList.cpp deleted file mode 100644 index abca965c..00000000 --- a/src/gui/wxgui/components/wxTitleManagerList.cpp +++ /dev/null @@ -1,1333 +0,0 @@ -#include "wxgui/components/wxTitleManagerList.h" -#include "wxgui/helpers/wxHelpers.h" -#include "util/helpers/SystemException.h" -#include "Cafe/TitleList/GameInfo.h" -#include "Cafe/TitleList/TitleInfo.h" -#include "Cafe/TitleList/TitleList.h" -#include "wxgui/components/wxGameList.h" -#include "wxgui/helpers/wxCustomEvents.h" -#include "wxgui/helpers/wxHelpers.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../wxHelper.h" - -#include - -#include "config/ActiveSettings.h" -#include "wxgui/ChecksumTool.h" -#include "wxgui/MainWindow.h" -#include "Cafe/TitleList/TitleId.h" -#include "Cafe/TitleList/SaveList.h" -#include "Cafe/TitleList/TitleList.h" - -#include -#include - -#include "Common/FileStream.h" - -wxDEFINE_EVENT(wxEVT_TITLE_FOUND, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_TITLE_REMOVED, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_REMOVE_ENTRY, wxCommandEvent); - -wxTitleManagerList::wxTitleManagerList(wxWindow* parent, wxWindowID id) - : wxListView(parent, id, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL) -{ - AddColumns(); - - // tooltip TODO: extract class mb wxPanelTooltip - m_tooltip_window = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER); - auto* tooltip_sizer = new wxBoxSizer(wxVERTICAL); - m_tooltip_text = new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString); - tooltip_sizer->Add(m_tooltip_text , 0, wxALL, 5); - m_tooltip_window->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); - m_tooltip_window->SetSizerAndFit(tooltip_sizer); - m_tooltip_window->Hide(); - m_tooltip_timer = new wxTimer(this); - - Bind(wxEVT_LIST_COL_CLICK, &wxTitleManagerList::OnColumnClick, this); - Bind(wxEVT_CONTEXT_MENU, &wxTitleManagerList::OnContextMenu, this); - Bind(wxEVT_LIST_ITEM_SELECTED, &wxTitleManagerList::OnItemSelected, this); - Bind(wxEVT_TIMER, &wxTitleManagerList::OnTimer, this); - Bind(wxEVT_REMOVE_ITEM, &wxTitleManagerList::OnRemoveItem, this); - Bind(wxEVT_REMOVE_ENTRY, &wxTitleManagerList::OnRemoveEntry, this); - Bind(wxEVT_TITLE_FOUND, &wxTitleManagerList::OnTitleDiscovered, this); - Bind(wxEVT_TITLE_REMOVED, &wxTitleManagerList::OnTitleRemoved, this); - Bind(wxEVT_CLOSE_WINDOW, &wxTitleManagerList::OnClose, this); - - m_callbackIdTitleList = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { ((wxTitleManagerList*)ctx)->HandleTitleListCallback(evt); }, this); - m_callbackIdSaveList = CafeSaveList::RegisterCallback([](CafeSaveListCallbackEvent* evt, void* ctx) { ((wxTitleManagerList*)ctx)->HandleSaveListCallback(evt); }, this); - - ShowSortIndicator(ColumnTitleId); -} - -wxTitleManagerList::~wxTitleManagerList() -{ - CafeSaveList::UnregisterCallback(m_callbackIdSaveList); - CafeTitleList::UnregisterCallback(m_callbackIdTitleList); -} - -boost::optional wxTitleManagerList::GetSelectedTitleEntry() const -{ - const auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - const auto tmp = GetTitleEntry(selection); - if (tmp.has_value()) - return tmp.value(); - } - - return {}; -} - -boost::optional wxTitleManagerList::GetSelectedTitleEntry() -{ - const auto selection = GetFirstSelected(); - if (selection != wxNOT_FOUND) - { - const auto tmp = GetTitleEntry(selection); - if (tmp.has_value()) - return tmp.value(); - } - - return {}; -} - -//boost::optional wxTitleManagerList::GetTitleEntry(EntryType type, uint64 titleid) -//{ -// for(const auto& v : m_data) -// { -// if (v->entry.title_id == titleid && v->entry.type == type) -// return v->entry; -// } -// -// return {}; -//} - -boost::optional wxTitleManagerList::GetTitleEntryByUID(uint64 uid) -{ - for (const auto& v : m_data) - { - if (v->entry.location_uid == uid) - return v->entry; - } - return {}; -} - -void wxTitleManagerList::AddColumns() -{ - wxListItem col0; - col0.SetId(ColumnTitleId); - col0.SetText(_("Title ID")); - col0.SetWidth(120); - InsertColumn(ColumnTitleId, col0); - - wxListItem col1; - col1.SetId(ColumnName); - col1.SetText(_("Name")); - col1.SetWidth(435); - InsertColumn(ColumnName, col1); - - wxListItem col2; - col2.SetId(ColumnType); - col2.SetText(_("Type")); - col2.SetWidth(65); - InsertColumn(ColumnType, col2); - - wxListItem col3; - col3.SetId(ColumnVersion); - col3.SetText(_("Version")); - col3.SetWidth(40); - InsertColumn(ColumnVersion, col3); - - wxListItem col4; - col4.SetId(ColumnRegion); - col4.SetText(_("Region")); - col4.SetWidth(60); - InsertColumn(ColumnRegion, col4); - - wxListItem col5; - col5.SetId(ColumnFormat); - col5.SetText(_("Format")); - col5.SetWidth(63); - InsertColumn(ColumnFormat, col5); - - wxListItem col6; - col6.SetId(ColumnLocation); - col6.SetText(_("Location")); - col6.SetWidth(63); - InsertColumn(ColumnLocation, col6); -} - -wxString wxTitleManagerList::OnGetItemText(long item, long column) const -{ - if (item >= GetItemCount()) - return wxEmptyString; - - const auto entry = GetTitleEntry(item); - if (entry.has_value()) - return GetTitleEntryText(entry.value(), (ItemColumn)column); - - return wxEmptyString; -} - -wxItemAttr* wxTitleManagerList::OnGetItemAttr(long item) const -{ - static wxColour bgColourPrimary = GetBackgroundColour(); - static wxColour bgColourSecondary = wxHelper::CalculateAccentColour(bgColourPrimary); - static wxListItemAttr s_primary_attr(GetTextColour(), bgColourPrimary, GetFont()); - static wxListItemAttr s_secondary_attr(GetTextColour(), bgColourSecondary, GetFont()); - return item % 2 == 0 ? &s_primary_attr : &s_secondary_attr; -} - -boost::optional wxTitleManagerList::GetTitleEntry(long item) -{ - long counter = 0; - for (const auto& data : m_sorted_data) - { - if (!data.get().visible) - continue; - - if (item != counter++) - continue; - - return data.get().entry; - } - - return {}; -} - -boost::optional wxTitleManagerList::GetTitleEntry(const fs::path& path) const -{ - const auto tmp = _pathToUtf8(path); - for (const auto& data : m_data) - { - if (boost::iequals(_pathToUtf8(data->entry.path), tmp)) - return data->entry; - } - - return {}; -} -boost::optional wxTitleManagerList::GetTitleEntry(const fs::path& path) -{ - const auto tmp = _pathToUtf8(path); - for (const auto& data : m_data) - { - if (boost::iequals(_pathToUtf8(data->entry.path), tmp)) - return data->entry; - } - - return {}; -} - -boost::optional wxTitleManagerList::GetTitleEntry(long item) const -{ - long counter = 0; - for (const auto& data : m_sorted_data) - { - if (!data.get().visible) - continue; - - if (item != counter++) - continue; - - return data.get().entry; - } - - return {}; -} - -void wxTitleManagerList::OnConvertToCompressedFormat(uint64 titleId, uint64 rightClickedUID) -{ - TitleInfo titleInfo_base; - TitleInfo titleInfo_update; - TitleInfo titleInfo_aoc; - - titleId = TitleIdParser::MakeBaseTitleId(titleId); // if the titleId of a separate update is selected, this converts it back to the base titleId - TitleIdParser titleIdParser(titleId); - bool hasBaseTitleId = titleIdParser.GetType() != TitleIdParser::TITLE_TYPE::AOC; - bool hasUpdateTitleId = titleIdParser.CanHaveSeparateUpdateTitleId(); - TitleId updateTitleId = hasUpdateTitleId ? titleIdParser.GetSeparateUpdateTitleId() : 0; - - // todo - AOC titleIds might differ from the base/update game in other bits than the type. We have to use the meta data from the base/update to match aoc to the base title id - // for now we just assume they match - TitleId aocTitleId; - if (hasBaseTitleId) - aocTitleId = (titleId & (uint64)~0xFF00000000) | (uint64)0xC00000000; - else - aocTitleId = titleId; - - // find base and update - for (const auto& data : m_data) - { - if (hasBaseTitleId && data->entry.title_id == titleId) - { - if (!titleInfo_base.IsValid()) - { - titleInfo_base = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); - if(data->entry.location_uid == rightClickedUID) - break; // prefer the users selection - } - } - } - for (const auto& data : m_data) - { - if (hasUpdateTitleId && data->entry.title_id == updateTitleId) - { - if (!titleInfo_update.IsValid()) - { - titleInfo_update = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); - if(data->entry.location_uid == rightClickedUID) - break; - } - else - { - // if multiple updates are present use the newest one - if (titleInfo_update.GetAppTitleVersion() < data->entry.version) - titleInfo_update = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); - if(data->entry.location_uid == rightClickedUID) - break; - } - } - } - // find AOC - for (const auto& data : m_data) - { - if (data->entry.title_id == aocTitleId) - { - titleInfo_aoc = CafeTitleList::GetTitleInfoByUID(data->entry.location_uid); - if(data->entry.location_uid == rightClickedUID) - break; - } - } - - wxString msg = _("The following content will be converted to a compressed Wii U archive file (.wua):"); - msg.append("\n \n"); - - if (titleInfo_base.IsValid()) - msg.append(formatWxString(_("Base game:\n{}"), titleInfo_base.GetPrintPath())); - else - msg.append(_("Base game:\nNot installed")); - - msg.append("\n\n"); - - if (titleInfo_update.IsValid()) - msg.append(formatWxString(_("Update:\n{}"), titleInfo_update.GetPrintPath())); - else - msg.append(_("Update:\nNot installed")); - - msg.append("\n\n"); - - if (titleInfo_aoc.IsValid()) - msg.append(formatWxString(_("DLC:\n{}"), titleInfo_aoc.GetPrintPath())); - else - msg.append(_("DLC:\nNot installed")); - - const int answer = wxMessageBox(msg, _("Confirmation"), wxOK | wxCANCEL | wxCENTRE | wxICON_QUESTION, this); - if (answer != wxOK) - return; - std::vector titlesToConvert; - if (titleInfo_base.IsValid()) - titlesToConvert.emplace_back(&titleInfo_base); - if (titleInfo_update.IsValid()) - titlesToConvert.emplace_back(&titleInfo_update); - if (titleInfo_aoc.IsValid()) - titlesToConvert.emplace_back(&titleInfo_aoc); - if (titlesToConvert.empty()) - return; - // get short name - CafeConsoleLanguage languageId = CafeConsoleLanguage::EN; // todo - use user's locale - std::string shortName; - if (titleInfo_base.IsValid()) - shortName = titleInfo_base.GetMetaInfo()->GetShortName(languageId); - else if (titleInfo_update.IsValid()) - shortName = titleInfo_update.GetMetaInfo()->GetShortName(languageId); - else if (titleInfo_aoc.IsValid()) - shortName = titleInfo_aoc.GetMetaInfo()->GetShortName(languageId); - - if (!shortName.empty()) - { - boost::replace_all(shortName, ":", ""); - } - // for the default output directory we use the first game path configured by the user - std::string defaultDir = ""; - if (!GetConfig().game_paths.empty()) - defaultDir = GetConfig().game_paths.front(); - // get the short name, which we will use as a suggested default file name - std::string defaultFileName = std::move(shortName); - boost::replace_all(defaultFileName, "/", ""); - boost::replace_all(defaultFileName, "\\", ""); - - CafeConsoleRegion region = CafeConsoleRegion::Auto; - if (titleInfo_base.IsValid() && titleInfo_base.HasValidXmlInfo()) - region = titleInfo_base.GetMetaInfo()->GetRegion(); - else if (titleInfo_update.IsValid() && titleInfo_update.HasValidXmlInfo()) - region = titleInfo_update.GetMetaInfo()->GetRegion(); - - if (region == CafeConsoleRegion::JPN) - defaultFileName.append(" (JP)"); - else if (region == CafeConsoleRegion::EUR) - defaultFileName.append(" (EU)"); - else if (region == CafeConsoleRegion::USA) - defaultFileName.append(" (US)"); - if (titleInfo_update.IsValid()) - { - defaultFileName.append(fmt::format(" (v{})", titleInfo_update.GetAppTitleVersion())); - } - defaultFileName.append(".wua"); - - // ask the user to provide a path for the output file - wxFileDialog saveFileDialog(this, _("Save Wii U game archive file"), defaultDir, wxString::FromUTF8(defaultFileName), - "WUA files (*.wua)|*.wua", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (saveFileDialog.ShowModal() == wxID_CANCEL || saveFileDialog.GetPath().IsEmpty()) - return; - fs::path outputPath(wxHelper::MakeFSPath(saveFileDialog.GetPath())); - fs::path outputPathTmp(wxHelper::MakeFSPath(saveFileDialog.GetPath().append("__tmp"))); - struct ZArchiveWriterContext - { - static void NewOutputFile(const int32_t partIndex, void* _ctx) - { - ZArchiveWriterContext* ctx = (ZArchiveWriterContext*)_ctx; - ctx->fs = FileStream::createFile2(ctx->outputPath); - if (!ctx->fs) - ctx->isValid = false; - } - - static void WriteOutputData(const void* data, size_t length, void* _ctx) - { - ZArchiveWriterContext* ctx = (ZArchiveWriterContext*)_ctx; - if (ctx->fs) - ctx->fs->writeData(data, length); - } - - bool RecursivelyCountFiles(const std::string& fscPath) - { - sint32 fscStatus; - std::unique_ptr vfDir(fsc_openDirIterator(fscPath.c_str(), &fscStatus)); - if (!vfDir) - return false; - if (cancelled) - return false; - FSCDirEntry dirEntry; - while (fsc_nextDir(vfDir.get(), &dirEntry)) - { - if (dirEntry.isFile) - { - totalInputFileSize += (uint64)dirEntry.fileSize; - totalFileCount++; - } - else if (dirEntry.isDirectory) - { - if (!RecursivelyCountFiles(fmt::format("{}{}/", fscPath, dirEntry.path))) - { - return false; - } - } - else - cemu_assert_unimplemented(); - } - return true; - } - - bool RecursivelyAddFiles(std::string archivePath, std::string fscPath) - { - sint32 fscStatus; - std::unique_ptr vfDir(fsc_openDirIterator(fscPath.c_str(), &fscStatus)); - if (!vfDir) - return false; - if (cancelled) - return false; - zaWriter->MakeDir(archivePath.c_str(), false); - FSCDirEntry dirEntry; - while (fsc_nextDir(vfDir.get(), &dirEntry)) - { - if (dirEntry.isFile) - { - zaWriter->StartNewFile((archivePath + dirEntry.path).c_str()); - std::unique_ptr vFile(fsc_open((fscPath + dirEntry.path).c_str(), FSC_ACCESS_FLAG::OPEN_FILE | FSC_ACCESS_FLAG::READ_PERMISSION, &fscStatus)); - if (!vFile) - return false; - transferBuffer.resize(32 * 1024); // 32KB - uint32 readBytes; - while (true) - { - readBytes = vFile->fscReadData(transferBuffer.data(), transferBuffer.size()); - if(readBytes == 0) - break; - zaWriter->AppendData(transferBuffer.data(), readBytes); - if (cancelled) - return false; - transferredInputBytes += readBytes; - } - currentFileIndex++; - } - else if (dirEntry.isDirectory) - { - if (!RecursivelyAddFiles(fmt::format("{}{}/", archivePath, dirEntry.path), fmt::format("{}{}/", fscPath, dirEntry.path))) - return false; - } - else - cemu_assert_unimplemented(); - } - return true; - } - - bool LoadTitleMetaAndCountFiles(TitleInfo* titleInfo) - { - std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath(); - titleInfo->Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE); - bool r = RecursivelyCountFiles(temporaryMountPath); - titleInfo->Unmount(temporaryMountPath.c_str()); - return r; - } - - bool StoreTitle(TitleInfo* titleInfo) - { - std::string temporaryMountPath = TitleInfo::GetUniqueTempMountingPath(); - titleInfo->Mount(temporaryMountPath.c_str(), "", FSC_PRIORITY_BASE); - bool r = RecursivelyAddFiles(fmt::format("{:016x}_v{}/", titleInfo->GetAppTitleId(), titleInfo->GetAppTitleVersion()), temporaryMountPath); - titleInfo->Unmount(temporaryMountPath.c_str()); - return r; - } - - bool AddTitles(TitleInfo** titles, size_t count) - { - currentFileIndex = 0; - totalFileCount = 0; - // count files - for (size_t i = 0; i < count; i++) - { - if (!LoadTitleMetaAndCountFiles(titles[i])) - return false; - if (cancelled) - return false; - } - // store files - for (size_t i = 0; i < count; i++) - { - if (!StoreTitle(titles[i])) - return false; - } - return true; - } - - ~ZArchiveWriterContext() - { - delete fs; - delete zaWriter; - }; - - fs::path outputPath; - FileStream* fs; - ZArchiveWriter* zaWriter{}; - bool isValid{false}; - std::vector transferBuffer; - std::atomic_bool cancelled{false}; - // progress - std::atomic_uint32_t totalFileCount{}; - std::atomic_uint32_t currentFileIndex{}; - std::atomic_uint64_t totalInputFileSize{}; - std::atomic_uint64_t transferredInputBytes{}; - }writerContext; - - // mount and store - writerContext.isValid = true; - writerContext.outputPath = outputPathTmp; - writerContext.zaWriter = new ZArchiveWriter(&ZArchiveWriterContext::NewOutputFile, &ZArchiveWriterContext::WriteOutputData, &writerContext); - if (!writerContext.isValid) - { - // failed to create file - wxMessageBox(_("Unable to create file"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - // open progress dialog - wxGenericProgressDialog progressDialog("Converting to .wua", - _("Counting files..."), - 100, // range - this, // parent - wxPD_CAN_ABORT - ); - progressDialog.Show(); - - auto asyncWorker = std::async(std::launch::async, &ZArchiveWriterContext::AddTitles, &writerContext, titlesToConvert.data(), titlesToConvert.size()); - while (!future_is_ready(asyncWorker)) - { - if (writerContext.cancelled) - { - progressDialog.Update(0, _("Stopping...")); - } - else if (writerContext.currentFileIndex != 0) - { - uint64 numSizeCompleted = writerContext.transferredInputBytes; - uint64 numSizeTotal = writerContext.totalInputFileSize; - uint32 pct = (uint32)(numSizeCompleted * (uint64)100 / numSizeTotal); - pct = std::min(pct, (uint32)100); - if (pct >= 100) - pct = 99; // never set it to 100 as progress == total will make .Update() call ShowModal() and lock up this loop - std::string textSuffix = fmt::format(" ({}MiB/{}MiB)", numSizeCompleted / 1024 / 1024, numSizeTotal / 1024 / 1024); - progressDialog.Update(pct, _("Converting files...") + textSuffix); - } - else - { - progressDialog.Update(0, _("Collecting list of files...") + fmt::format(" ({})", writerContext.totalFileCount.load())); - } - if (progressDialog.WasCancelled()) - writerContext.cancelled.store(true); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - progressDialog.Update(100-1, _("Finalizing...")); - bool r = asyncWorker.get(); - if (!r) - { - delete writerContext.fs; - writerContext.fs = nullptr; - std::error_code ec; - fs::remove(outputPathTmp, ec); - return; - } - writerContext.zaWriter->Finalize(); - delete writerContext.fs; - writerContext.fs = nullptr; - // verify the created WUA file - ZArchiveReader* zreader = ZArchiveReader::OpenFromFile(outputPathTmp); - if (!zreader) - { - wxMessageBox(_("Conversion failed\n"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - std::error_code ec; - fs::remove(outputPathTmp, ec); - return; - } - // todo - do a quick verification here - delete zreader; - // finish - progressDialog.Hide(); - fs::rename(outputPathTmp, outputPath); - - // ask user if they want to delete the original titles - // todo - - - CafeTitleList::Refresh(); - wxMessageBox(_("Conversion finished\n"), _("Complete"), wxOK | wxCENTRE | wxICON_INFORMATION, this); -} - -void wxTitleManagerList::OnClose(wxCloseEvent& event) -{ - event.Skip(); - // wait until all tasks are complete - if (m_context_worker.valid()) - m_context_worker.get(); -} - -void wxTitleManagerList::OnColumnClick(wxListEvent& event) -{ - const int column = event.GetColumn(); - SortEntries(column); - event.Skip(); -} - -void wxTitleManagerList::RemoveItem(long item) -{ - const int item_count = GetItemCount(); - - const ItemData* ref = nullptr; - long counter = 0; - for(auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) - { - if (!it->get().visible) - continue; - - if (item != counter++) - continue; - - ref = &(it->get()); - m_sorted_data.erase(it); - break; - } - - // shouldn't happen - if (ref == nullptr) - return; - - for(auto it = m_data.begin(); it != m_data.end(); ++it) - { - if (ref != (*it).get()) - continue; - - m_data.erase(it); - break; - } - - SetItemCount(std::max(0, item_count - 1)); - RefreshPage(); -} - -void wxTitleManagerList::RemoveItem(const TitleEntry& entry) -{ - const int item_count = GetItemCount(); - - const TitleEntry* ref = &entry; - for (auto it = m_sorted_data.begin(); it != m_sorted_data.end(); ++it) - { - if (ref != &it->get().entry) - continue; - - m_sorted_data.erase(it); - break; - } - - for (auto it = m_data.begin(); it != m_data.end(); ++it) - { - if (ref != &(*it).get()->entry) - continue; - - m_data.erase(it); - break; - } - - SetItemCount(std::max(0, item_count - 1)); - RefreshPage(); -} - -void wxTitleManagerList::OnItemSelected(wxListEvent& event) -{ - event.Skip(); - m_tooltip_timer->Stop(); - const auto selection = event.GetIndex(); - - if (selection == wxNOT_FOUND) - { - m_tooltip_window->Hide(); - return; - } - - const auto entry = GetTitleEntry(selection); - if (!entry.has_value()) - { - m_tooltip_window->Hide(); - return; - } - - m_tooltip_window->Hide(); - return; - - //const auto mouse_position = wxGetMousePosition() - GetScreenPosition(); - //m_tooltip_window->SetPosition(wxPoint(mouse_position.x + 15, mouse_position.y + 15)); - - //wxString msg; - //switch(entry->error) - //{ - //case TitleError::WrongBaseLocation: - // msg = _("This base game is installed at the wrong location."); - // break; - //case TitleError::WrongUpdateLocation: - // msg = _("This update is installed at the wrong location."); - // break; - //case TitleError::WrongDlcLocation: - // msg = _("This DLC is installed at the wrong location."); - // break; - //default: - // return;; - //} - - //m_tooltip_text->SetLabel(formatWxString("{}\n{}", msg, _("You can use the context menu to fix it."))); - //m_tooltip_window->Fit(); - //m_tooltip_timer->StartOnce(250); -} - -enum ContextMenuEntries -{ - kContextMenuOpenDirectory = wxID_HIGHEST + 1, - kContextMenuDelete, - kContextMenuLaunch, - kContextMenuVerifyGameFiles, - kContextMenuConvertToWUA, -}; -void wxTitleManagerList::OnContextMenu(wxContextMenuEvent& event) -{ - // still doing work - if (m_context_worker.valid() && !future_is_ready(m_context_worker)) - return; - - wxMenu menu; - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &wxTitleManagerList::OnContextMenuSelected, this); - - const auto selection = GetFirstSelected(); - if (selection == wxNOT_FOUND) - return; - - const auto entry = GetTitleEntry(selection); - if (!entry.has_value()) - return; - - if(entry->type == EntryType::Base) - menu.Append(kContextMenuLaunch, _("&Launch title")); - - menu.Append(kContextMenuOpenDirectory, _("&Open directory")); - if (entry->type != EntryType::Save) - menu.Append(kContextMenuVerifyGameFiles, _("&Verify integrity of game files")); - - menu.AppendSeparator(); - - if (entry->type != EntryType::Save && entry->format != EntryFormat::WUA) - { - menu.Append(kContextMenuConvertToWUA, _("Convert to compressed Wii U archive (.wua)")); - - menu.AppendSeparator(); - } - menu.Append(kContextMenuDelete, _("&Delete")); - - PopupMenu(&menu); - - // TODO: fix tooltip position -} - -bool wxTitleManagerList::DeleteEntry(long index, const TitleEntry& entry) -{ - wxDTorFunc reset_text(wxQueueEvent, this, new wxSetStatusBarTextEvent(wxEmptyString, 1)); - wxQueueEvent(this, new wxSetStatusBarTextEvent("Deleting entry...", 1)); - - wxString msg; - const bool is_directory = fs::is_directory(entry.path); - if(is_directory) - msg = formatWxString(_("Are you really sure that you want to delete the following folder:\n{}"), _pathToUtf8(entry.path)); - else - msg = formatWxString(_("Are you really sure that you want to delete the following file:\n{}"), _pathToUtf8(entry.path)); - - const auto result = wxMessageBox(msg, _("Warning"), wxYES_NO | wxCENTRE | wxICON_EXCLAMATION, this); - if (result == wxNO) - return false; - - std::error_code ec; - if (is_directory) - { - if (entry.type != EntryType::Save) - { - // delete content, meta, code folders first - const auto content = entry.path / "content"; - fs::remove_all(content, ec); - - const auto meta = entry.path / "meta"; - fs::remove_all(meta, ec); - - const auto code = entry.path / "code"; - fs::remove_all(code, ec); - } - else - { - // delete meta, user folders first - const auto meta = entry.path / "meta"; - fs::remove_all(meta, ec); - - const auto user = entry.path / "user"; - fs::remove_all(user, ec); - } - - - // check if folder is empty - if(fs::is_empty(entry.path, ec)) - fs::remove_all(entry.path, ec); - } - else // simply remove file - fs::remove(entry.path, ec); - - if(ec) - { - const auto error_msg = formatWxString(_("Error when trying to delete the entry:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK|wxCENTRE, this); - return false; - } - - // thread safe request to delete entry - const auto evt = new wxCommandEvent(wxEVT_REMOVE_ITEM); - evt->SetInt(index); - wxQueueEvent(this, evt); - return true; -} - -void wxTitleManagerList::OnContextMenuSelected(wxCommandEvent& event) -{ - // still doing work - if (m_context_worker.valid() && !future_is_ready(m_context_worker)) - return; - - const auto selection = GetFirstSelected(); - if (selection == wxNOT_FOUND) - return; - - const auto entry = GetTitleEntry(selection); - if (!entry.has_value()) - return; - - switch (event.GetId()) - { - case kContextMenuOpenDirectory: - { - const auto path = fs::is_directory(entry->path) ? entry->path : entry->path.parent_path(); - wxLaunchDefaultApplication(wxHelper::FromPath(path)); - } - break; - case kContextMenuDelete: - m_context_worker = std::async(std::launch::async, &wxTitleManagerList::DeleteEntry, this, selection, entry.value()); - break; - case kContextMenuLaunch: - { - try - { - MainWindow::RequestLaunchGame(entry->path, wxLaunchGameEvent::INITIATED_BY::TITLE_MANAGER); - Close(); - } - catch (const std::exception& ex) - { - cemuLog_log(LogType::Force, "wxTitleManagerList::OnContextMenuSelected: can't launch title: {}", ex.what()); - } - } - break; - case kContextMenuVerifyGameFiles: - (new ChecksumTool(this, entry.value()))->Show(); - break; - case kContextMenuConvertToWUA: - - OnConvertToCompressedFormat(entry.value().title_id, entry.value().location_uid); - break; - } -} - -void wxTitleManagerList::OnTimer(wxTimerEvent& event) -{ - if(event.GetTimer().GetId() != m_tooltip_timer->GetId()) - { - event.Skip(); - return; - } - - m_tooltip_window->Show(); -} - -void wxTitleManagerList::OnRemoveItem(wxCommandEvent& event) -{ - RemoveItem(event.GetInt()); -} - -void wxTitleManagerList::OnRemoveEntry(wxCommandEvent& event) -{ - wxASSERT(event.GetClientData() != nullptr); - RemoveItem(*(TitleEntry*)event.GetClientData()); -} - -wxString wxTitleManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColumn column) -{ - switch (column) - { - case ColumnTitleId: - return formatWxString("{:08x}-{:08x}", (uint32) (entry.title_id >> 32), (uint32) (entry.title_id & 0xFFFFFFFF)); - case ColumnName: - return entry.name; - case ColumnType: - return GetTranslatedTitleEntryType(entry.type); - case ColumnVersion: - return formatWxString("{}", entry.version); - case ColumnRegion: - return wxGetTranslation(fmt::format("{}", entry.region)); - case ColumnFormat: - { - if (entry.type == EntryType::Save) - return _("Save folder"); - switch (entry.format) - { - case wxTitleManagerList::EntryFormat::Folder: - return _("Folder"); - case wxTitleManagerList::EntryFormat::WUD: - return _("WUD"); - case wxTitleManagerList::EntryFormat::NUS: - return _("NUS"); - case wxTitleManagerList::EntryFormat::WUA: - return _("WUA"); - case wxTitleManagerList::EntryFormat::WUHB: - return _("WUHB"); - } - return ""; - } - case ColumnLocation: - { - const auto relative_mlc_path = _pathToUtf8(entry.path.lexically_relative(ActiveSettings::GetMlcPath())); - if (relative_mlc_path.starts_with("usr") || relative_mlc_path.starts_with("sys")) - return _("MLC"); - else - return _("Game Paths"); - } - default: - UNREACHABLE; - } - - return wxEmptyString; -} - -wxString wxTitleManagerList::GetTranslatedTitleEntryType(EntryType type) -{ - switch (type) - { - case EntryType::Base: - return _("base"); - case EntryType::Update: - return _("update"); - case EntryType::Dlc: - return _("DLC"); - case EntryType::Save: - return _("save"); - case EntryType::System: - return _("system"); - default: - return std::to_string(static_cast>(type)); - } -} - -void wxTitleManagerList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt) -{ - if (evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED && - evt->eventType != CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) - return; - - auto& titleInfo = *evt->titleInfo; - wxTitleManagerList::EntryType entryType; - switch (titleInfo.GetTitleType()) - { - case TitleIdParser::TITLE_TYPE::BASE_TITLE_UPDATE: - entryType = EntryType::Update; - break; - case TitleIdParser::TITLE_TYPE::AOC: - entryType = EntryType::Dlc; - break; - case TitleIdParser::TITLE_TYPE::SYSTEM_DATA: - case TitleIdParser::TITLE_TYPE::SYSTEM_OVERLAY_TITLE: - case TitleIdParser::TITLE_TYPE::SYSTEM_TITLE: - entryType = EntryType::System; - break; - default: - entryType = EntryType::Base; - } - - wxTitleManagerList::EntryFormat entryFormat; - switch (titleInfo.GetFormat()) - { - case TitleInfo::TitleDataFormat::WUD: - entryFormat = EntryFormat::WUD; - break; - case TitleInfo::TitleDataFormat::NUS: - entryFormat = EntryFormat::NUS; - break; - case TitleInfo::TitleDataFormat::WIIU_ARCHIVE: - entryFormat = EntryFormat::WUA; - break; - case TitleInfo::TitleDataFormat::WUHB: - entryFormat = EntryFormat::WUHB; - break; - case TitleInfo::TitleDataFormat::HOST_FS: - default: - entryFormat = EntryFormat::Folder; - break; - } - - if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_DISCOVERED) - { - if (titleInfo.IsCached()) - return; // the title list only displays non-cached entries - wxTitleManagerList::TitleEntry entry(entryType, entryFormat, titleInfo.GetPath()); - - ParsedMetaXml* metaInfo = titleInfo.GetMetaInfo(); - if(titleInfo.IsSystemDataTitle()) - return; // dont show system data titles for now - entry.location_uid = titleInfo.GetUID(); - entry.title_id = titleInfo.GetAppTitleId(); - std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); - const auto nl = name.find(L'\n'); - if (nl != std::string::npos) - name.replace(nl, 1, " - "); - entry.name = wxString::FromUTF8(name); - entry.version = titleInfo.GetAppTitleVersion(); - entry.region = metaInfo->GetRegion(); - - auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_FOUND); - cmdEvt->SetClientObject(new wxCustomData(entry)); - wxQueueEvent(this, cmdEvt); - } - else if (evt->eventType == CafeTitleListCallbackEvent::TYPE::TITLE_REMOVED) - { - wxTitleManagerList::TitleEntry entry(entryType, entryFormat, titleInfo.GetPath()); - entry.location_uid = titleInfo.GetUID(); - entry.title_id = titleInfo.GetAppTitleId(); - - auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_REMOVED); - cmdEvt->SetClientObject(new wxCustomData(entry)); - wxQueueEvent(this, cmdEvt); - } -} - -void wxTitleManagerList::HandleSaveListCallback(struct CafeSaveListCallbackEvent* evt) -{ - if (evt->eventType == CafeSaveListCallbackEvent::TYPE::SAVE_DISCOVERED) - { - ParsedMetaXml* metaInfo = evt->saveInfo->GetMetaInfo(); - if (!metaInfo) - return; - auto& saveInfo = *evt->saveInfo; - wxTitleManagerList::TitleEntry entry(EntryType::Save, EntryFormat::Folder, saveInfo.GetPath()); - entry.location_uid = std::hash() ( metaInfo->GetTitleId() ); - entry.title_id = metaInfo->GetTitleId(); - std::string name = metaInfo->GetLongName(GetConfig().console_language.GetValue()); - const auto nl = name.find(L'\n'); - if (nl != std::string::npos) - name.replace(nl, 1, " - "); - entry.name = wxString::FromUTF8(name); - entry.version = metaInfo->GetTitleVersion(); - entry.region = metaInfo->GetRegion(); - - auto* cmdEvt = new wxCommandEvent(wxEVT_TITLE_FOUND); - cmdEvt->SetClientObject(new wxCustomData(entry)); - wxQueueEvent(this, cmdEvt); - } -} - -void wxTitleManagerList::OnTitleDiscovered(wxCommandEvent& event) -{ - auto* obj = dynamic_cast(event.GetClientObject()); - wxASSERT(obj); - AddTitle(obj); -} - -void wxTitleManagerList::OnTitleRemoved(wxCommandEvent& event) -{ - auto* obj = dynamic_cast(event.GetClientObject()); - wxASSERT(obj); - for (auto& itr : m_data) - { - if (itr.get()->entry.location_uid == obj->get().location_uid) - { - RemoveItem(itr.get()->entry); - break; - } - } -} - -void wxTitleManagerList::AddTitle(TitleEntryData_t* obj) -{ - const auto& data = obj->GetData(); - if (GetTitleEntryByUID(data.location_uid).has_value()) - return; // already in list - m_data.emplace_back(std::make_unique(true, data)); - m_sorted_data.emplace_back(*m_data[m_data.size() - 1]); - SetItemCount(m_data.size()); -} - -int wxTitleManagerList::AddImage(const wxImage& image) const -{ - return -1; // m_image_list->Add(image.Scale(kListIconWidth, kListIconWidth, wxIMAGE_QUALITY_BICUBIC)); -} - -// return < -bool wxTitleManagerList::SortFunc(int column, const Type_t& v1, const Type_t& v2) -{ - // last sort option - if (column == -1) - return v1.get().entry.path.compare(v2.get().entry.path) < 0; - - // visible have always priority - if (!v1.get().visible && v2.get().visible) - return false; - else if (v1.get().visible && !v2.get().visible) - return true; - - const auto& entry1 = v1.get().entry; - const auto& entry2 = v2.get().entry; - - // check column: title id -> type -> path - if (column == ColumnTitleId) - { - // ensure strong ordering -> use type since only one entry should be now (should be changed if every save for every user is displayed separately?) - if (entry1.title_id == entry2.title_id) - return SortFunc(ColumnType, v1, v2); - - return entry1.title_id < entry2.title_id; - } - else if (column == ColumnName) - { - const int tmp = entry1.name.CmpNoCase(entry2.name); - if(tmp == 0) - return SortFunc(ColumnTitleId, v1, v2); - - return tmp < 0; - } - else if (column == ColumnType) - { - if(entry1.type == entry2.type) - return SortFunc(-1, v1, v2); - - return std::underlying_type_t(entry1.type) < std::underlying_type_t(entry2.type); - } - else if (column == ColumnVersion) - { - if(entry1.version == entry2.version) - return SortFunc(ColumnTitleId, v1, v2); - - return entry1.version < entry2.version; - } - else if (column == ColumnRegion) - { - if(entry1.region == entry2.region) - return SortFunc(ColumnTitleId, v1, v2); - - return std::underlying_type_t(entry1.region) < std::underlying_type_t(entry2.region); - } - else if (column == ColumnFormat) - { - if(entry1.format == entry2.format) - return SortFunc(ColumnType, v1, v2); - - return std::underlying_type_t(entry1.format) < std::underlying_type_t(entry2.format); - } - - return false; -} -void wxTitleManagerList::SortEntries(int column) -{ - bool ascending; - if (column == -1) - { - column = GetSortIndicator(); - if (column == -1) - column = ColumnTitleId; - ascending = IsAscendingSortIndicator(); - } - else - ascending = GetUpdatedAscendingSortIndicator(column); - - if (column != ColumnTitleId && column != ColumnName && column != ColumnType && column != ColumnVersion && column != ColumnRegion && column != ColumnFormat) - return; - - std::sort(m_sorted_data.begin(), m_sorted_data.end(), - [this, column, ascending](const Type_t& v1, const Type_t& v2) -> bool { - return ascending ? SortFunc(column, v1, v2) : SortFunc(column, v2, v1); - }); - - ShowSortIndicator(column, ascending); - RefreshPage(); -} - -void wxTitleManagerList::RefreshPage() -{ - long item_count = GetItemCount(); - - if (item_count > 0) - RefreshItems(GetTopItem(), std::min(item_count - 1, GetTopItem() + GetCountPerPage() + 1)); -} - -int wxTitleManagerList::Filter(const wxString& filter, const wxString& prefix, ItemColumn column) -{ - if (prefix.empty()) - return -1; - - if (!filter.StartsWith(prefix)) - return -1; - - int counter = 0; - const auto tmp_filter = filter.substr(prefix.size() - 1).Trim(false); - for (auto&& data : m_data) - { - if (GetTitleEntryText(data->entry, column).Upper().Contains(tmp_filter)) - { - data->visible = true; - ++counter; - } - else - data->visible = false; - } - return counter; -} - -void wxTitleManagerList::Filter(const wxString& filter) -{ - if(filter.empty()) - { - std::for_each(m_data.begin(), m_data.end(), [](ItemDataPtr& data) { data->visible = true; }); - SetItemCount(m_data.size()); - RefreshPage(); - return; - } - - const auto filter_upper = filter.Upper().Trim(false).Trim(true); - int counter = 0; - - if (const auto result = Filter(filter_upper, "TITLEID:", ColumnTitleId) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "NAME:", ColumnName) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "TYPE:", ColumnType) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "REGION:", ColumnRegion) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "VERSION:", ColumnVersion) != -1) - counter = result; - else if (const auto result = Filter(filter_upper, "FORMAT:", ColumnFormat) != -1) - counter = result; - else if(filter_upper == "ERROR") - { - for (auto&& data : m_data) - { - bool visible = true; - data->visible = visible; - if (visible) - ++counter; - } - } - else - { - for (auto&& data : m_data) - { - bool visible = false; - if (data->entry.name.Upper().Contains(filter_upper)) - visible = true; - else if (GetTitleEntryText(data->entry, ColumnTitleId).Upper().Contains(filter_upper)) - visible = true; - else if (GetTitleEntryText(data->entry, ColumnType).Upper().Contains(filter_upper)) - visible = true; - - data->visible = visible; - if (visible) - ++counter; - } - } - - SetItemCount(counter); - RefreshPage(); -} - -size_t wxTitleManagerList::GetCountByType(EntryType type) const -{ - size_t result = 0; - for(const auto& data : m_data) - { - if (data->entry.type == type) - ++result; - } - return result; -} - -void wxTitleManagerList::ClearItems() -{ - m_sorted_data.clear(); - m_data.clear(); - SetItemCount(0); - RefreshPage(); -} - -void wxTitleManagerList::AutosizeColumns() -{ - wxAutosizeColumns(this, ColumnTitleId, ColumnMAX - 1); -} diff --git a/src/gui/wxgui/components/wxTitleManagerList.h b/src/gui/wxgui/components/wxTitleManagerList.h deleted file mode 100644 index 5781e3bf..00000000 --- a/src/gui/wxgui/components/wxTitleManagerList.h +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once - -#include "wxgui/helpers/wxCustomData.h" -#include "config/CemuConfig.h" - -#include - -#include // std::optional doesn't support optional reference inner types yet -#include -#include - -class wxTitleManagerList : public wxListView -{ - friend class TitleManager; -public: - wxTitleManagerList(wxWindow* parent, wxWindowID id = wxID_ANY); - ~wxTitleManagerList(); - - enum ItemColumn - { - ColumnTitleId = 0, - ColumnName, - ColumnType, - ColumnVersion, - ColumnRegion, - ColumnFormat, - ColumnLocation, - - ColumnMAX, - }; - - enum class EntryType - { - Base, - Update, - Dlc, - Save, - System, - }; - - enum class EntryFormat - { - Folder, - WUD, - NUS, - WUA, - WUHB, - }; - - // sort by column, if -1 will sort by last column or default (=titleid) - void SortEntries(int column); - void RefreshPage(); - void Filter(const wxString& filter); - [[nodiscard]] size_t GetCountByType(EntryType type) const; - void ClearItems(); - void AutosizeColumns(); - - struct TitleEntry - { - TitleEntry(const EntryType& type, const EntryFormat& format, fs::path path) - : type(type), format(format), path(std::move(path)) {} - - EntryType type; - EntryFormat format; - fs::path path; - - int icon = -1; - uint64 location_uid; - uint64 title_id; - wxString name; - uint32_t version = 0; - CafeConsoleRegion region; - - std::vector persistent_ids; // only used for save - }; - boost::optional GetSelectedTitleEntry() const; - -private: - void AddColumns(); - int Filter(const wxString& filter, const wxString& prefix, ItemColumn column); - boost::optional GetSelectedTitleEntry(); - boost::optional GetTitleEntryByUID(uint64 uid); - - class wxPanel* m_tooltip_window; - class wxStaticText* m_tooltip_text; - class wxTimer* m_tooltip_timer; - - void OnClose(wxCloseEvent& event); - void OnColumnClick(wxListEvent& event); - void OnContextMenu(wxContextMenuEvent& event); - void OnItemSelected(wxListEvent& event); - void OnContextMenuSelected(wxCommandEvent& event); - void OnTimer(class wxTimerEvent& event); - void OnRemoveItem(wxCommandEvent& event); - void OnRemoveEntry(wxCommandEvent& event); - - void OnTitleDiscovered(wxCommandEvent& event); - void OnTitleRemoved(wxCommandEvent& event); - void HandleTitleListCallback(struct CafeTitleListCallbackEvent* evt); - void HandleSaveListCallback(struct CafeSaveListCallbackEvent* evt); - - using TitleEntryData_t = wxCustomData; - void AddTitle(TitleEntryData_t* obj); - int AddImage(const wxImage& image) const; - wxString OnGetItemText(long item, long column) const override; - wxItemAttr* OnGetItemAttr(long item) const override; - [[nodiscard]] boost::optional GetTitleEntry(long item) const; - [[nodiscard]] boost::optional GetTitleEntry(long item); - [[nodiscard]] boost::optional GetTitleEntry(const fs::path& path) const; - [[nodiscard]] boost::optional GetTitleEntry(const fs::path& path); - - void OnConvertToCompressedFormat(uint64 titleId, uint64 rightClickedUID); - bool DeleteEntry(long index, const TitleEntry& entry); - - void RemoveItem(long item); - void RemoveItem(const TitleEntry& entry); - - struct ItemData - { - ItemData(bool visible, const TitleEntry& entry) - : visible(visible), entry(entry) {} - - bool visible; - TitleEntry entry; - }; - using ItemDataPtr = std::unique_ptr; - std::vector m_data; - std::vector> m_sorted_data; - - using Type_t = std::reference_wrapper; - bool SortFunc(int column, const Type_t& v1, const Type_t& v2); - - static wxString GetTitleEntryText(const TitleEntry& entry, ItemColumn column); - static wxString GetTranslatedTitleEntryType(EntryType entryType); - std::future m_context_worker; - - uint64 m_callbackIdTitleList; - uint64 m_callbackIdSaveList; -}; diff --git a/src/gui/wxgui/debugger/BreakpointWindow.cpp b/src/gui/wxgui/debugger/BreakpointWindow.cpp deleted file mode 100644 index 8dc449d9..00000000 --- a/src/gui/wxgui/debugger/BreakpointWindow.cpp +++ /dev/null @@ -1,321 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/BreakpointWindow.h" - -#include - -#include "wxgui/debugger/DebuggerWindow2.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" - -#include "Cemu/ExpressionParser/ExpressionParser.h" - -enum -{ - MENU_ID_CREATE_CODE_BP_EXECUTION = 1, - MENU_ID_CREATE_CODE_BP_LOGGING, - MENU_ID_CREATE_MEM_BP_READ, - MENU_ID_CREATE_MEM_BP_WRITE, - MENU_ID_DELETE_BP, -}; - -enum ItemColumns -{ - ColumnEnabled = 0, - ColumnAddress, - ColumnType, - ColumnComment, -}; - -BreakpointWindow::BreakpointWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size) - : wxFrame(&parent, wxID_ANY, _("Breakpoints"), wxDefaultPosition, wxSize(420, 250), wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT) -{ - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - - m_breakpoints = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); - m_breakpoints->EnableCheckBoxes(true); - - wxListItem col0; - col0.SetId(ColumnEnabled); - col0.SetText(_("On")); - col0.SetWidth(32); - m_breakpoints->InsertColumn(ColumnEnabled, col0); - - wxListItem col1; - col1.SetId(ColumnAddress); - col1.SetText(_("Address")); - col1.SetWidth(75); - m_breakpoints->InsertColumn(ColumnAddress, col1); - - wxListItem col2; - col2.SetId(ColumnType); - col2.SetText(_("Type")); - col2.SetWidth(42); - m_breakpoints->InsertColumn(ColumnType, col2); - - wxListItem col3; - col3.SetId(ColumnComment); - col3.SetText(_("Comment")); - col3.SetWidth(250); - m_breakpoints->InsertColumn(ColumnComment, col3); - - main_sizer->Add(m_breakpoints, 1, wxEXPAND); - - this->SetSizer(main_sizer); - this->wxWindowBase::Layout(); - - this->Centre(wxBOTH); - - if (parent.GetConfig().data().pin_to_main) - OnMainMove(main_position, main_size); - - m_breakpoints->Bind(wxEVT_LIST_ITEM_CHECKED, &BreakpointWindow::OnBreakpointToggled, this); - m_breakpoints->Bind(wxEVT_LIST_ITEM_UNCHECKED, &BreakpointWindow::OnBreakpointToggled, this); - m_breakpoints->Bind(wxEVT_LEFT_DCLICK, &BreakpointWindow::OnLeftDClick, this); - m_breakpoints->Bind(wxEVT_RIGHT_DOWN, &BreakpointWindow::OnRightDown, this); - - OnUpdateView(); -} - -BreakpointWindow::~BreakpointWindow() -{ - m_breakpoints->Unbind(wxEVT_LIST_ITEM_CHECKED, &BreakpointWindow::OnBreakpointToggled, this); - m_breakpoints->Unbind(wxEVT_LIST_ITEM_UNCHECKED, &BreakpointWindow::OnBreakpointToggled, this); - m_breakpoints->Unbind(wxEVT_LEFT_DCLICK, &BreakpointWindow::OnLeftDClick, this); - m_breakpoints->Unbind(wxEVT_RIGHT_DOWN, &BreakpointWindow::OnRightDown, this); -} - -void BreakpointWindow::OnMainMove(const wxPoint& main_position, const wxSize& main_size) -{ - wxSize size(420, 250); - this->SetSize(size); - - wxPoint position = main_position; - position.x -= 420; - position.y += main_size.y - 250; - this->SetPosition(position); -} - -void BreakpointWindow::OnUpdateView() -{ - Freeze(); - - m_breakpoints->DeleteAllItems(); - std::vector& breakpoints = debugger_lockBreakpoints(); - - uint32 i = 0; - for (const auto bpBase : breakpoints) - { - DebuggerBreakpoint* bp = bpBase; - while (bp) - { - wxListItem item = {}; - item.SetId(i++); - - const auto index = m_breakpoints->InsertItem(item); - m_breakpoints->SetItemPtrData(index, (uintptr_t)bp->id); - m_breakpoints->SetItem(index, ColumnAddress, wxString::Format("%08x", bp->address)); - const char* typeName = "UKN"; - if (bp->bpType == DEBUGGER_BP_T_NORMAL) - typeName = "X"; - else if (bp->bpType == DEBUGGER_BP_T_LOGGING) - typeName = "LOG"; - else if (bp->bpType == DEBUGGER_BP_T_ONE_SHOT) - typeName = "XS"; - else if (bp->bpType == DEBUGGER_BP_T_MEMORY_READ) - typeName = "R"; - else if (bp->bpType == DEBUGGER_BP_T_MEMORY_WRITE) - typeName = "W"; - - m_breakpoints->SetItem(index, ColumnType, typeName); - m_breakpoints->SetItem(index, ColumnComment, bp->comment); - m_breakpoints->CheckItem(index, bp->enabled); - - bp = bp->next; - } - } - debugger_unlockBreakpoints(); - Thaw(); -} - -void BreakpointWindow::OnGameLoaded() -{ - OnUpdateView(); -} - -void BreakpointWindow::OnBreakpointToggled(wxListEvent& event) -{ - const sint32 index = event.GetIndex(); - uint64 bpId = (uint64)event.GetData(); - debugger_lockBreakpoints(); - DebuggerBreakpoint* bp = debugger_getBreakpointById(bpId); - if (bp) - { - const bool state = m_breakpoints->IsItemChecked(index); - debugger_toggleBreakpoint(bp->address, state, bp); - } - debugger_unlockBreakpoints(); -} - -void BreakpointWindow::OnLeftDClick(wxMouseEvent& event) -{ - const auto position = event.GetPosition(); - int flags = 0; - const long index = m_breakpoints->HitTest(position, flags); - - if (index == wxNOT_FOUND) - return; - - sint32 x = position.x; - const auto enabled_width = m_breakpoints->GetColumnWidth(ColumnEnabled); - if (x <= enabled_width) - return; - - x -= enabled_width; - const auto address_width = m_breakpoints->GetColumnWidth(ColumnAddress); - if (x <= address_width) - { - const auto item = m_breakpoints->GetItemText(index, ColumnAddress); - const auto address = std::stoul(item.ToStdString(), nullptr, 16); - g_debuggerDispatcher.MoveToAddressInDisassembly(address); - return; - } - - x -= address_width; - const auto type_width = m_breakpoints->GetColumnWidth(ColumnType); - if (x <= type_width) - return; - - x -= type_width; - const auto comment_width = m_breakpoints->GetColumnWidth(ColumnComment); - if (x <= comment_width) - { - std::vector& breakpoints = debugger_lockBreakpoints(); - if (index >= breakpoints.size()) - { - debugger_unlockBreakpoints(); - return; - } - DebuggerBreakpoint* bp = breakpoints[index]; - auto bpId = bp->id; - - const auto item = m_breakpoints->GetItemText(index, ColumnAddress); - const auto address = std::stoul(item.ToStdString(), nullptr, 16); - - bool isLoggingBP = bp->bpType == DEBUGGER_BP_T_LOGGING; - const wxString dialogTitle = isLoggingBP ? _("Enter a new logging message") : _("Enter a new comment"); - const wxString dialogMessage = isLoggingBP ? _("Set logging message when code at address %08x is ran.\nUse placeholders like {r3} or {f3} to log register values") : _("Set comment for breakpoint at address %08x"); - const wxString existingComment = bp->comment; - debugger_unlockBreakpoints(); - wxTextEntryDialog set_comment_dialog(this, dialogMessage, dialogTitle, existingComment); - if (set_comment_dialog.ShowModal() == wxID_OK) - { - if (isLoggingBP) - { - debugger_lockBreakpoints(); - DebuggerBreakpoint* bp = debugger_getBreakpointById(bpId); - if (bp) - { - bp->comment = set_comment_dialog.GetValue().ToStdWstring(); - m_breakpoints->SetItem(index, ColumnComment, set_comment_dialog.GetValue()); - } - debugger_unlockBreakpoints(); - } - } - } -} - -void BreakpointWindow::OnRightDown(wxMouseEvent& event) -{ - int flags = 0; - const long index = m_breakpoints->HitTest(event.GetPosition(), flags); - if (index == wxNOT_FOUND || index < 0 || index >= m_breakpoints->GetItemCount()) - { - wxMenu menu; - menu.Append(MENU_ID_CREATE_CODE_BP_EXECUTION, _("Create execution breakpoint")); - menu.Append(MENU_ID_CREATE_CODE_BP_LOGGING, _("Create logging breakpoint")); - menu.Append(MENU_ID_CREATE_MEM_BP_READ, _("Create memory breakpoint (read)")); - menu.Append(MENU_ID_CREATE_MEM_BP_WRITE, _("Create memory breakpoint (write)")); - - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &BreakpointWindow::OnContextMenuClick, this); - PopupMenu(&menu); - } - else - { - m_breakpoints->Focus(index); - m_breakpoints->Select(index); - - wxMenu menu; - menu.Append(MENU_ID_DELETE_BP, _("Delete breakpoint")); - - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &BreakpointWindow::OnContextMenuClickSelected, this); - PopupMenu(&menu); - } -} - -void BreakpointWindow::OnContextMenuClickSelected(wxCommandEvent& evt) -{ - auto& breakpoints = debugger_lockBreakpoints(); - long selectedIndex = m_breakpoints->GetFirstSelected(); - if (selectedIndex == wxNOT_FOUND || selectedIndex < 0 || selectedIndex >= (long)breakpoints.size()) - { - debugger_unlockBreakpoints(); - return; - } - BreakpointId bpId = (uint64)m_breakpoints->GetItemData(selectedIndex); - DebuggerBreakpoint* bp = debugger_getBreakpointById(bpId); - if (!bp) - { - debugger_unlockBreakpoints(); - return; - } - if (evt.GetId() == MENU_ID_DELETE_BP) - { - debugger_deleteBreakpoint(bpId); - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); - wxPostEvent(this->m_parent, evt); - } - debugger_unlockBreakpoints(); -} - -void BreakpointWindow::OnContextMenuClick(wxCommandEvent& evt) -{ - wxTextEntryDialog goto_dialog(this, _("Enter a memory address"), _("Set breakpoint"), wxEmptyString); - if (goto_dialog.ShowModal() == wxID_OK) - { - ExpressionParser parser; - - auto value = goto_dialog.GetValue().ToStdString(); - std::transform(value.begin(), value.end(), value.begin(), tolower); - - uint32_t newBreakpointAddress = 0; - try - { - debugger_addParserSymbols(parser); - newBreakpointAddress = parser.IsConstantExpression("0x"+value) ? (uint32)parser.Evaluate("0x"+value) : (uint32)parser.Evaluate(value); - } - catch (const std::exception& ex) - { - wxMessageBox(ex.what(), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - switch (evt.GetId()) - { - case MENU_ID_CREATE_CODE_BP_EXECUTION: - debugger_createCodeBreakpoint(newBreakpointAddress, DEBUGGER_BP_T_NORMAL); - break; - case MENU_ID_CREATE_CODE_BP_LOGGING: - debugger_createCodeBreakpoint(newBreakpointAddress, DEBUGGER_BP_T_LOGGING); - break; - case MENU_ID_CREATE_MEM_BP_READ: - debugger_createMemoryBreakpoint(newBreakpointAddress, true, false); - break; - case MENU_ID_CREATE_MEM_BP_WRITE: - debugger_createMemoryBreakpoint(newBreakpointAddress, false, true); - break; - } - - this->OnUpdateView(); - } -} diff --git a/src/gui/wxgui/debugger/BreakpointWindow.h b/src/gui/wxgui/debugger/BreakpointWindow.h deleted file mode 100644 index 330247e0..00000000 --- a/src/gui/wxgui/debugger/BreakpointWindow.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -class DebuggerWindow2; -class wxListEvent; -class wxListView; - -class BreakpointWindow : public wxFrame -{ -public: - BreakpointWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size); - virtual ~BreakpointWindow(); - - void OnMainMove(const wxPoint& position, const wxSize& main_size); - void OnUpdateView(); - void OnGameLoaded(); - -private: - void OnBreakpointToggled(wxListEvent& event); - void OnLeftDClick(wxMouseEvent& event); - void OnRightDown(wxMouseEvent& event); - - void OnContextMenuClick(wxCommandEvent& evt); - void OnContextMenuClickSelected(wxCommandEvent& evt); - - wxListView* m_breakpoints; -}; diff --git a/src/gui/wxgui/debugger/DebuggerWindow2.cpp b/src/gui/wxgui/debugger/DebuggerWindow2.cpp deleted file mode 100644 index b3bb5105..00000000 --- a/src/gui/wxgui/debugger/DebuggerWindow2.cpp +++ /dev/null @@ -1,936 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/DebuggerWindow2.h" - -#include "wxHelper.h" - -#include "Common/FileStream.h" -#include "config/ActiveSettings.h" -#include "Cafe/HW/MMU/MMU.h" -#include "Cafe/OS/RPL/rpl_structs.h" -#include "Cafe/OS/RPL/rpl_debug_symbols.h" -#include "Cafe/OS/RPL/rpl_symbol_storage.h" - -#include "wxgui/debugger/RegisterWindow.h" -#include "wxgui/debugger/DumpWindow.h" - -#include "Cafe/HW/Espresso/Debugger/Debugger.h" - -#include "Cafe/OS/RPL/rpl.h" -#include "wxgui/debugger/DisasmCtrl.h" -#include "wxgui/debugger/SymbolWindow.h" -#include "wxgui/debugger/BreakpointWindow.h" -#include "wxgui/debugger/ModuleWindow.h" -#include "util/helpers/helpers.h" -#include "util/helpers/StringHelpers.h" -#include "Cafe/HW/Espresso/Recompiler/PPCRecompiler.h" -#include "Cemu/Logging/CemuLogging.h" - -#include "resource/embedded/resources.h" - -enum -{ - // file - MENU_ID_FILE_EXIT = wxID_HIGHEST + 8000, - // settings - MENU_ID_OPTIONS_PIN_TO_MAINWINDOW, - MENU_ID_OPTIONS_BREAK_ON_START, - MENU_ID_OPTIONS_LOG_MEMORY_BREAKPOINTS, - MENU_ID_OPTIONS_SWITCH_CPU_MODE, - // window - MENU_ID_WINDOW_REGISTERS, - MENU_ID_WINDOW_DUMP, - MENU_ID_WINDOW_STACK, - MENU_ID_WINDOW_BREAKPOINTS, - MENU_ID_WINDOW_MODULE, - MENU_ID_WINDOW_SYMBOL, - - // tool - TOOL_ID_GOTO, - TOOL_ID_BP, - TOOL_ID_PAUSE, - TOOL_ID_STEP_OVER, - TOOL_ID_STEP_INTO, -}; - -wxDEFINE_EVENT(wxEVT_DEBUGGER_CLOSE, wxCloseEvent); -wxDEFINE_EVENT(wxEVT_UPDATE_VIEW, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_BREAKPOINT_CHANGE, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_BREAKPOINT_HIT, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_MOVE_TO_DISASM_ADDR, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_RUN, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_NOTIFY_MODULE_LOADED, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_NOTIFY_MODULE_UNLOADED, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_NOTIFY_GRAPHIC_PACKS_MODIFIED, wxCommandEvent); - -wxBEGIN_EVENT_TABLE(DebuggerWindow2, wxFrame) - EVT_SHOW(DebuggerWindow2::OnShow) - EVT_CLOSE(DebuggerWindow2::OnClose) - EVT_COMMAND(wxID_ANY, wxEVT_UPDATE_VIEW, DebuggerWindow2::OnUpdateView) - EVT_COMMAND(wxID_ANY, wxEVT_BREAKPOINT_CHANGE, DebuggerWindow2::OnBreakpointChange) - EVT_COMMAND(wxID_ANY, wxEVT_MOVE_TO_DISASM_ADDR, DebuggerWindow2::OnMoveToDisasmAddr) - EVT_COMMAND(wxID_ANY, wxEVT_COMMAND_TOOL_CLICKED, DebuggerWindow2::OnToolClicked) - EVT_COMMAND(wxID_ANY, wxEVT_BREAKPOINT_HIT, DebuggerWindow2::OnBreakpointHit) - EVT_COMMAND(wxID_ANY, wxEVT_RUN, DebuggerWindow2::OnRunProgram) - EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_LOADED, DebuggerWindow2::OnNotifyModuleLoaded) - EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded) - EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_GRAPHIC_PACKS_MODIFIED, DebuggerWindow2::OnNotifyGraphicPacksModified) - EVT_COMMAND(wxID_ANY, wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, DebuggerWindow2::OnDisasmCtrlGotoAddress) - // file menu - EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit) - // window - EVT_MENU_RANGE(MENU_ID_WINDOW_REGISTERS, MENU_ID_WINDOW_MODULE, DebuggerWindow2::OnWindowMenu) -wxEND_EVENT_TABLE() - -DebuggerModuleInfo::DebuggerModuleInfo(RPLModule* module) -{ - moduleName = module->moduleName; - patchCRC = module->patchCRC; - textArea.base = module->regionMappingBase_text.GetMPTR(); - textArea.origBase = module->regionOrigAddr_text; - textArea.size = module->regionSize_text; - dataArea.base = module->regionMappingBase_data; - dataArea.origBase = module->regionOrigAddr_data; - dataArea.size = module->regionSize_data; -} - -struct DebuggerModuleInfoNotify : public wxClientData -{ - DebuggerModuleInfo moduleInfo; - - DebuggerModuleInfoNotify(RPLModule* module) : moduleInfo(module) {}; -}; - -DebuggerWindow2* s_debuggerWindow; - -static bool TryParseMapAddress(std::string_view token, uint32& value) -{ - trim(token, "\t\n\v\f\r ,:;()[]"); - if (const auto colonIndex = token.find_last_of(':'); colonIndex != std::string_view::npos) - token = token.substr(colonIndex + 1); - trim(token, "\t\n\v\f\r ,:;()[]"); - if (!token.empty() && (token.back() == 'h' || token.back() == 'H')) - token.remove_suffix(1); - if (token.size() > 2 && token[0] == '0' && (token[1] == 'x' || token[1] == 'X')) - token.remove_prefix(2); - if (token.empty()) - return false; - - uint64 parsedValue = 0; - const auto result = std::from_chars(token.data(), token.data() + token.size(), parsedValue, 16); - if (result.ec != std::errc() || result.ptr != (token.data() + token.size()) || parsedValue > std::numeric_limits::max()) - return false; - - value = static_cast(parsedValue); - return true; -} - -static bool TryParseMapSymbolLine(std::string_view rawLine, uint32& address, std::string& symbolName) -{ - auto line = rawLine; - trim(line); - if (line.empty()) - return false; - - const auto addressEnd = line.find_first_of(" \t"); - if (addressEnd == std::string_view::npos) - return false; - if (!TryParseMapAddress(line.substr(0, addressEnd), address)) - return false; - - line = line.substr(addressEnd + 1); - trim(line); - if (line.empty()) - return false; - - const auto symbolEnd = line.find_first_of(" \t"); - auto symbolToken = symbolEnd == std::string_view::npos ? line : line.substr(0, symbolEnd); - trim(symbolToken); - if (symbolToken.empty()) - return false; - - symbolName.assign(symbolToken); - return true; -} - -static std::optional GetRelocatedMapAddress(const DebuggerModuleInfo& moduleInfo, uint32 mapAddress) -{ - if (moduleInfo.textArea.ContainsOriginalAddress(mapAddress)) - return moduleInfo.textArea.base + (mapAddress - moduleInfo.textArea.origBase); - if (moduleInfo.dataArea.ContainsOriginalAddress(mapAddress)) - return moduleInfo.dataArea.base + (mapAddress - moduleInfo.dataArea.origBase); - return std::nullopt; -} - -void DebuggerConfig::Load(XMLConfigParser& parser) -{ - pin_to_main = parser.get("PinToMainWindow", true); - break_on_start = parser.get("break_on_start", true); - log_memory_breakpoints = parser.get("log_memory_breakpoints", false); - - auto window_parser = parser.get("Windows"); - show_register = window_parser.get("Registers", true); - show_dump = window_parser.get("MemoryDump", true); - show_stack = window_parser.get("Stack", true); - show_breakpoints = window_parser.get("Breakpoints", true); - show_modules = window_parser.get("Modules", true); - show_symbols = window_parser.get("Symbols", true); -} - -void DebuggerConfig::Save(XMLConfigParser& parser) -{ - parser.set("PinToMainWindow", pin_to_main); - parser.set("break_on_start", break_on_start); - parser.set("log_memory_breakpoints", log_memory_breakpoints); - - auto window_parser = parser.set("Windows"); - window_parser.set("Registers", show_register); - window_parser.set("MemoryDump", show_dump); - window_parser.set("Stack", show_stack); - window_parser.set("Breakpoints", show_breakpoints); - window_parser.set("Modules", show_modules); - window_parser.set("Symbols", show_symbols); -} - -void DebuggerModuleStorage::Load(XMLConfigParser& parser) -{ - auto breakpoints_parser = parser.get("Breakpoints"); - for (auto element = breakpoints_parser.get("Entry"); element.valid(); element = breakpoints_parser.get("Entry", element)) - { - const auto address_string = element.get("Address", ""); - if (*address_string == '\0') - continue; - - uint32 relative_address = std::strtoul(address_string, nullptr, 16); - if (relative_address == 0) - continue; - - auto type = element.get("Type", 0); - auto enabled = element.get("Enabled", true); - const auto comment = element.get("Comment", ""); - - // calculate absolute address - uint32 module_base_address = (type == DEBUGGER_BP_T_NORMAL || type == DEBUGGER_BP_T_LOGGING) ? this->moduleInfo.textArea.base : this->moduleInfo.dataArea.base; - uint32 address = module_base_address + relative_address; - - // don't change anything if there's already a breakpoint - if (debugger_getFirstBP(address) != nullptr) - continue; - - // register breakpoints in debugger - if (type == DEBUGGER_BP_T_NORMAL) - debugger_createCodeBreakpoint(address, DEBUGGER_BP_T_NORMAL); - else if (type == DEBUGGER_BP_T_LOGGING) - debugger_createCodeBreakpoint(address, DEBUGGER_BP_T_LOGGING); - else if (type == DEBUGGER_BP_T_MEMORY_READ) - debugger_createMemoryBreakpoint(address, true, false); - else if (type == DEBUGGER_BP_T_MEMORY_WRITE) - debugger_createMemoryBreakpoint(address, false, true); - else - continue; - - DebuggerBreakpoint* debugBreakpoint = debugger_getFirstBP(address); - - if (!enabled) - debugger_toggleBreakpoint(address, false, debugBreakpoint); - - debugBreakpoint->comment = boost::nowide::widen(comment); - } - - auto comments_parser = parser.get("Comments"); - for (XMLConfigParser element = comments_parser.get("Entry"); element.valid(); element = comments_parser.get("Entry", element)) - { - const auto address_string = element.get("Address", ""); - if (*address_string == '\0') - continue; - - uint32 address = std::strtoul(address_string, nullptr, 16); - if (address == 0) - continue; - - const auto comment = element.get("Comment", ""); - if (*comment == '\0') - continue; - - rplDebugSymbol_createComment(address, boost::nowide::widen(comment).c_str()); - } -} - -void DebuggerModuleStorage::Save(XMLConfigParser& parser) -{ - auto breakpoints_parser = parser.set("Breakpoints"); - auto& breakpoints = debugger_lockBreakpoints(); - for (const auto& bp : breakpoints) - { - // check breakpoint type - if (bp->dbType != DEBUGGER_BP_T_DEBUGGER) - continue; - - // check whether the breakpoint is part of the current module being saved - uint32 relativeAddress; - if (moduleInfo.textArea.ContainsAddress(bp->address)) - relativeAddress = bp->address - moduleInfo.textArea.base; - else if (moduleInfo.dataArea.ContainsAddress(bp->address)) - relativeAddress = bp->address - moduleInfo.dataArea.base; - else - continue; - - auto entry = breakpoints_parser.set("Entry"); - entry.set("Address", fmt::format("{:#10x}", relativeAddress)); - entry.set("Comment", boost::nowide::narrow(bp->comment).c_str()); - entry.set("Type", bp->bpType); - entry.set("Enabled", bp->enabled); - - if (this->delete_breakpoints_after_saving) - debugger_deleteBreakpoint(bp->id); - this->delete_breakpoints_after_saving = false; - } - debugger_unlockBreakpoints(); - - auto comments_parser = parser.set("Comments"); - for (const auto& comment_entry : rplDebugSymbol_getSymbols()) - { - // check comment type - const auto commentAddress = comment_entry.first; - const auto comment = static_cast(comment_entry.second); - if (!comment || comment->type != RplDebugSymbolComment) - continue; - - // check whether the comment is part of the current module being saved - uint32 relativeAddress; - if (moduleInfo.textArea.ContainsAddress(commentAddress)) - relativeAddress = commentAddress - moduleInfo.textArea.base; - else - continue; - - auto entry = comments_parser.set("Entry"); - entry.set("Address", fmt::format("{:#10x}", relativeAddress)); - entry.set("Comment", boost::nowide::narrow(comment->comment).c_str()); - } -} - -void DebuggerWindow2::CreateToolBar() -{ - m_toolbar = wxFrame::CreateToolBar(wxTB_HORIZONTAL, wxID_ANY); - m_toolbar->SetToolBitmapSize(wxSize(16, 16)); - - wxBitmap goto_bitmap = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_GOTO_png, sizeof(DEBUGGER_GOTO_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_toolbar->AddTool(TOOL_ID_GOTO, wxEmptyString, goto_bitmap, wxNullBitmap, wxITEM_NORMAL, _("GoTo (CTRL + G)"), "test", NULL); - m_toolbar->AddSeparator(); - - wxBitmap bp_bitmap = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_BP_RED_png, sizeof(DEBUGGER_BP_RED_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_toolbar->AddTool(TOOL_ID_BP, wxEmptyString, bp_bitmap, wxNullBitmap, wxITEM_NORMAL, _("Toggle Breakpoint (F9)"), wxEmptyString, NULL); - m_toolbar->AddSeparator(); - - m_pause = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_PAUSE_png, sizeof(DEBUGGER_PAUSE_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_run = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_PLAY_png, sizeof(DEBUGGER_PLAY_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_toolbar->AddTool(TOOL_ID_PAUSE, wxEmptyString, m_pause, wxNullBitmap, wxITEM_NORMAL, _("Break (F5)"), wxEmptyString, NULL); - - wxBitmap step_into_bitmap = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_STEP_INTO_png, sizeof(DEBUGGER_STEP_INTO_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - wxBitmap step_over_bitmap = wxHelper::LoadThemedBitmapFromPNG(DEBUGGER_STEP_OVER_png, sizeof(DEBUGGER_STEP_OVER_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_toolbar->AddTool(TOOL_ID_STEP_INTO, wxEmptyString, step_into_bitmap, wxNullBitmap, wxITEM_NORMAL, _("Step Into (F11)"), wxEmptyString, NULL); - m_toolbar->AddTool(TOOL_ID_STEP_OVER, wxEmptyString, step_over_bitmap, wxNullBitmap, wxITEM_NORMAL, _("Step Over (F10)"), wxEmptyString, NULL); - m_toolbar->AddSeparator(); - - m_toolbar->Realize(); - - m_toolbar->EnableTool(TOOL_ID_STEP_INTO, false); - m_toolbar->EnableTool(TOOL_ID_STEP_OVER, false); - -} - -void DebuggerWindow2::LoadModuleStorage(const DebuggerModuleInfo& moduleInfo) -{ - auto path = GetModuleStoragePath(moduleInfo.moduleName, moduleInfo.patchCRC); - bool already_loaded = std::any_of(m_modulesStorage.begin(), m_modulesStorage.end(), [path](const std::unique_ptr& debug) { return debug->GetFilename() == path; }); - if (!path.empty() && !already_loaded) - { - auto& moduleStorage = m_modulesStorage.emplace_back(new XMLDebuggerModuleConfig(path, { moduleInfo, false })); - moduleStorage->Load(); - LoadModuleMap(moduleStorage->data()); - } -} - -void DebuggerWindow2::SaveModuleStorage(const DebuggerModuleInfo& moduleInfo, bool deleteModuleStorage) -{ - auto path = GetModuleStoragePath(moduleInfo.moduleName, moduleInfo.patchCRC); - for (auto it = m_modulesStorage.begin(); it != m_modulesStorage.end(); ++it) - { - auto& moduleStorage = *it; - if (moduleStorage->data().moduleInfo.moduleName == moduleInfo.moduleName && moduleStorage->data().moduleInfo.patchCRC == moduleInfo.patchCRC) - { - moduleStorage->data().delete_breakpoints_after_saving = deleteModuleStorage; // make sure breakpoints are deleted when the storage is removed - moduleStorage->Save(path); - if (deleteModuleStorage) - { - UnloadModuleMap(moduleStorage->data()); - m_modulesStorage.erase(it); - } - return; - } - } -} - -void DebuggerWindow2::LoadModuleMap(DebuggerModuleStorage& moduleStorage) -{ - UnloadModuleMap(moduleStorage); - - const auto mapPath = GetModuleMapPath(moduleStorage.moduleInfo.moduleName, moduleStorage.moduleInfo.patchCRC); - if (mapPath.empty()) - return; - - std::unique_ptr fileStream(FileStream::openFile(mapPath.c_str())); - if (!fileStream) - return; - - const auto fileSize = fileStream->GetSize(); - if (fileSize == 0 || fileSize > std::numeric_limits::max()) - return; - - const auto readSize = static_cast(fileSize); - std::vector fileData(readSize); - if (fileStream->readData(fileData.data(), readSize) != readSize) - return; - - std::string_view fileView(fileData.data(), fileData.size()); - for (const auto& line : StringHelpers::StringLineIterator(fileView)) - { - uint32 mapAddress = 0; - std::string symbolName; - if (TryParseMapSymbolLine(line, mapAddress, symbolName)) - { - const auto relocatedAddress = GetRelocatedMapAddress(moduleStorage.moduleInfo, mapAddress); - if (relocatedAddress) - { - moduleStorage.loaded_map_symbols.emplace_back(rplSymbolStorage_store(moduleStorage.moduleInfo.moduleName.c_str(), symbolName.c_str(), *relocatedAddress, RPL_STORED_SYMBOL_MAP)); - } - } - } -} - -void DebuggerWindow2::UnloadModuleMap(DebuggerModuleStorage& moduleStorage) -{ - for (auto* symbol : moduleStorage.loaded_map_symbols) - { - if (symbol) - rplSymbolStorage_remove(symbol); - } - moduleStorage.loaded_map_symbols.clear(); -} - -DebuggerWindow2::DebuggerWindow2(wxFrame& parent, const wxRect& display_size) - : wxFrame(&parent, wxID_ANY, _("PPC Debugger"), wxDefaultPosition, wxSize(1280, 300), wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT), - m_module_address(0) -{ - g_debuggerDispatcher.SetDebuggerCallbacks(this); - - const auto file = ActiveSettings::GetConfigPath("debugger/config.xml"); - m_config.SetFilename(file.generic_wstring()); - m_config.Load(); - - debugger_setOptionBreakOnEntry(m_config.data().break_on_start); - debugger_setOptionLogOnlyMemoryBreakpoints(m_config.data().log_memory_breakpoints); - - m_main_position = parent.GetPosition(); - m_main_size = parent.GetSize(); - - auto y = std::max(300, (display_size.GetHeight() - 500 - 300) * 0.8); - this->SetSize(1280, y); - - CreateMenuBar(); - CreateToolBar(); - - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - - // load configs and module storage for already loaded modules - const auto module_list = RPLLoader_GetModuleList(); - for (sint32 i = 0; i < module_list.size(); i++) - { - const auto module = module_list[i]; - DebuggerModuleInfo moduleInfo(module); - LoadModuleStorage(moduleInfo); - } - - wxString label_text = _("> no modules loaded"); - if (module_list.size() != 0) - { - RPLModule* currentModule = RPLLoader_FindModuleByCodeAddr(MEMORY_CODEAREA_ADDR); - if (currentModule) - label_text = wxString::Format("> %s", currentModule->moduleName.c_str()); - else - label_text = _("> unknown module"); - } - - m_module_label = new wxStaticText(this, wxID_ANY, label_text); - m_module_label->SetForegroundColour(wxColour(0xFFbf52fe)); - main_sizer->Add(m_module_label, 0, wxEXPAND | wxALL, 5); - - m_disasm_ctrl = new DisasmCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxScrolledWindowStyle); - main_sizer->Add(m_disasm_ctrl, 1, wxEXPAND); - - this->SetSizer(main_sizer); - this->wxWindowBase::Layout(); - - m_register_window = new RegisterWindow(*this, m_main_position, m_main_size); - m_dump_window = new DumpWindow(*this, m_main_position, m_main_size); - m_breakpoint_window = new BreakpointWindow(*this, m_main_position, m_main_size); - m_module_window = new ModuleWindow(*this, m_main_position, m_main_size); - m_symbol_window = new SymbolWindow(*this, m_main_position, m_main_size); - - const bool value = m_config.data().pin_to_main; - m_config.data().pin_to_main = true; - OnParentMove(m_main_position, m_main_size); - m_config.data().pin_to_main = value; - - s_debuggerWindow = this; -} - -DebuggerWindow2::~DebuggerWindow2() -{ - g_debuggerDispatcher.ClearDebuggerCallbacks(); - - s_debuggerWindow = nullptr; - - CleanupForDestroy(); - - if (m_register_window && m_register_window->IsShown()) - m_register_window->Close(true); - - if (m_dump_window && m_dump_window->IsShown()) - m_dump_window->Close(true); - - if (m_breakpoint_window && m_breakpoint_window->IsShown()) - m_breakpoint_window->Close(true); - - if (m_module_window && m_module_window->IsShown()) - m_module_window->Close(true); - - if (m_symbol_window && m_symbol_window->IsShown()) - m_symbol_window->Close(true); - - // reenable recompiler if force interpreter option was enabled - if (m_forceInterpreter) - { - PPCRecompiler_Enable(); - m_forceInterpreter = false; - cemuLog_log(LogType::Force, "Debugger: Switched CPU mode to recompiler"); - } -} - -// note: Can be called twice in some circumstances where early cleanup is needed. Will also always be called from destructor -void DebuggerWindow2::CleanupForDestroy() -{ - debugger_setOptionBreakOnEntry(false); - // save module storage - while (!m_modulesStorage.empty()) - { - SaveModuleStorage(m_modulesStorage[0]->data().moduleInfo, true); // deleting the module storage will also delete breakpoints belonging to the module's data - } - m_config.Save(); -} - -void DebuggerWindow2::OnClose(wxCloseEvent& event) -{ - debugger_setOptionBreakOnEntry(false); - - const wxCloseEvent parentEvent(wxEVT_DEBUGGER_CLOSE); - wxPostEvent(m_parent, parentEvent); - - event.Skip(); -} - -void DebuggerWindow2::OnMoveToDisasmAddr(wxCommandEvent& event) -{ - MPTR address = (uint32)(uint64)event.GetClientData(); - if (!address) - return; - UpdateModuleLabel(address); - m_disasm_ctrl->CenterOffset(address); -} - -void DebuggerWindow2::OnDisasmCtrlGotoAddress(wxCommandEvent& event) -{ - uint32 address = static_cast(event.GetExtraLong()); - UpdateModuleLabel(address); -} - -void DebuggerWindow2::OnParentMove(const wxPoint& main_position, const wxSize& main_size) -{ - m_main_position = main_position; - m_main_size = main_size; - - if (!m_config.data().pin_to_main) - return; - - wxSize size(m_main_size.x, GetSize().GetHeight()); - SetSize(size); - - wxPoint position = m_main_position; - position.y -= size.y; - SetPosition(position); - - m_register_window->OnMainMove(main_position, main_size); - m_dump_window->OnMainMove(main_position, main_size); - m_breakpoint_window->OnMainMove(main_position, main_size); - m_module_window->OnMainMove(main_position, main_size); - m_symbol_window->OnMainMove(main_position, main_size); -} - -void DebuggerWindow2::OnNotifyModuleLoaded(wxCommandEvent& event) -{ - DebuggerModuleInfoNotify* moduleNotif = static_cast(event.GetClientObject()); - LoadModuleStorage(moduleNotif->moduleInfo); - m_module_window->OnGameLoaded(); - m_symbol_window->OnGameLoaded(); - m_disasm_ctrl->Init(); -} - -void DebuggerWindow2::OnNotifyModuleUnloaded(wxCommandEvent& event) -{ - DebuggerModuleInfoNotify* moduleNotif = static_cast(event.GetClientObject()); - SaveModuleStorage(moduleNotif->moduleInfo, true); - m_module_window->OnGameLoaded(); - m_symbol_window->OnGameLoaded(); - m_disasm_ctrl->Init(); -} - -void DebuggerWindow2::OnNotifyGraphicPacksModified(wxCommandEvent& event) -{ - m_module_window->OnGameLoaded(); - m_symbol_window->OnGameLoaded(); - m_disasm_ctrl->Init(); -} - -void DebuggerWindow2::OnGameLoaded() -{ - m_disasm_ctrl->Init(); - - m_dump_window->OnGameLoaded(); - m_module_window->OnGameLoaded(); - m_breakpoint_window->OnGameLoaded(); - m_symbol_window->OnGameLoaded(); - - RPLModule* current_rpl_module = RPLLoader_FindModuleByCodeAddr(MEMORY_CODEAREA_ADDR); - if(current_rpl_module) - m_module_label->SetLabel(wxString::Format("> %s", current_rpl_module->moduleName.c_str())); - - this->SendSizeEvent(); -} - -XMLDebuggerConfig& DebuggerWindow2::GetConfig() -{ - return m_config; -} - -bool DebuggerWindow2::Show(bool show) -{ - const bool result = wxFrame::Show(show); - - if (show) - { - m_register_window->Show(m_config.data().show_register); - m_dump_window->Show(m_config.data().show_dump); - m_breakpoint_window->Show(m_config.data().show_breakpoints); - m_module_window->Show(m_config.data().show_modules); - m_symbol_window->Show(m_config.data().show_symbols); - } - else - { - m_register_window->Show(false); - m_dump_window->Show(false); - m_breakpoint_window->Show(false); - m_module_window->Show(false); - m_symbol_window->Show(false); - } - - return result; -} - -std::wstring DebuggerWindow2::GetModuleStoragePath(std::string module_name, uint32_t crc_hash) const -{ - if (module_name.empty() || crc_hash == 0) return {}; - return ActiveSettings::GetConfigPath("debugger/{}_{:#10x}.xml", module_name, crc_hash).generic_wstring(); -} - -std::wstring DebuggerWindow2::GetModuleMapPath(std::string module_name, uint32_t crc_hash) const -{ - if (module_name.empty() || crc_hash == 0) return {}; - return ActiveSettings::GetConfigPath("debugger/{}_{:#10x}.map", module_name, crc_hash).generic_wstring(); -} - -void DebuggerWindow2::OnBreakpointHit(wxCommandEvent& event) -{ - PPCInterpreter_t* hCPU = debugger_lockDebugSession(); - if (!hCPU) - return; - MPTR ip = hCPU->instructionPointer; - debugger_unlockDebugSession(hCPU); - - UpdateModuleLabel(ip); - - m_toolbar->SetToolShortHelp(TOOL_ID_PAUSE, _("Run (F5)")); - m_toolbar->SetToolNormalBitmap(TOOL_ID_PAUSE, m_run); - - m_toolbar->EnableTool(TOOL_ID_STEP_INTO, true); - m_toolbar->EnableTool(TOOL_ID_STEP_OVER, true); - - m_disasm_ctrl->CenterOffset(ip); -} - -void DebuggerWindow2::OnRunProgram(wxCommandEvent& event) -{ - m_toolbar->SetToolShortHelp(TOOL_ID_PAUSE, _("Break (F5)")); - m_toolbar->SetToolNormalBitmap(TOOL_ID_PAUSE, m_pause); - - m_toolbar->EnableTool(TOOL_ID_STEP_INTO, false); - m_toolbar->EnableTool(TOOL_ID_STEP_OVER, false); -} - -void DebuggerWindow2::OnToolClicked(wxCommandEvent& event) -{ - switch(event.GetId()) - { - case TOOL_ID_GOTO: - m_disasm_ctrl->GoToAddressDialog(); - break; - case TOOL_ID_PAUSE: - if (debugger_isTrapped()) - debugger_stepCommand(DebuggerStepCommand::Run); - else - debugger_requestBreak(); - break; - case TOOL_ID_STEP_INTO: - if (debugger_isTrapped()) - debugger_stepCommand(DebuggerStepCommand::StepInto); - break; - case TOOL_ID_STEP_OVER: - if (debugger_isTrapped()) - debugger_stepCommand(DebuggerStepCommand::StepOver); - break; - } -} - -void DebuggerWindow2::OnBreakpointChange(wxCommandEvent& event) -{ - m_breakpoint_window->OnUpdateView(); - m_disasm_ctrl->RefreshControl(); - UpdateModuleLabel(); -} - -void DebuggerWindow2::OnOptionsInput(wxCommandEvent& event) -{ - switch (event.GetId()) - { - case MENU_ID_OPTIONS_PIN_TO_MAINWINDOW: - { - const bool value = !m_config.data().pin_to_main; - m_config.data().pin_to_main = value; - if (value) - OnParentMove(m_main_position, m_main_size); - - break; - } - case MENU_ID_OPTIONS_BREAK_ON_START: - { - const bool value = !m_config.data().break_on_start; - m_config.data().break_on_start = value; - debugger_setOptionBreakOnEntry(value); - break; - } - case MENU_ID_OPTIONS_LOG_MEMORY_BREAKPOINTS: - { - const bool value = !m_config.data().log_memory_breakpoints; - m_config.data().log_memory_breakpoints = value; - debugger_setOptionLogOnlyMemoryBreakpoints(value); - break; - } - case MENU_ID_OPTIONS_SWITCH_CPU_MODE: - { - if (m_forceInterpreter) - { - PPCRecompiler_Enable(); - m_forceInterpreter = false; - cemuLog_log(LogType::Force, "Debugger: Switched CPU mode to recompiler"); - } - else - { - PPCRecompiler_Disable(); - m_forceInterpreter = true; - cemuLog_log(LogType::Force, "Debugger: Switched CPU mode to interpreter"); - } - break; - } - default: - return; - } - - m_config.Save(); -} - -void DebuggerWindow2::OnWindowMenu(wxCommandEvent& event) -{ - switch (event.GetId()) - { - case MENU_ID_WINDOW_REGISTERS: - { - const bool value = !m_config.data().show_register; - m_config.data().show_register = value; - m_register_window->Show(value); - break; - } - case MENU_ID_WINDOW_DUMP: - { - const bool value = !m_config.data().show_dump; - m_config.data().show_dump = value; - m_dump_window->Show(value); - break; - } - case MENU_ID_WINDOW_BREAKPOINTS: - { - const bool value = !m_config.data().show_breakpoints; - m_config.data().show_breakpoints = value; - m_breakpoint_window->Show(value); - break; - } - case MENU_ID_WINDOW_MODULE: - { - const bool value = !m_config.data().show_modules; - m_config.data().show_modules = value; - m_module_window->Show(value); - break; - } - case MENU_ID_WINDOW_SYMBOL: - { - const bool value = !m_config.data().show_symbols; - m_config.data().show_symbols = value; - m_symbol_window->Show(value); - break; - } - default: - return; - } - - m_config.Save(); -} - -void DebuggerWindow2::OnUpdateView(wxCommandEvent& event) -{ - UpdateModuleLabel(); - m_disasm_ctrl->OnUpdateView(); - m_register_window->OnUpdateView(); - m_breakpoint_window->OnUpdateView(); -} - -void DebuggerWindow2::OnExit(wxCommandEvent& event) -{ - this->Close(); -} - -void DebuggerWindow2::OnShow(wxShowEvent& event) -{ - m_register_window->Show(); - m_dump_window->Show(); - m_breakpoint_window->Show(); - m_module_window->Show(); - m_symbol_window->Show(); - event.Skip(); -} - -void DebuggerWindow2::CreateMenuBar() -{ - auto menu_bar = new wxMenuBar; - - // file - wxMenu* file_menu = new wxMenu; - file_menu->Append(MENU_ID_FILE_EXIT, _("&Exit")); - file_menu->Bind(wxEVT_MENU, &DebuggerWindow2::OnExit, this); - - menu_bar->Append(file_menu, _("&File")); - - // options - wxMenu* options_menu = new wxMenu; - options_menu->Append(MENU_ID_OPTIONS_PIN_TO_MAINWINDOW, _("&Pin to main window"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().pin_to_main); - options_menu->Append(MENU_ID_OPTIONS_BREAK_ON_START, _("Break on &entry point"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().break_on_start); - options_menu->Append(MENU_ID_OPTIONS_LOG_MEMORY_BREAKPOINTS, _("Log only memory breakpoints"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().log_memory_breakpoints); - options_menu->Append(MENU_ID_OPTIONS_SWITCH_CPU_MODE, _("Switch to &interpreter CPU mode"), wxEmptyString, wxITEM_CHECK); - menu_bar->Append(options_menu, _("&Options")); - - // window - wxMenu* window_menu = new wxMenu; - window_menu->Append(MENU_ID_WINDOW_REGISTERS, _("&Registers"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().show_register); - window_menu->Append(MENU_ID_WINDOW_DUMP, _("&Memory Dump"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().show_dump); - window_menu->Append(MENU_ID_WINDOW_BREAKPOINTS, _("&Breakpoints"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().show_breakpoints); - window_menu->Append(MENU_ID_WINDOW_MODULE, _("Module&list"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().show_modules); - window_menu->Append(MENU_ID_WINDOW_SYMBOL, _("&Symbols"), wxEmptyString, wxITEM_CHECK)->Check(m_config.data().show_symbols); - - menu_bar->Append(window_menu, _("&Window")); - - SetMenuBar(menu_bar); - - options_menu->Bind(wxEVT_MENU, &DebuggerWindow2::OnOptionsInput, this); - window_menu->Bind(wxEVT_MENU, &DebuggerWindow2::OnWindowMenu, this); -} - -void DebuggerWindow2::UpdateModuleLabel(uint32 address) -{ - if (address == 0) - address = m_disasm_ctrl->GetViewBaseAddress(); - - RPLModule* module = RPLLoader_FindModuleByCodeAddr(address); - if (module && m_module_address != module->regionMappingBase_text.GetMPTR()) - { - m_module_label->SetLabel(wxString::Format("> %s", module->moduleName.c_str())); - m_module_address = module->regionMappingBase_text.GetMPTR(); - } - else if (address >= mmuRange_CODECAVE.getBase() && address < mmuRange_CODECAVE.getEnd()) - { - m_module_label->SetLabel(wxString::Format("> %s", "Cemu codecave")); - m_module_address = mmuRange_CODECAVE.getBase(); - } -} - -void DebuggerWindow2::UpdateViewThreadsafe() -{ - auto* evt = new wxCommandEvent(wxEVT_UPDATE_VIEW); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::NotifyDebugBreakpointHit() -{ - auto* evt = new wxCommandEvent(wxEVT_BREAKPOINT_HIT); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::NotifyRun() -{ - auto* evt = new wxCommandEvent(wxEVT_RUN); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::MoveToAddressInDisassembly(MPTR address) -{ - auto* evt = new wxCommandEvent(wxEVT_MOVE_TO_DISASM_ADDR); - evt->SetClientData((void*)(uintptr_t)address); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::NotifyGraphicPacksModified() -{ - auto* evt = new wxCommandEvent(wxEVT_NOTIFY_GRAPHIC_PACKS_MODIFIED); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::NotifyModuleLoaded(RPLModule* module) -{ - DebuggerModuleInfoNotify* notifData = new DebuggerModuleInfoNotify(module); - auto* evt = new wxCommandEvent(wxEVT_NOTIFY_MODULE_LOADED); - evt->SetClientObject(notifData); - wxQueueEvent(this, evt); -} - -void DebuggerWindow2::NotifyModuleUnloaded(RPLModule* module) -{ - DebuggerModuleInfoNotify* notifData = new DebuggerModuleInfoNotify(module); - auto* evt = new wxCommandEvent(wxEVT_NOTIFY_MODULE_UNLOADED); - evt->SetClientObject(notifData); - wxQueueEvent(this, evt); -} diff --git a/src/gui/wxgui/debugger/DebuggerWindow2.h b/src/gui/wxgui/debugger/DebuggerWindow2.h deleted file mode 100644 index 83cc5edb..00000000 --- a/src/gui/wxgui/debugger/DebuggerWindow2.h +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include "wxgui/debugger/DisasmCtrl.h" -#include "config/XMLConfig.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/HW/Espresso/Debugger/GDBStub.h" - -#include -#include -#include - -class BreakpointWindow; -class RegisterWindow; -class DumpWindow; -class ModuleWindow; -class SymbolWindow; -class wxStaticText; -struct RPLStoredSymbol; - -wxDECLARE_EVENT(wxEVT_UPDATE_VIEW, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_BREAKPOINT_HIT, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_RUN, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_BREAKPOINT_CHANGE, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_MOVE_TO_DISASM_ADDR, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_NOTIFY_MODULE_LOADED, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_NOTIFY_MODULE_UNLOADED, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_NOTIFY_GRAPHIC_PACKS_MODIFIED, wxCommandEvent); - -extern class DebuggerWindow2* s_debuggerWindow; - -struct DebuggerConfig -{ - DebuggerConfig() - : pin_to_main(true), break_on_start(true), log_memory_breakpoints(false), show_register(true), show_dump(true), show_stack(true), show_breakpoints(true), show_modules(true), show_symbols(true) {} - - bool pin_to_main; - bool break_on_start; - bool log_memory_breakpoints; - - bool show_register; - bool show_dump; - bool show_stack; - bool show_breakpoints; - bool show_modules; - bool show_symbols; - - void Load(XMLConfigParser& parser); - void Save(XMLConfigParser& parser); -}; -typedef XMLDataConfig XMLDebuggerConfig; - -struct DebuggerModuleInfo -{ - struct ModuleArea - { - MPTR base; - MPTR origBase; - uint32 size; - - bool ContainsAddress(MPTR addr) const - { - return addr >= base && (addr < (base+size)); - } - - bool ContainsOriginalAddress(MPTR addr) const - { - return addr >= origBase && (addr < (origBase + size)); - } - }; - - std::string moduleName; - uint32 patchCRC; - ModuleArea textArea; - ModuleArea dataArea; - - DebuggerModuleInfo(RPLModule* module); -}; - -struct DebuggerModuleStorage -{ - DebuggerModuleInfo moduleInfo; - bool delete_breakpoints_after_saving; - std::vector loaded_map_symbols; - - void Load(XMLConfigParser& parser); - void Save(XMLConfigParser& parser); -}; -typedef XMLDataConfig XMLDebuggerModuleConfig; - -static wxBitmap LoadThemedBitmapFromPNG(const uint8* data, size_t size, const wxColour& tint) -{ - wxMemoryInputStream strm(data, size); - wxImage img(strm, wxBITMAP_TYPE_PNG); - img.Replace(0x00, 0x00, 0x00, tint.Red(), tint.Green(), tint.Blue()); - return wxBitmap(img); -} - -class DebuggerWindow2 : public wxFrame, public DebuggerCallbacks -{ -public: - void CreateToolBar(); - void LoadModuleStorage(const struct DebuggerModuleInfo& moduleInfo); - void SaveModuleStorage(const struct DebuggerModuleInfo& moduleInfo, bool deleteModuleStorage); - DebuggerWindow2(wxFrame& parent, const wxRect& display_size); - ~DebuggerWindow2(); - void CleanupForDestroy(); - - void OnParentMove(const wxPoint& position, const wxSize& size); - void OnGameLoaded(); - - XMLDebuggerConfig& GetConfig(); - - bool Show(bool show = true) override; - std::wstring GetModuleStoragePath(std::string module_name, uint32_t crc_hash) const; - -private: - void OnBreakpointHit(wxCommandEvent& event); - void OnRunProgram(wxCommandEvent& event); - void OnToolClicked(wxCommandEvent& event); - void OnBreakpointChange(wxCommandEvent& event); - void OnOptionsInput(wxCommandEvent& event); - void OnWindowMenu(wxCommandEvent& event); - void OnUpdateView(wxCommandEvent& event); - void OnExit(wxCommandEvent& event); - void OnShow(wxShowEvent& event); - void OnClose(wxCloseEvent& event); - void OnMoveToDisasmAddr(wxCommandEvent& event); - void OnNotifyModuleLoaded(wxCommandEvent& event); - void OnNotifyModuleUnloaded(wxCommandEvent& event); - void OnNotifyGraphicPacksModified(wxCommandEvent& event); - // events from DisasmCtrl - void OnDisasmCtrlGotoAddress(wxCommandEvent& event); - - void CreateMenuBar(); - void UpdateModuleLabel(uint32 address = 0); - void LoadModuleMap(DebuggerModuleStorage& moduleStorage); - void UnloadModuleMap(DebuggerModuleStorage& moduleStorage); - std::wstring GetModuleMapPath(std::string module_name, uint32_t crc_hash) const; - - void UpdateViewThreadsafe() override; - void NotifyDebugBreakpointHit() override; - void NotifyRun() override; - void MoveToAddressInDisassembly(MPTR address) override; - void NotifyGraphicPacksModified() override; - void NotifyModuleLoaded(struct RPLModule* module) override; - void NotifyModuleUnloaded(struct RPLModule* module) override; - - XMLDebuggerConfig m_config; - std::vector> m_modulesStorage; - - wxPoint m_main_position; - wxSize m_main_size; - - RegisterWindow* m_register_window; - DumpWindow* m_dump_window; - BreakpointWindow* m_breakpoint_window; - ModuleWindow* m_module_window; - SymbolWindow* m_symbol_window; - - DisasmCtrl* m_disasm_ctrl; - - wxToolBar* m_toolbar; - wxBitmap m_run, m_pause; - - uint32 m_module_address; - wxStaticText* m_module_label; - - bool m_forceInterpreter{false}; - -wxDECLARE_EVENT_TABLE(); -}; - diff --git a/src/gui/wxgui/debugger/DisasmCtrl.cpp b/src/gui/wxgui/debugger/DisasmCtrl.cpp deleted file mode 100644 index 913df026..00000000 --- a/src/gui/wxgui/debugger/DisasmCtrl.cpp +++ /dev/null @@ -1,895 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/DisasmCtrl.h" - -#include "wxHelper.h" -#include "Cafe/OS/RPL/rpl_structs.h" -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/OS/RPL/rpl_symbol_storage.h" -#include "Cafe/OS/RPL/rpl_debug_symbols.h" -#include "Cemu/PPCAssembler/ppcAssembler.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "wxgui/debugger/DebuggerWindow2.h" -#include "util/helpers/helpers.h" - -#include "Cemu/ExpressionParser/ExpressionParser.h" -#include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h" - -#include - -wxDEFINE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); - -#define MAX_SYMBOL_LEN (120) - - -constexpr uint8 arrowRightPNG[] = -{ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0B, - 0x08, 0x03, 0x00, 0x00, 0x00, 0x41, 0x3C, 0xFD, 0x0B, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4C, 0x54, 0x45, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xA5, 0x67, 0xB9, 0xCF, 0x00, 0x00, 0x00, 0x02, - 0x74, 0x52, 0x4E, 0x53, 0xFF, 0x00, 0xE5, 0xB7, 0x30, 0x4A, 0x00, 0x00, - 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC3, 0x00, 0x00, - 0x0E, 0xC3, 0x01, 0xC7, 0x6F, 0xA8, 0x64, 0x00, 0x00, 0x00, 0x2C, 0x49, - 0x44, 0x41, 0x54, 0x18, 0x57, 0x63, 0x60, 0x84, 0x03, 0x08, 0x13, 0x59, - 0x00, 0xCC, 0x46, 0x11, 0x00, 0x71, 0x80, 0x24, 0x32, 0xC0, 0x10, 0x60, - 0xC0, 0x10, 0xC0, 0x00, 0x58, 0xCC, 0x80, 0xD8, 0x00, 0x02, 0x60, 0x3E, - 0x7E, 0x77, 0x00, 0x31, 0x23, 0x23, 0x00, 0x21, 0x95, 0x00, 0x5B, 0x20, - 0x73, 0x8D, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, - 0x42, 0x60, 0x82 -}; -std::optional g_ipArrowBitmap; - - -static wxColour theme_textForeground; -static wxColour theme_textForegroundMuted; -static wxColour theme_background; - -// background colors for lines in the disassembly view -static wxColour theme_lineBreakpointAndCurrentInstruction; -static wxColour theme_lineCurrentInstruction; -static wxColour theme_lineBreakpointSet; -static wxColour theme_lineLoggingBreakpointSet; -static wxColour theme_lineLastGotoAddress; - -// text colors for addresses in the disassembly view -static wxColour theme_syntaxGPR; -static wxColour theme_syntaxFPR; -static wxColour theme_syntaxSPR; -static wxColour theme_syntaxCR; -static wxColour theme_syntaxIMM; -static wxColour theme_syntaxIMMOffset; -static wxColour theme_syntaxCallIMM; -static wxColour theme_syntaxPseudoOrUnknown; -static wxColour theme_syntaxSymbol; - -static wxColour theme_opCode; -static wxColour theme_typeData; -static wxColour theme_patchedOpCode; -static wxColour theme_patchedData; - - -static void InitSyntaxColors() -{ - theme_textForeground = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - theme_textForegroundMuted = wxSystemSettings::SelectLightDark(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT), wxColour(140, 142, 145)); - theme_background = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - - // line background highlights - theme_lineBreakpointSet = wxSystemSettings::SelectLightDark(wxColour(0xFF,0x80,0x80), wxColour(0x84,0x21,0x21)); // red for a non-current breakpoint - theme_lineBreakpointAndCurrentInstruction = wxSystemSettings::SelectLightDark(wxColour(0xD2,0x7E,0xD2), wxColour(0x91,0x44,0xA4)); // pink for a current breakpoint - theme_lineCurrentInstruction = wxSystemSettings::SelectLightDark(wxColour(0xFF,0xA0,0x80), wxColour(0x87,0x53,0x1A)); // light orange - theme_lineLoggingBreakpointSet = wxSystemSettings::SelectLightDark(wxColour(0xAB,0xED,0xEE), wxColour(0x25,0x5D,0x6D)); // light blue - theme_lineLastGotoAddress = wxSystemSettings::SelectLightDark(wxColour(0xE0,0xE0,0xE0), wxColour(0x40,0x40,0x40)); // very light gray to indicate the line that was last jumped to - - // disassembly syntax colors - theme_syntaxGPR = wxSystemSettings::SelectLightDark(wxColour(0x66,0x00,0x00), wxColour(0xE0,0x6C,0x75)); - theme_syntaxFPR = wxSystemSettings::SelectLightDark(wxColour(0x66,0x66,0x00), wxColour(0xD1,0x9A,0x66)); - theme_syntaxSPR = wxSystemSettings::SelectLightDark(wxColour(0x00,0x66,0x66), wxColour(0x56,0xB6,0xC2)); - theme_syntaxCR = wxSystemSettings::SelectLightDark(wxColour(0x00,0x66,0x66), wxColour(0x56,0xB6,0xC2)); - theme_syntaxIMM = wxSystemSettings::SelectLightDark(wxColour(0x00,0x66,0x00), wxColour(0x9D,0xDE,0x6F)); - theme_syntaxIMMOffset = wxSystemSettings::SelectLightDark(wxColour(0x00,0x66,0x00), wxColour(0x9D,0xDE,0x6F)); - theme_syntaxCallIMM = wxSystemSettings::SelectLightDark(wxColour(0x00,0x00,0x88), wxColour(0x61,0xAF,0xEF)); - theme_syntaxPseudoOrUnknown = wxSystemSettings::SelectLightDark(wxColour(0xA0,0xA0,0xA0), wxColour(0x5C,0x63,0x70)); - theme_syntaxSymbol = wxSystemSettings::SelectLightDark(wxColour(0xA0,0x00,0x00), wxColour(0xE0,0x6C,0x75)); - - // opcode & data highlighting - theme_opCode = wxSystemSettings::SelectLightDark(wxColour(0xFF,0x00,0x40), wxColour(0xFF,0x70,0x7B)); - theme_patchedOpCode = wxSystemSettings::SelectLightDark(wxColour(0xFF,0x20,0x20), wxColour(0xCC,0x52,0xF5)); - theme_typeData = wxSystemSettings::SelectLightDark(wxColour(0xFF,0x20,0x20), wxColour(0xCE,0x91,0x78)); - theme_patchedData = wxSystemSettings::SelectLightDark(wxColour(0xFF,0x20,0x20), wxColour(0xF4,0x43,0x36)); - - // theme the current instruction pointer arrow - g_ipArrowBitmap = wxHelper::LoadThemedBitmapFromPNG(arrowRightPNG, sizeof(arrowRightPNG), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); -} - - -#define OFFSET_ADDRESS (60) -#define OFFSET_ADDRESS_RELATIVE (90) -#define OFFSET_DISASSEMBLY (300) - -#define OFFSET_DISASSEMBLY_OPERAND (80) - -DisasmCtrl::DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style) - : TextList(parent, id, pos, size, style), m_mouse_line(-1), m_mouse_line_drawn(-1), m_active_line(-1) -{ - Init(); - - if (!g_ipArrowBitmap.has_value()) - { - InitSyntaxColors(); - } - - auto tooltip_sizer = new wxBoxSizer(wxVERTICAL); - tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString), 0, wxALL, 5); - m_tooltip_window->SetSizer(tooltip_sizer); - - Bind(wxEVT_MENU, &DisasmCtrl::OnContextMenuEntryClicked, this, IDContextMenu_ToggleBreakpoint, IDContextMenu_Last); -} - -void DisasmCtrl::Init() -{ - SelectCodeRegion(mmuRange_TEXT_AREA.getBase()); -} - -void DisasmCtrl::SelectCodeRegion(uint32 newAddress) -{ - if (newAddress >= mmuRange_TEXT_AREA.getBase() && newAddress < mmuRange_TEXT_AREA.getEnd()) - { - currentCodeRegionStart = MEMORY_CODEAREA_ADDR; - currentCodeRegionEnd = RPLLoader_GetMaxCodeOffset(); - currentCodeRegionEnd = std::max(currentCodeRegionEnd, currentCodeRegionStart + 0x1000); - } - MMURange* mmuRange = memory_getMMURangeByAddress(newAddress); - if (mmuRange) - { - currentCodeRegionStart = mmuRange->getBase(); - currentCodeRegionEnd = mmuRange->getEnd(); - } - else - { - currentCodeRegionStart = 0; - currentCodeRegionEnd = 0; - } - - // update line tracking - sint32 element_count = currentCodeRegionEnd - currentCodeRegionStart; - // if (element_count <= 0x00010000) - // element_count = 0x00010000; - - if (this->SetElementCount(element_count / 4)) - { - Scroll(0, 0); - RefreshControl(); - } -} - -void DisasmCtrl::DrawDisassemblyLine(wxDC& dc, const wxPoint& linePosition, MPTR virtualAddress, RPLModule* rplModule) -{ - wxPoint position = linePosition; - - bool hasPatch = debugger_hasPatch(virtualAddress); - - PPCDisassembledInstruction disasmInstr; - - const DebuggerBreakpoint* bp = debugger_getFirstBP(virtualAddress); - while (bp) - { - if (bp->isExecuteBP() && bp->enabled) - break; - bp = bp->next; - } - - uint32 opcode; - - if (bp) - opcode = bp->originalOpcodeValue; - else - opcode = memory_readU32(virtualAddress); - - ppcAssembler_disassemble(virtualAddress, opcode, &disasmInstr); - - bool hasActiveBP = false; - PPCInterpreter_t* sessionCpu = debugger_lockDebugSession(); - if (sessionCpu) - { - hasActiveBP = sessionCpu->instructionPointer == virtualAddress; - debugger_unlockDebugSession(sessionCpu); - } - - // write virtual address - wxColour background_colour; - if (hasActiveBP && bp != nullptr) - background_colour = theme_lineBreakpointAndCurrentInstruction; - else if (hasActiveBP) - background_colour = theme_lineCurrentInstruction; - else if (bp != nullptr) - background_colour = bp->bpType == DEBUGGER_BP_T_NORMAL ? theme_lineBreakpointSet : theme_lineLoggingBreakpointSet; - else if(virtualAddress == m_lastGotoTarget) - background_colour = theme_lineLastGotoAddress; - else - background_colour = theme_background; - - DrawLineBackground(dc, position, background_colour); - - dc.SetTextForeground(theme_textForeground); - dc.DrawText(wxString::Format("%08x", virtualAddress), position); - position.x += OFFSET_ADDRESS; - - dc.SetTextForeground(theme_textForegroundMuted); - if (rplModule) - dc.DrawText(wxString::Format("+0x%-8x", virtualAddress - rplModule->regionMappingBase_text.GetMPTR()), position); - else - dc.DrawText("???", position); - - position.x += OFFSET_ADDRESS_RELATIVE; - - // draw arrow to clearly indicate instruction pointer - if(hasActiveBP) - dc.DrawBitmap(*g_ipArrowBitmap, wxPoint(position.x - 24, position.y + 2), false); - - // handle data symbols - auto debugSymbolDataType = DebugSymbolStorage::GetDataType(virtualAddress); - if (debugSymbolDataType == DEBUG_SYMBOL_TYPE::FLOAT) - { - dc.SetTextForeground(hasPatch ? theme_patchedData : theme_typeData); - dc.DrawText(fmt::format(".float"), position); - - position.x += OFFSET_DISASSEMBLY_OPERAND; - dc.SetTextForeground(hasPatch ? theme_patchedData : theme_syntaxIMM); - dc.DrawText(fmt::format("{}", memory_readFloat(virtualAddress)), position); - - return; - } - else if (debugSymbolDataType == DEBUG_SYMBOL_TYPE::U32) - { - dc.SetTextForeground(hasPatch ? theme_patchedData : theme_typeData); - dc.DrawText(fmt::format(".uint"), position); - - position.x += OFFSET_DISASSEMBLY_OPERAND; - dc.SetTextForeground(hasPatch ? theme_patchedData : theme_syntaxIMM); - dc.DrawText(fmt::format("{}", memory_readU32(virtualAddress)), position); - - return; - } - - sint32 start_width = position.x; - dc.SetTextForeground(hasPatch ? theme_patchedOpCode : theme_opCode); - std::string opName = ppcAssembler_getInstructionName(disasmInstr.ppcAsmCode); - std::transform(opName.begin(), opName.end(), opName.begin(), tolower); - dc.DrawText(wxString::Format("%-12s", opName.c_str()), position); - position.x += OFFSET_DISASSEMBLY_OPERAND; - - bool isRLWINM = disasmInstr.ppcAsmCode == PPCASM_OP_RLWINM || disasmInstr.ppcAsmCode == PPCASM_OP_RLWINM_ || - disasmInstr.ppcAsmCode == PPCASM_OP_CLRLWI || disasmInstr.ppcAsmCode == PPCASM_OP_CLRLWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_CLRRWI || disasmInstr.ppcAsmCode == PPCASM_OP_CLRRWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_EXTLWI || disasmInstr.ppcAsmCode == PPCASM_OP_EXTLWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_EXTRWI || disasmInstr.ppcAsmCode == PPCASM_OP_EXTRWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_SLWI || disasmInstr.ppcAsmCode == PPCASM_OP_SLWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_SRWI || disasmInstr.ppcAsmCode == PPCASM_OP_SRWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_ROTRWI || disasmInstr.ppcAsmCode == PPCASM_OP_ROTRWI_ || - disasmInstr.ppcAsmCode == PPCASM_OP_ROTLWI || disasmInstr.ppcAsmCode == PPCASM_OP_ROTLWI_; - bool forceDecDisplay = isRLWINM; - - if (disasmInstr.ppcAsmCode == PPCASM_OP_UKN) - { - // show raw bytes - WriteText(dc, wxString::Format("%02x %02x %02x %02x", (opcode >> 24) & 0xFF, (opcode >> 16) & 0xFF, (opcode >> 8) & 0xFF, (opcode >> 0) & 0xFF), position, theme_syntaxPseudoOrUnknown); - } - - bool is_first_operand = true; - for (sint32 o = 0; o < PPCASM_OPERAND_COUNT; o++) - { - if (((disasmInstr.operandMask >> o) & 1) == 0) - continue; - - if (!is_first_operand) - WriteText(dc, ", ", position, theme_textForeground); - - is_first_operand = false; - switch (disasmInstr.operand[o].type) - { - case PPCASM_OPERAND_TYPE_GPR: - WriteText(dc, wxString::Format("r%d", disasmInstr.operand[o].registerIndex), position, theme_syntaxGPR); - break; - - case PPCASM_OPERAND_TYPE_FPR: - WriteText(dc, wxString::Format("f%d", disasmInstr.operand[o].registerIndex), position, theme_syntaxFPR); - break; - - case PPCASM_OPERAND_TYPE_SPR: - WriteText(dc, wxString::Format("spr%d", disasmInstr.operand[o].registerIndex), position, theme_syntaxSPR); - break; - - case PPCASM_OPERAND_TYPE_CR: - WriteText(dc, wxString::Format("cr%d", disasmInstr.operand[o].registerIndex), position, theme_syntaxCR); - break; - - case PPCASM_OPERAND_TYPE_IMM: - { - wxString string; - if (disasmInstr.operand[o].isSignedImm) - { - sint32 sImm = disasmInstr.operand[o].immS32; - if (disasmInstr.operand[o].immWidth == 16 && (sImm & 0x8000)) - sImm |= (sint32)0xFFFF0000; - - if ((sImm > -10 && sImm < 10) || forceDecDisplay) - string = wxString::Format("%d", sImm); - else - { - if (sImm < 0) - string = wxString::Format("-0x%x", -sImm); - else - string = wxString::Format("0x%x", sImm); - } - } - else - { - uint32 uImm = disasmInstr.operand[o].immS32; - if ((uImm >= 0 && uImm < 10) || forceDecDisplay) - string = wxString::Format("%u", uImm); - else - string = wxString::Format("0x%x", uImm); - } - - WriteText(dc, string, position, theme_syntaxIMM); - break; - } - case PPCASM_OPERAND_TYPE_PSQMODE: - { - if (disasmInstr.operand[o].immS32) - WriteText(dc, "single", position, theme_syntaxIMM); - else - WriteText(dc, "paired", position, theme_syntaxIMM); - break; - } - case PPCASM_OPERAND_TYPE_CIMM: - { - wxString string; - // use symbol for function calls if available - uint32 callDest = disasmInstr.operand[o].immU32; - RPLStoredSymbol* storedSymbol = nullptr; - if (disasmInstr.ppcAsmCode == PPCASM_OP_BL || disasmInstr.ppcAsmCode == PPCASM_OP_BLA) - storedSymbol = rplSymbolStorage_getByAddress(callDest); - - if (storedSymbol) - { - // if symbol is within same module then don't display libname prefix - RPLModule* rplModuleCurrent = RPLLoader_FindModuleByCodeAddr(virtualAddress); // cache this - if (rplModuleCurrent && callDest >= rplModuleCurrent->regionMappingBase_text.GetMPTR() && callDest < (rplModuleCurrent->regionMappingBase_text.GetMPTR() + rplModuleCurrent->regionSize_text)) - string = wxString((char*)storedSymbol->symbolName); - else - string = wxString::Format("%s.%s", (char*)storedSymbol->libName, (char*)storedSymbol->symbolName); - - // truncate name after 36 characters - if (string.Length() >= 36) - { - string.Truncate(34); - string.Append(".."); - } - } - else - { - string = wxString::Format("0x%08x", disasmInstr.operand[o].immU32); - } - - WriteText(dc, string, position, theme_syntaxCallIMM); - - if (disasmInstr.ppcAsmCode != PPCASM_OP_BL) - { - wxString x = wxString(" "); - if (callDest <= virtualAddress) - x.Append(wxUniChar(0x2191)); // arrow up - else - x.Append(wxUniChar(0x2193)); // arrow down - - WriteText(dc, x, position, theme_textForeground); - } - - break; - } - case PPCASM_OPERAND_TYPE_MEM: - { - // offset - wxString string; - if (disasmInstr.operand[o].isSignedImm && disasmInstr.operand[o].immS32 >= 0) - string = wxString::Format("+0x%x", disasmInstr.operand[o].immS32); - else - string = wxString::Format("-0x%x", -disasmInstr.operand[o].immS32); - - WriteText(dc, string, position, theme_syntaxIMMOffset); - WriteText(dc, "(", position, theme_textForeground); - - // register - WriteText(dc, wxString::Format("r%d", disasmInstr.operand[o].registerIndex), position, theme_syntaxGPR); - WriteText(dc, ")", position, theme_textForeground); - break; - } - default: - // TODO - WriteText(dc, "", position, wxColour(0xFF444444)); - } - } - - position.x = start_width + OFFSET_DISASSEMBLY; - const auto comment = static_cast(rplDebugSymbol_getForAddress(virtualAddress)); - if (comment && comment->type == RplDebugSymbolComment) - WriteText(dc, comment->comment, position, theme_textForeground); - else if (isRLWINM) - { - sint32 rS, rA, SH, MB, ME; - rS = (opcode >> 21) & 0x1f; - rA = (opcode >> 16) & 0x1f; - SH = (opcode >> 11) & 0x1f; - MB = (opcode >> 6) & 0x1f; - ME = (opcode >> 1) & 0x1f; - - uint32 mask = ppcAssembler_generateMaskRLW(MB, ME); - - wxString string; - if (SH == 0) - string = wxString::Format("r%d=r%d&0x%x", rA, rS, mask); - else if ((0xFFFFFFFF << (uint32)disasmInstr.operand[2].immS32) == mask) - string = wxString::Format("r%d=r%d<<%d", rA, rS, SH); - else if ((0xFFFFFFFF >> (32 - SH) == mask)) - string = wxString::Format("r%d=r%d>>%d", rA, rS, 32 - SH); - else - string = wxString::Format("r%d=(r%d<<<%d)&0x%x", rA, rS, SH, mask); - WriteText(dc, string, position, theme_textForegroundMuted); - } - else if (disasmInstr.ppcAsmCode == PPCASM_OP_SUBF || disasmInstr.ppcAsmCode == PPCASM_OP_SUBF_) - { - sint32 rD, rA, rB; - rD = (opcode >> 21) & 0x1f; - rA = (opcode >> 16) & 0x1f; - rB = (opcode >> 11) & 0x1f; - - wxString string; - string = wxString::Format("r%d=r%d-r%d", rD, rB, rA); - WriteText(dc, string, position, theme_textForegroundMuted); - } -} - -void DisasmCtrl::DrawLabelName(wxDC& dc, const wxPoint& linePosition, MPTR virtualAddress, RPLStoredSymbol* storedSymbol) -{ - wxString symbol_string = wxString::Format("%s:", (char*)storedSymbol->symbolName); - if (symbol_string.Length() > MAX_SYMBOL_LEN) - { - symbol_string.Truncate(MAX_SYMBOL_LEN - 3); - symbol_string.Append("..:"); - } - wxPoint tmpPos(linePosition); - WriteText(dc, symbol_string, tmpPos, theme_syntaxSymbol); -} - -void DisasmCtrl::OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) -{ - wxPoint position(0, 0); - - RPLModule* current_rpl_module = RPLLoader_FindModuleByCodeAddr(GetViewBaseAddress()); - - if (currentCodeRegionStart == currentCodeRegionEnd) - return; - - sint32 viewFirstLine = GetViewStart().y; - sint32 lineOffset = start - viewFirstLine; - - cemu_assert_debug(lineOffset >= 0); - - sint32 instructionIndex = 0; - sint32 numLinesToUpdate = lineOffset + count; - numLinesToUpdate = std::min(numLinesToUpdate, (sint32)m_elements_visible); - - if(m_lineToAddress.size() != m_elements_visible) - m_lineToAddress.resize(m_elements_visible); - - sint32 lineIndex = 0; - while(lineIndex < numLinesToUpdate) - { - const uint32 virtualAddress = GetViewBaseAddress() + instructionIndex * 4; - instructionIndex++; - - if (virtualAddress < currentCodeRegionStart || - virtualAddress >= currentCodeRegionEnd) - { - NextLine(position, &start_position); - m_lineToAddress[lineIndex] = std::nullopt; - lineIndex++; - continue; - } - - // check if valid memory address - if (!memory_isAddressRangeAccessible(virtualAddress, 4)) - { - NextLine(position, &start_position); - m_lineToAddress[lineIndex] = std::nullopt; - lineIndex++; - continue; - } - - // draw symbol as label - RPLStoredSymbol* storedSymbol = rplSymbolStorage_getByAddress(virtualAddress); - if (storedSymbol) - { - if(lineIndex >= lineOffset) - DrawLabelName(dc, position, virtualAddress, storedSymbol); - m_lineToAddress[lineIndex] = virtualAddress; - lineIndex++; - if (lineIndex >= numLinesToUpdate) - break; - NextLine(position, &start_position); - } - m_lineToAddress[lineIndex] = virtualAddress; - if (lineIndex >= lineOffset) - DrawDisassemblyLine(dc, position, virtualAddress, current_rpl_module); - NextLine(position, &start_position); - lineIndex++; - } - - // draw vertical separator lines: offset | disassembly | comment - dc.SetPen(*wxLIGHT_GREY_PEN); - - wxPoint line_from = start_position; - line_from.x = OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE - 5; - wxPoint line_to = line_from; - line_to.y += (count + 1) * m_line_height; - dc.DrawLine(line_from, line_to); - - line_from.x += OFFSET_DISASSEMBLY; - line_to.x += OFFSET_DISASSEMBLY; - dc.DrawLine(line_from, line_to); -} - -void DisasmCtrl::OnMouseMove(const wxPoint& start_position, uint32 line) -{ - /*m_mouse_line = line; - if (m_mouse_line_drawn != -1) - RefreshLine(m_mouse_line_drawn); - if (m_mouse_line != -1) - RefreshLine(m_mouse_line);*/ - - wxPoint position = start_position; - - // address - if (position.x <= OFFSET_ADDRESS) - { - return; - } - - position.x -= OFFSET_ADDRESS; - - // relative offset - if (position.x <= OFFSET_ADDRESS_RELATIVE) - { - return; - } - - position.x -= OFFSET_ADDRESS_RELATIVE; - - // disassembly code - if (position.x <= OFFSET_DISASSEMBLY) - { - if(m_mouse_down) - { - - } - - if (position.x <= OFFSET_DISASSEMBLY_OPERAND) - { - return; - } - - position.x -= OFFSET_DISASSEMBLY_OPERAND; - } -} - -void DisasmCtrl::OnKeyPressed(sint32 key_code, const wxPoint& position) -{ - auto optVirtualAddress = LinePixelPosToAddress(position.y); - switch (key_code) - { - case WXK_F9: - { - if (optVirtualAddress) - { - debugger_toggleExecuteBreakpoint(*optVirtualAddress); - - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); - wxPostEvent(this->m_parent, evt); - } - return; - } - case 'G': - { - if(IsKeyDown(WXK_CONTROL)) - { - GoToAddressDialog(); - return; - } - } - } - - // debugger currently in break state - PPCInterpreter_t* debugCpu = debugger_lockDebugSession(); - if (debugCpu) - { - switch (key_code) - { - case WXK_F5: - { - debugger_stepCommand(DebuggerStepCommand::Run); - break; - } - case WXK_F10: - { - debugger_stepCommand(DebuggerStepCommand::StepOver); - break; - } - case WXK_F11: - { - debugger_stepCommand(DebuggerStepCommand::StepInto); - break; - } - } - debugger_unlockDebugSession(debugCpu); - } - else - { - if (key_code == WXK_F5) - { - debugger_requestBreak(); - } - } -} - -void DisasmCtrl::OnMouseDClick(const wxPoint& position, uint32 line) -{ - wxPoint pos = position; - auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height); - if (!optVirtualAddress) - return; - MPTR virtualAddress = *optVirtualAddress; - - // address - if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE) - { - // address + address relative - debugger_toggleExecuteBreakpoint(virtualAddress); - RefreshLine(line); - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); - wxPostEvent(this->m_parent, evt); - return; - } - else if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + OFFSET_DISASSEMBLY) - { - // double-clicked on disassembly (operation and operand data) - wxString currentInstruction = wxEmptyString; - wxTextEntryDialog set_value_dialog(this, _("Enter a new instruction."), wxString::Format(_("Overwrite instruction at address %08x"), virtualAddress), currentInstruction); - if (set_value_dialog.ShowModal() == wxID_OK) - { - PPCAssemblerInOut ctx = { 0 }; - ctx.virtualAddress = virtualAddress; - if (ppcAssembler_assembleSingleInstruction(set_value_dialog.GetValue().c_str(), &ctx)) - { - debugger_createPatch(virtualAddress, { ctx.outputData.data(), ctx.outputData.size() }); - RefreshLine(line); - } - } - return; - } - else - { - // comment - const auto comment = static_cast(rplDebugSymbol_getForAddress(virtualAddress)); - - wxString old_comment = wxEmptyString; - if (comment && comment->type == RplDebugSymbolComment) - old_comment = comment->comment; - - wxTextEntryDialog set_value_dialog(this, _("Enter a new comment."), wxString::Format(_("Create comment at address %08x"), virtualAddress), old_comment); - if (set_value_dialog.ShowModal() == wxID_OK) - { - rplDebugSymbol_createComment(virtualAddress, set_value_dialog.GetValue().wc_str()); - RefreshLine(line); - } - return; - } -} - -void DisasmCtrl::CopyToClipboard(std::string text) -{ - if (wxClipboard::Get()->Open()) - { - wxClipboard::Get()->SetData(new wxTextDataObject(text)); - wxClipboard::Get()->Close(); - } -} - -static uint32 GetUnrelocatedAddress(MPTR address) -{ - RPLModule* rplModule = RPLLoader_FindModuleByCodeAddr(address); - if (!rplModule) - return 0; - if (address >= rplModule->regionMappingBase_text.GetMPTR() && address < (rplModule->regionMappingBase_text.GetMPTR() + rplModule->regionSize_text)) - return 0x02000000 + (address - rplModule->regionMappingBase_text.GetMPTR()); - return 0; -} - -void DisasmCtrl::OnContextMenu(const wxPoint& position, uint32 line) -{ - auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height); - if (!optVirtualAddress) - return; - MPTR virtualAddress = *optVirtualAddress; - m_contextMenuAddress = virtualAddress; - // show dialog - wxMenu menu; - menu.Append(IDContextMenu_ToggleBreakpoint, _("Toggle breakpoint")); - menu.Append(IDContextMenu_ToggleLoggingBreakpoint, _("Toggle logging point")); - if(debugger_hasPatch(virtualAddress)) - menu.Append(IDContextMenu_RestoreOriginalInstructions, _("Restore original instructions")); - menu.AppendSeparator(); - menu.Append(IDContextMenu_CopyAddress, _("Copy address")); - uint32 unrelocatedAddress = GetUnrelocatedAddress(virtualAddress); - if (unrelocatedAddress && unrelocatedAddress != virtualAddress) - menu.Append(IDContextMenu_CopyUnrelocatedAddress, _("Copy virtual address (for IDA/Ghidra)")); - PopupMenu(&menu); -} - -void DisasmCtrl::OnContextMenuEntryClicked(wxCommandEvent& event) -{ - switch(event.GetId()) - { - case IDContextMenu_ToggleBreakpoint: - { - debugger_toggleExecuteBreakpoint(m_contextMenuAddress); - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); - wxPostEvent(this->m_parent, evt); - break; - } - case IDContextMenu_ToggleLoggingBreakpoint: - { - debugger_toggleLoggingBreakpoint(m_contextMenuAddress); - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); - wxPostEvent(this->m_parent, evt); - break; - } - case IDContextMenu_RestoreOriginalInstructions: - { - debugger_removePatch(m_contextMenuAddress); - wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); // This also refreshes the disassembly view - wxPostEvent(this->m_parent, evt); - break; - } - case IDContextMenu_CopyAddress: - { - CopyToClipboard(fmt::format("{:#10x}", m_contextMenuAddress)); - break; - } - case IDContextMenu_CopyUnrelocatedAddress: - { - uint32 unrelocatedAddress = GetUnrelocatedAddress(m_contextMenuAddress); - CopyToClipboard(fmt::format("{:#10x}", unrelocatedAddress)); - break; - } - default: - UNREACHABLE; - } -} - -bool DisasmCtrl::OnShowTooltip(const wxPoint& position, uint32 line) -{ - return false; -} - -void DisasmCtrl::ScrollWindow(int dx, int dy, const wxRect* prect) -{ - // scroll and repaint everything - RefreshControl(nullptr); - TextList::ScrollWindow(dx, dy, nullptr); -} - -wxSize DisasmCtrl::DoGetBestSize() const -{ - return TextList::DoGetBestSize(); -} - -void DisasmCtrl::OnUpdateView() -{ - RefreshControl(); -} - -uint32 DisasmCtrl::GetViewBaseAddress() const -{ - if (GetViewStart().y < 0) - return MPTR_NULL; - return currentCodeRegionStart + GetViewStart().y * 4; -} - -std::optional DisasmCtrl::LinePixelPosToAddress(sint32 posY) -{ - if (posY < 0) - return std::nullopt; - - sint32 lineIndex = posY / m_line_height; - if (lineIndex >= m_lineToAddress.size()) - return std::nullopt; - - return m_lineToAddress[lineIndex]; -} - -uint32 DisasmCtrl::AddressToScrollPos(uint32 offset) const -{ - return (offset - currentCodeRegionStart) / 4; -} - -void DisasmCtrl::CenterOffset(uint32 offset) -{ - if (offset < currentCodeRegionStart || offset >= currentCodeRegionEnd) - SelectCodeRegion(offset); - - const sint32 line = AddressToScrollPos(offset); - if (line < 0 || line >= (sint32)m_element_count) - return; - - if (m_active_line != -1) - RefreshLine(m_active_line); - - DoScroll(0, std::max(0, line - (sint32)(m_elements_visible / 2))); - - m_active_line = line; - RefreshLine(m_active_line); -} - -void DisasmCtrl::GoToAddressDialog() -{ - wxTextEntryDialog goto_dialog(this, _("Enter a target address."), _("GoTo address"), wxEmptyString); - if (goto_dialog.ShowModal() == wxID_OK) - { - ExpressionParser parser; - - auto value = goto_dialog.GetValue().ToStdString(); - std::transform(value.begin(), value.end(), value.begin(), tolower); - - // trim any leading spaces - while(!value.empty() && value[0] == ' ') - value.erase(value.begin()); - - debugger_addParserSymbols(parser); - - // try to parse expression as hex value first (it should interpret 1234 as 0x1234, not 1234) - if (parser.IsConstantExpression("0x"+value)) - { - const auto result = (uint32)parser.Evaluate("0x"+value); - m_lastGotoTarget = result; - CenterOffset(result); - wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); - evt.SetExtraLong(static_cast(result)); - wxPostEvent(GetParent(), evt); - } - else if (parser.IsConstantExpression(value)) - { - const auto result = (uint32)parser.Evaluate(value); - m_lastGotoTarget = result; - CenterOffset(result); - wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); - evt.SetExtraLong(static_cast(result)); - wxPostEvent(GetParent(), evt); - } - else - { - try - { - // if not a constant expression (i.e. relying on unknown variables), then evaluating will throw an exception with a detailed error message - const auto _ = (uint32)parser.Evaluate(value); - } - catch (const std::exception& ex) - { - wxMessageBox(ex.what(), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } - } - } -} diff --git a/src/gui/wxgui/debugger/DisasmCtrl.h b/src/gui/wxgui/debugger/DisasmCtrl.h deleted file mode 100644 index 61f7afa6..00000000 --- a/src/gui/wxgui/debugger/DisasmCtrl.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once -#include "wxgui/components/TextList.h" - -wxDECLARE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); // Notify parent that goto address operation completed. Event contains the address that was jumped to. - -class DisasmCtrl : public TextList -{ - enum - { - IDContextMenu_ToggleBreakpoint = wxID_HIGHEST + 1, - IDContextMenu_ToggleLoggingBreakpoint, - IDContextMenu_RestoreOriginalInstructions, - IDContextMenu_CopyAddress, - IDContextMenu_CopyUnrelocatedAddress, - IDContextMenu_Last - }; -public: - - DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style); - - void Init(); - wxSize DoGetBestSize() const override; - - void OnUpdateView(); - - uint32 GetViewBaseAddress() const; - std::optional LinePixelPosToAddress(sint32 posY); - - uint32 AddressToScrollPos(uint32 offset) const; - void CenterOffset(uint32 offset); - void GoToAddressDialog(); - -protected: - void OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) override; - void OnMouseMove(const wxPoint& position, uint32 line) override; - void OnKeyPressed(sint32 key_code, const wxPoint& position) override; - void OnMouseDClick(const wxPoint& position, uint32 line) override; - void OnContextMenu(const wxPoint& position, uint32 line) override; - void OnContextMenuEntryClicked(wxCommandEvent& event); - bool OnShowTooltip(const wxPoint& position, uint32 line) override; - void ScrollWindow(int dx, int dy, const wxRect* prect) override; - - void DrawDisassemblyLine(wxDC& dc, const wxPoint& linePosition, MPTR virtualAddress, struct RPLModule* rplModule); - void DrawLabelName(wxDC& dc, const wxPoint& linePosition, MPTR virtualAddress, struct RPLStoredSymbol* storedSymbol); - - void SelectCodeRegion(uint32 newAddress); - -private: - void CopyToClipboard(std::string text); - - sint32 m_mouse_line, m_mouse_line_drawn; - sint32 m_active_line; - uint32 m_lastGotoTarget{}; - uint32 m_contextMenuAddress{}; - // code region info - uint32 currentCodeRegionStart; - uint32 currentCodeRegionEnd; - // line to address mapping - std::vector> m_lineToAddress; -}; diff --git a/src/gui/wxgui/debugger/DumpCtrl.cpp b/src/gui/wxgui/debugger/DumpCtrl.cpp deleted file mode 100644 index 8bfb2dff..00000000 --- a/src/gui/wxgui/debugger/DumpCtrl.cpp +++ /dev/null @@ -1,441 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/DumpCtrl.h" -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/OS/RPL/rpl_structs.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "util/helpers/helpers.h" - -#include "Cemu/ExpressionParser/ExpressionParser.h" - - -#define OFFSET_ADDRESS (60) -#define OFFSET_ADDRESS_RELATIVE (90) -#define OFFSET_MEMORY (450) - -enum { - ID_WRITE_U8 = wxID_HIGHEST + 1, - ID_WRITE_U16, - ID_WRITE_U32, - ID_WRITE_FLOAT, - ID_WRITE_STRING -}; - -DumpCtrl::DumpCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style) - : TextList(parent, id, pos, size, style) -{ - MMURange* range = memory_getMMURangeByAddress(0x10000000); - if (range) - { - m_memoryRegion.baseAddress = range->getBase(); - m_memoryRegion.size = range->getSize(); - Init(); - } - else - { - m_memoryRegion.baseAddress = 0x10000000; - m_memoryRegion.size = 0x1000; - Init(); - } - - Bind(wxEVT_MENU, &DumpCtrl::OnMenuSelected, this); -} - -void DumpCtrl::Init() -{ - uint32 element_count = m_memoryRegion.size; - this->SetElementCount(element_count / 0x10); - Scroll(0, 0); - RefreshControl(); -} - -void DumpCtrl::OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) -{ - wxPoint position = start_position; - uint32 endAddr = m_memoryRegion.baseAddress + m_memoryRegion.size; - RPLModule* currentCodeRPL = RPLLoader_FindModuleByCodeAddr(LineToOffset(start)); - RPLModule* currentDataRPL = RPLLoader_FindModuleByDataAddr(LineToOffset(start)); - for (sint32 i = 0; i <= count; i++) - { - const uint32 virtual_address = LineToOffset(start + i); - - dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - dc.DrawText(wxString::Format("%08x", virtual_address), position); - position.x += OFFSET_ADDRESS; - - dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - if (currentCodeRPL) - { - dc.DrawText(wxString::Format("+0x%-8x", virtual_address - currentCodeRPL->regionMappingBase_text.GetMPTR()), position); - } - else if (currentDataRPL) - { - dc.DrawText(wxString::Format("+0x%-8x", virtual_address - currentDataRPL->regionMappingBase_data), position); - } - else - { - dc.DrawText("???", position); - } - - position.x += OFFSET_ADDRESS_RELATIVE; - - sint32 start_width = position.x; - - if (!memory_isAddressRangeAccessible(virtual_address, 0x10)) - { - for (sint32 f=0; f<0x10; f++) - { - wxPoint p(position); - WriteText(dc, wxString::Format("?? "), p); - position.x += (m_char_width * 3); - } - } - else - { - std::array data; - memory_readBytes(virtual_address, data); - for (auto b : data) - { - wxPoint p(position); - WriteText(dc, wxString::Format("%02x ", b), p); - position.x += (m_char_width * 3); - } - position.x = start_width = OFFSET_MEMORY; - dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - for (auto b : data) - { - if (isprint(b)) - dc.DrawText(wxString::Format("%c ", b), position); - else - dc.DrawText(".", position); - - position.x += m_char_width; - } - } - - // display goto indicator - if (m_lastGotoOffset >= virtual_address && m_lastGotoOffset < (virtual_address + 16)) - { - sint32 indicatorX = start_position.x + OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + (m_lastGotoOffset - virtual_address) * m_char_width * 3; - wxPoint line1(start_position.x + indicatorX - 2, position.y); - wxPoint line2(line1.x, line1.y + m_line_height); - dc.SetPen(*wxRED_PEN); - dc.DrawLine(line1, line2); - dc.DrawLine(line1, wxPoint(line1.x + 3, line1.y)); - dc.DrawLine(line2, wxPoint(line2.x + 3, line2.y)); - } - - NextLine(position, &start_position); - } - - // draw vertical separator lines for 4 byte blocks - sint32 cursorOffsetHexBytes = start_position.x + OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE; - wxPoint line_from( - cursorOffsetHexBytes + m_char_width * (3 * 4 - 1) + m_char_width / 2, - start_position.y - ); - wxPoint line_to(line_from.x, line_from.y + m_line_height * (count + 1)); - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_INACTIVECAPTIONTEXT)); - for (sint32 i = 0; i < 3; i++) - { - dc.DrawLine(line_from, line_to); - line_from.x += m_char_width * (3 * 4); - line_to.x += m_char_width * (3 * 4); - } -} - -void DumpCtrl::OnMouseMove(const wxPoint& start_position, uint32 line) -{ - wxPoint position = start_position; - - // address - if (position.x <= OFFSET_ADDRESS) - { - return; - } - - position.x -= OFFSET_ADDRESS; - - // relative offset - if (position.x <= OFFSET_ADDRESS_RELATIVE) - { - return; - } - - position.x -= OFFSET_ADDRESS_RELATIVE; - - // byte code - if (position.x <= OFFSET_MEMORY) - { - - } - - // string view - position.x -= OFFSET_MEMORY; -} - -uint32 DumpCtrl::PositionToAddress(const wxPoint& position, uint32 line) -{ - wxPoint pos = position; - - if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE) - return MPTR_NULL; - - pos.x -= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE; - - if (pos.x > OFFSET_MEMORY) - return MPTR_NULL; - - const uint32 byteIndex = (pos.x / m_char_width) / 3; - return LineToOffset(line) + byteIndex; -} - -void DumpCtrl::OnMouseDClick(const wxPoint& position, uint32 line) -{ - uint32 address = PositionToAddress(position, line); - - if (address == MPTR_NULL) - return; - - if (!memory_isAddressRangeAccessible(address, 1)) - return; - - if (WriteNumericDialog(address)) - { - wxRect updateRect(0, line * m_line_height, GetSize().x, m_line_height); - RefreshControl(&updateRect); - } -} - -void DumpCtrl::GoToAddressDialog() -{ - wxTextEntryDialog goto_dialog(this, _("Enter a target address."), _("GoTo address"), wxEmptyString); - if (goto_dialog.ShowModal() == wxID_OK) - { - ExpressionParser parser; - - auto value = goto_dialog.GetValue().ToStdString(); - std::transform(value.begin(), value.end(), value.begin(), tolower); - - debugger_addParserSymbols(parser); - - // try to parse expression as hex value first (it should interpret 1234 as 0x1234, not 1234) - if (parser.IsConstantExpression("0x"+value)) - { - const auto result = (uint32)parser.Evaluate("0x"+value); - m_lastGotoOffset = result; - CenterOffset(result); - } - else if (parser.IsConstantExpression(value)) - { - const auto result = (uint32)parser.Evaluate(value); - m_lastGotoOffset = result; - CenterOffset(result); - } - else - { - try - { - const auto _ = (uint32)parser.Evaluate(value); - } - catch (const std::exception& ex) - { - wxMessageBox(ex.what(), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - } - } - } -} - -void DumpCtrl::CenterOffset(uint32 offset) -{ - // check if the offset is valid - if (!memory_isAddressRangeAccessible(offset, 1)) - return; - // set region and line - MMURange* range = memory_getMMURangeByAddress(offset); - if (m_memoryRegion.baseAddress != range->getBase() || m_memoryRegion.size != range->getSize()) - { - m_memoryRegion.baseAddress = range->getBase(); - m_memoryRegion.size = range->getSize(); - Init(); - } - - const sint32 line = OffsetToLine(offset); - if (line < 0 || line >= (sint32)m_element_count) - return; - - DoScroll(0, std::max(0, line - ((sint32)m_elements_visible / 2))); - - RefreshControl(); - //RefreshLine(line); -} - -uint32 DumpCtrl::LineToOffset(uint32 line) -{ - return m_memoryRegion.baseAddress + line * 0x10; -} - -uint32 DumpCtrl::OffsetToLine(uint32 offset) -{ - return (offset - m_memoryRegion.baseAddress) / 0x10; -} - -void DumpCtrl::OnKeyPressed(sint32 key_code, const wxPoint& position) -{ - switch (key_code) - { - case 'G': - { - if (IsKeyDown(WXK_CONTROL)) - { - GoToAddressDialog(); - return; - } - } - } -} - -wxSize DumpCtrl::DoGetBestSize() const -{ - return TextList::DoGetBestSize(); -} - -void DumpCtrl::OnContextMenu(const wxPoint& position, uint32 line) -{ - const uint32 address = PositionToAddress(position, line); - - if (!memory_isAddressRangeAccessible(address, 1)) - return; - - m_writerContextAddress = address; - m_writerContextLine = line; - - wxMenu menu; - - menu.Append(ID_WRITE_U8, _("Write Byte")); - menu.Append(ID_WRITE_U16, _("Write Int16")); - menu.Append(ID_WRITE_U32, _("Write Int32")); - menu.Append(ID_WRITE_FLOAT, _("Write Float")); - menu.Append(ID_WRITE_STRING, _("Write String")); - - PopupMenu(&menu); -} - -void DumpCtrl::OnMenuSelected(wxCommandEvent& event) -{ - bool update = false; - switch (event.GetId()) - { - case ID_WRITE_U8: - update = WriteNumericDialog(m_writerContextAddress); - break; - case ID_WRITE_U16: - update = WriteNumericDialog(m_writerContextAddress); - break; - case ID_WRITE_U32: - update = WriteNumericDialog(m_writerContextAddress); - break; - case ID_WRITE_FLOAT: - update = WriteNumericDialog(m_writerContextAddress); - break; - case ID_WRITE_STRING: - update = WriteString(m_writerContextAddress); - break; - } - - if (update) - { - wxRect updateRect(0, m_writerContextLine * m_line_height, GetSize().x, m_line_height); - RefreshControl(&updateRect); - } -} - -template -bool DumpCtrl::WriteNumericDialog(uint32 address) -{ - static_assert( - std::is_same::value || - std::is_same::value || - std::is_same::value || - std::is_same::value, - "Unsupported type" - ); - - T value; - const char* dataType; - wxString label; - - if constexpr (std::is_same::value) - { - value = memory_readU8(address); - dataType = "byte"; - label = wxString::Format("0x%02x", value); - } - else if constexpr (std::is_same::value) - { - value = memory_readU16(address); - dataType = "int16"; - label = wxString::Format("0x%04x", value); - } - else if constexpr (std::is_same::value) - { - value = memory_readU32(address); - dataType = "int32"; - label = wxString::Format("0x%08x", value); - } - else if constexpr (std::is_same::value) - { - value = memory_readFloat(address); - dataType = "float"; - label = wxString::Format("%f", value); - } - - wxTextEntryDialog dialog( - this, - _("Enter a new value."), - wxString::Format(_("Write %s at address 0x%08x"), dataType, address), - label - ); - - if (dialog.ShowModal() != wxID_OK) - return false; - - - const T newValue = ConvertString(dialog.GetValue().ToStdString()); - - if constexpr (std::is_same::value) - memory_writeU8(address, newValue); - else if constexpr (std::is_same::value) - memory_writeU16(address, newValue); - else if constexpr (std::is_same::value) - memory_writeU32(address, newValue); - else if constexpr (std::is_same::value) - memory_writeFloat(address, newValue); - - return true; -} - -bool DumpCtrl::WriteString(uint32 address) -{ - wxTextEntryDialog dialog( - this, - _("Enter string"), - wxString::Format(_("Write string at address 0x%08x"), address), - "" - ); - - if (dialog.ShowModal() != wxID_OK) - return false; - - std::string text = dialog.GetValue().ToStdString(); - - if (text.empty()) - return false; - - for (size_t i = 0; i < text.size(); i++) - memory_writeU8(address + i, static_cast(text[i])); - - // null-terminator - memory_writeU8(address + text.size(), 0); - - return true; -} diff --git a/src/gui/wxgui/debugger/DumpCtrl.h b/src/gui/wxgui/debugger/DumpCtrl.h deleted file mode 100644 index b63bbc85..00000000 --- a/src/gui/wxgui/debugger/DumpCtrl.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include "wxgui/components/TextList.h" - -class DumpCtrl : public TextList -{ -public: - DumpCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style); - - void Init(); - wxSize DoGetBestSize() const override; - -protected: - void GoToAddressDialog(); - void CenterOffset(uint32 offset); - uint32 LineToOffset(uint32 line); - uint32 OffsetToLine(uint32 offset); - uint32 PositionToAddress(const wxPoint& position, uint32 line); - - void OnDraw(wxDC& dc, sint32 start, sint32 count, const wxPoint& start_position) override; - void OnMouseMove(const wxPoint& position, uint32 line) override; - void OnMouseDClick(const wxPoint& position, uint32 line) override; - void OnKeyPressed(sint32 key_code, const wxPoint& position) override; - void OnContextMenu(const wxPoint& position, uint32 line) override; - void OnMenuSelected(wxCommandEvent& event); - - template - bool WriteNumericDialog(uint32 address); - bool WriteString(uint32 address); -private: - struct - { - uint32 baseAddress; - uint32 size; - }m_memoryRegion; - uint32 m_lastGotoOffset{0}; - uint32 m_writerContextAddress {0}; - uint32 m_writerContextLine {0}; -}; diff --git a/src/gui/wxgui/debugger/DumpWindow.cpp b/src/gui/wxgui/debugger/DumpWindow.cpp deleted file mode 100644 index f152e8f6..00000000 --- a/src/gui/wxgui/debugger/DumpWindow.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/DumpWindow.h" - -#include "wxgui/debugger/DebuggerWindow2.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "wxgui/debugger/DumpCtrl.h" - -enum -{ - // REGISTER - REGISTER_ADDRESS_R0 = wxID_HIGHEST + 8200, - REGISTER_LABEL_R0 = REGISTER_ADDRESS_R0 + 32, - REGISTER_LABEL_FPR0_0 = REGISTER_LABEL_R0 + 32, - REGISTER_LABEL_FPR1_0 = REGISTER_LABEL_R0 + 32, -}; - -DumpWindow::DumpWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size) - : wxFrame(&parent, wxID_ANY, _("Memory Dump"), wxDefaultPosition, wxSize(600, 250), wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT) -{ - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - m_dump_ctrl = new DumpCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxScrolledWindowStyle); - main_sizer->Add(m_dump_ctrl, 1, wxEXPAND); - - this->SetSizer(main_sizer); - this->wxWindowBase::Layout(); - - this->Centre(wxBOTH); - - if (parent.GetConfig().data().pin_to_main) - OnMainMove(main_position, main_size); -} - -void DumpWindow::OnMainMove(const wxPoint& main_position, const wxSize& main_size) -{ - wxSize size(600, 250); - this->SetSize(size); - - wxPoint position = main_position; - position.y += main_size.GetHeight(); - this->SetPosition(position); -} - -void DumpWindow::OnGameLoaded() -{ - m_dump_ctrl->Init(); -} diff --git a/src/gui/wxgui/debugger/DumpWindow.h b/src/gui/wxgui/debugger/DumpWindow.h deleted file mode 100644 index e45f3489..00000000 --- a/src/gui/wxgui/debugger/DumpWindow.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "wxgui/debugger/DumpCtrl.h" - -class DebuggerWindow2; - -class DumpWindow : public wxFrame -{ -public: - DumpWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size); - - void OnMainMove(const wxPoint& position, const wxSize& main_size); - void OnGameLoaded(); - -private: - wxScrolledWindow* m_scrolled_window; - DumpCtrl* m_dump_ctrl; -}; \ No newline at end of file diff --git a/src/gui/wxgui/debugger/ModuleWindow.cpp b/src/gui/wxgui/debugger/ModuleWindow.cpp deleted file mode 100644 index 77a36d09..00000000 --- a/src/gui/wxgui/debugger/ModuleWindow.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/ModuleWindow.h" - -#include - -#include "wxgui/debugger/DebuggerWindow2.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" - -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/OS/RPL/rpl_structs.h" - -#include "Cafe/GraphicPack/GraphicPack2.h" - -enum ItemColumns -{ - ColumnName = 0, - ColumnAddress, - ColumnSize, -}; - -ModuleWindow::ModuleWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size) - : wxFrame(&parent, wxID_ANY, _("Modules"), wxDefaultPosition, wxSize(420, 250), wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT) -{ - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - - m_modules = new wxListView(this, wxID_ANY); - - wxListItem col0; - col0.SetId(ColumnName); - col0.SetText(_("Name")); - col0.SetWidth(125); - m_modules->InsertColumn(ColumnName, col0); - - wxListItem col1; - col1.SetId(ColumnAddress); - col1.SetText(_("Address")); - col1.SetWidth(75); - col1.SetAlign(wxLIST_FORMAT_RIGHT); - m_modules->InsertColumn(ColumnAddress, col1); - - wxListItem col2; - col2.SetId(ColumnSize); - col2.SetText(_("Size")); - col2.SetWidth(75); - col2.SetAlign(wxLIST_FORMAT_RIGHT); - m_modules->InsertColumn(ColumnSize, col2); - - main_sizer->Add(m_modules, 1, wxEXPAND); - - this->SetSizer(main_sizer); - this->wxWindowBase::Layout(); - - this->Centre(wxBOTH); - - if (parent.GetConfig().data().pin_to_main) - OnMainMove(main_position, main_size); - - m_modules->Bind(wxEVT_LEFT_DCLICK, &ModuleWindow::OnLeftDClick, this); - - OnGameLoaded(); -} - -void ModuleWindow::OnMainMove(const wxPoint& main_position, const wxSize& main_size) -{ - wxSize size(420, 250); - this->SetSize(size); - - wxPoint position = main_position; - position.x -= 420; - position.y += main_size.y; - this->SetPosition(position); -} - - -void ModuleWindow::OnGameLoaded() -{ - Freeze(); - - m_modules->DeleteAllItems(); - - const auto module_list = RPLLoader_GetModuleList(); - for (size_t i = 0; i < module_list.size(); i++) - { - const auto module = module_list[i]; - if (module) - { - wxListItem item; - item.SetId(i); - item.SetText(module->moduleName); - - const auto index = m_modules->InsertItem(item); - m_modules->SetItem(index, ColumnAddress, wxString::Format("%08x", module->regionMappingBase_text.GetMPTR())); - m_modules->SetItem(index, ColumnSize, wxString::Format("%x", module->regionSize_text)); - } - } - - sint32 patch_count = 0; - for (auto& gfx_pack : GraphicPack2::GetGraphicPacks()) - { - for (auto& patch_group : gfx_pack->GetPatchGroups()) - { - if (patch_group->isApplied()) - { - wxListItem item; - item.SetId(module_list.size() + patch_count); - item.SetText(std::string(patch_group->getName())); - - const auto index = m_modules->InsertItem(item); - m_modules->SetItem(index, ColumnAddress, wxString::Format("%08x", patch_group->getCodeCaveBase())); - m_modules->SetItem(index, ColumnSize, wxString::Format("%x", patch_group->getCodeCaveSize())); - patch_count++; - } - } - } - - Thaw(); -} - - -void ModuleWindow::OnLeftDClick(wxMouseEvent& event) -{ - long selected = m_modules->GetFirstSelected(); - if (selected == wxNOT_FOUND) - return; - const auto text = m_modules->GetItemText(selected, ColumnAddress); - const auto address = std::stoul(text.ToStdString(), nullptr, 16); - if (address == 0) - return; - debugger_jumpToAddressInDisasm(address); -} diff --git a/src/gui/wxgui/debugger/ModuleWindow.h b/src/gui/wxgui/debugger/ModuleWindow.h deleted file mode 100644 index 76bb0c67..00000000 --- a/src/gui/wxgui/debugger/ModuleWindow.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -class DebuggerWindow2; -class wxListView; - -class ModuleWindow : public wxFrame -{ -public: - ModuleWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size); - - void OnMainMove(const wxPoint& position, const wxSize& main_size); - void OnGameLoaded(); - -private: - void OnLeftDClick(wxMouseEvent& event); - - wxListView* m_modules; -}; diff --git a/src/gui/wxgui/debugger/RegisterWindow.cpp b/src/gui/wxgui/debugger/RegisterWindow.cpp deleted file mode 100644 index 1e6605b1..00000000 --- a/src/gui/wxgui/debugger/RegisterWindow.cpp +++ /dev/null @@ -1,485 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/RegisterWindow.h" - -#include - -#include "wxgui/debugger/DebuggerWindow2.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/OS/RPL/rpl_structs.h" -#include "Cafe/HW/Espresso/EspressoISA.h" -#include "util/helpers/helpers.h" - -enum -{ - // REGISTER - kRegisterValueR0 = wxID_HIGHEST + 8400, - kRegisterLabelR0 = kRegisterValueR0 + 32, - kRegisterLabelLR = kRegisterLabelR0 + 32, - kRegisterValueLR = kRegisterLabelLR + 1, - kRegisterValueFPR0_0 = kRegisterValueLR + 32, - kRegisterValueFPR1_0 = kRegisterValueFPR0_0 + 32, - kRegisterValueCR0 = kRegisterValueFPR1_0 + 32, - kContextMenuZero, - kContextMenuInc, - kContextMenuDec, - kContextMenuCopy, - kContextMenuGotoDisasm, - kContextMenuGotoDump, -}; - -RegisterWindow::RegisterWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size) - : wxFrame(&parent, wxID_ANY, _("Registers"), wxDefaultPosition, wxSize(400, 975), wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT), - m_prev_snapshot({}), m_show_double_values(true), m_context_ctrl(nullptr) -{ - SetSizeHints(wxDefaultSize, wxDefaultSize); - SetMaxSize({ 400, 975 }); - this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - - auto scrolled_win = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - - auto gpr_sizer = new wxFlexGridSizer(0, 3, 0, 0); - - // GPRs - for(sint32 i = 0; i < 32; ++i) - { - gpr_sizer->Add(new wxStaticText(scrolled_win, wxID_ANY, wxString::Format("R%d", i)), 0, wxLEFT, 5); - - auto value = new wxTextCtrl(scrolled_win, kRegisterValueR0 + i, wxString::Format("%08x", 0), wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - value->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - value->Bind(wxEVT_LEFT_DCLICK, &RegisterWindow::OnMouseDClickEvent, this); - //value->Bind(wxEVT_CONTEXT_MENU, &RegisterWindow::OnValueContextMenu, this); - gpr_sizer->Add(value, 0, wxLEFT|wxRIGHT, 5); - - auto label = new wxTextCtrl(scrolled_win, kRegisterLabelR0 + i, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - label->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - label->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - label->SetMinSize(wxSize(500, -1)); - gpr_sizer->Add(label, 0, wxEXPAND); - } - - { - // LR - gpr_sizer->Add(new wxStaticText(scrolled_win, wxID_ANY, wxString::Format("LR")), 0, wxLEFT, 5); - auto value = new wxTextCtrl(scrolled_win, kRegisterValueLR, wxString::Format("%08x", 0), wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - value->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - value->Bind(wxEVT_LEFT_DCLICK, &RegisterWindow::OnMouseDClickEvent, this); - //value->Bind(wxEVT_CONTEXT_MENU, &RegisterWindow::OnValueContextMenu, this); - gpr_sizer->Add(value, 0, wxLEFT | wxRIGHT, 5); - auto label = new wxTextCtrl(scrolled_win, kRegisterLabelLR, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - label->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - label->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - label->SetMinSize(wxSize(500, -1)); - gpr_sizer->Add(label, 0, wxEXPAND); - } - - sizer->Add(gpr_sizer, 1, wxEXPAND); - - auto button = new wxButton(scrolled_win, wxID_ANY, _("FP view mode")); - button->Bind(wxEVT_BUTTON, &RegisterWindow::OnFPViewModePress, this); - sizer->Add(button, 0, wxALL, 5); - - auto fp_sizer = new wxFlexGridSizer(0, 3, 0, 0); - for (sint32 i = 0; i < 32; ++i) - { - fp_sizer->Add(new wxStaticText(scrolled_win, wxID_ANY, wxString::Format("FP%d", i)), 0, wxLEFT, 5); - - auto value0 = new wxTextCtrl(scrolled_win, kRegisterValueFPR0_0 + i, wxString::Format("%lf", 0.0), wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - value0->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - value0->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - value0->Bind(wxEVT_LEFT_DCLICK, &RegisterWindow::OnMouseDClickEvent, this); - fp_sizer->Add(value0, 0, wxLEFT | wxRIGHT, 5); - - auto value1 = new wxTextCtrl(scrolled_win, kRegisterValueFPR1_0 + i, wxString::Format("%lf", 0.0), wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - value1->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - value1->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - value1->Bind(wxEVT_LEFT_DCLICK, &RegisterWindow::OnMouseDClickEvent, this); - fp_sizer->Add(value1, 0, wxLEFT | wxRIGHT, 5); - } - - sizer->Add(fp_sizer, 0, wxEXPAND); - auto cr_sizer = new wxFlexGridSizer(0, 2, 0, 0); - - // CRs - for (sint32 i = 0; i < 8; ++i) - { - cr_sizer->Add(new wxStaticText(scrolled_win, wxID_ANY, wxString::Format("CR%d", i)), 0, wxLEFT, 5); - auto value = new wxTextCtrl(scrolled_win, kRegisterValueCR0 + i, "-", wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER | wxTE_RICH2); - value->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - //value->Bind(wxEVT_CONTEXT_MENU, &RegisterWindow::OnValueContextMenu, this); - cr_sizer->Add(value, 0, wxRIGHT, 5); - } - - sizer->Add(cr_sizer, 0, wxEXPAND); - scrolled_win->SetSizerAndFit(sizer); - scrolled_win->SetScrollRate(-1, 10); - - main_sizer->Add(scrolled_win, 1, wxEXPAND); - SetSizer(main_sizer); - Layout(); - - if (parent.GetConfig().data().pin_to_main) - OnMainMove(main_position, main_size); - - //Bind(wxEVT_COMMAND_MENU_SELECTED, &RegisterWindow::OnValueContextMenuSelected, this, kContextMenuZero, kContextMenuGotoDump); -} - - -void RegisterWindow::OnMainMove(const wxPoint& main_position, const wxSize& main_size) -{ - wxSize size(400, 255 + main_size.y + 250); - this->SetSize(size); - - wxPoint position = main_position; - position.x += main_size.x; - position.y -= 255; - this->SetPosition(position); -} - -void RegisterWindow::UpdateIntegerRegister(wxTextCtrl* label, wxTextCtrl* value, uint32 registerValue, bool hasChanged) -{ - //const auto value = dynamic_cast(GetWindowChild(kRegisterValueR0 + i)); - //wxASSERT(value); - - //const bool has_changed = register_value != m_prev_snapshot.gpr[i]; - if (hasChanged) - value->SetForegroundColour(m_changed_color); - else if (value->GetForegroundColour() != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)) - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - - value->ChangeValue(wxString::Format("%08x", registerValue)); - - //const auto label = dynamic_cast(GetWindowChild(kRegisterLabelR0 + i)); - //wxASSERT(label); - label->SetForegroundColour(hasChanged ? m_changed_color : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - - // check if address is a string - if (registerValue >= MEMORY_DATA_AREA_ADDR && registerValue < (MEMORY_DATA_AREA_ADDR + MEMORY_DATA_AREA_SIZE)) - { - bool is_valid_string = true; - std::stringstream buffer; - - uint32 string_offset = registerValue; - while (string_offset < (MEMORY_DATA_AREA_ADDR + MEMORY_DATA_AREA_SIZE)) - { - const uint8 c = memory_readU8(string_offset++); - if (isprint(c)) - buffer << c; - else if (c == '\0') - { - buffer << c; - break; - } - else - { - is_valid_string = false; - break; - } - } - - if (is_valid_string && buffer.tellp() > 1) - { - label->ChangeValue(wxString::Format("\"%s\"", buffer.str().c_str())); - return; - } - - // check for widestring - is_valid_string = true; - string_offset = registerValue; - std::wstringstream wbuffer; - while (string_offset < (MEMORY_DATA_AREA_ADDR + MEMORY_DATA_AREA_SIZE - 1)) - { - const uint16 c = memory_readU16(string_offset); - string_offset += 2; - - if (iswprint(c)) - wbuffer << c; - else if (c == L'\0') - { - wbuffer << c; - break; - } - else - { - is_valid_string = false; - break; - } - } - - if (is_valid_string && buffer.tellp() > 1) - { - label->ChangeValue(wxString::Format(L"ws\"%s\"", wbuffer.str().c_str())); - return; - } - } - - // check if address is a code offset - RPLModule* code_module = RPLLoader_FindModuleByCodeAddr(registerValue); - if (code_module) - { - label->ChangeValue(wxString::Format("<%s> + %x", code_module->moduleName.c_str(), registerValue - code_module->regionMappingBase_text.GetMPTR())); - return; - } - - label->ChangeValue(wxEmptyString); -} - -void RegisterWindow::OnUpdateView() -{ - PPCInterpreter_t* hCPU = debugger_lockDebugSession(); - if (!hCPU) - return; - PPCSnapshot snapshot = debugger_getSnapshotFromSession(hCPU); - debugger_unlockDebugSession(hCPU); - for (int i = 0; i < 32; ++i) - { - const uint32 registerValue = snapshot.gpr[i]; - const bool hasChanged = registerValue != m_prev_snapshot.gpr[i]; - const auto value = dynamic_cast(FindWindow(kRegisterValueR0 + i)); - wxASSERT(value); - const auto label = dynamic_cast(FindWindow(kRegisterLabelR0 + i)); - wxASSERT(label); - - UpdateIntegerRegister(label, value, registerValue, hasChanged); - } - - // update LR - { - const uint32 registerValue = snapshot.spr_lr; - const bool hasChanged = registerValue != m_prev_snapshot.spr_lr; - const auto value = dynamic_cast(FindWindow(kRegisterValueLR)); - wxASSERT(value); - const auto label = dynamic_cast(FindWindow(kRegisterLabelLR)); - wxASSERT(label); - UpdateIntegerRegister(label, value, registerValue, hasChanged); - } - - for (int i = 0; i < 32; ++i) - { - const uint64_t register_value = snapshot.fpr[i].fp0int; - - const auto value = dynamic_cast(FindWindow(kRegisterValueFPR0_0 + i)); - wxASSERT(value); - - const bool has_changed = register_value != m_prev_snapshot.fpr[i].fp0int; - if (has_changed) - value->SetForegroundColour(m_changed_color); - else if (value->GetForegroundColour() != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)) - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - else - continue; - - if(m_show_double_values) - value->ChangeValue(wxString::Format("%lf", snapshot.fpr[i].fp0)); - else - value->ChangeValue(wxString::Format("%016llx", register_value)); - } - - for (int i = 0; i < 32; ++i) - { - const uint64_t register_value = snapshot.fpr[i].fp1int; - - const auto value = dynamic_cast(FindWindow(kRegisterValueFPR1_0 + i)); - wxASSERT(value); - - const bool has_changed = register_value != m_prev_snapshot.fpr[i].fp1int; - if (has_changed) - value->SetForegroundColour(m_changed_color); - else if (value->GetForegroundColour() != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)) - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - else - continue; - - if (m_show_double_values) - value->ChangeValue(wxString::Format("%lf", snapshot.fpr[i].fp1)); - else - value->ChangeValue(wxString::Format("%016llx", register_value)); - } - - // update CRs - for (int i = 0; i < 8; ++i) - { - const auto value = dynamic_cast(FindWindow(kRegisterValueCR0 + i)); - wxASSERT(value); - - auto cr_bits_ptr = snapshot.cr + i * 4; - auto cr_bits_ptr_cmp = m_prev_snapshot.cr + i * 4; - - const bool has_changed = !std::equal(cr_bits_ptr, cr_bits_ptr + 4, cr_bits_ptr_cmp); - if (has_changed) - value->SetForegroundColour(m_changed_color); - else if (value->GetForegroundColour() != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)) - value->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - else - continue; - - std::vector joinArray = {}; - if (cr_bits_ptr[Espresso::CR_BIT_INDEX_LT] != 0) - joinArray.emplace_back("LT"); - if (cr_bits_ptr[Espresso::CR_BIT_INDEX_GT] != 0) - joinArray.emplace_back("GT"); - if (cr_bits_ptr[Espresso::CR_BIT_INDEX_EQ] != 0) - joinArray.emplace_back("EQ"); - if (cr_bits_ptr[Espresso::CR_BIT_INDEX_SO] != 0) - joinArray.emplace_back("SO"); - - if (joinArray.empty()) - value->ChangeValue("-"); - else - value->ChangeValue(fmt::format("{}", fmt::join(joinArray, ", "))); - } - - m_prev_snapshot = snapshot; -} - -void RegisterWindow::OnMouseDClickEvent(wxMouseEvent& event) -{ - PPCInterpreter_t* debugSession = debugger_lockDebugSession(); - if (!debugSession) - { - event.Skip(); - return; - } - PPCSnapshot ppcSnapshot = debugger_getSnapshotFromSession(debugSession); - debugger_unlockDebugSession(debugSession); - debugSession = nullptr; - - const auto id = event.GetId(); - if(kRegisterValueR0 <= id && id < kRegisterValueR0 + 32) - { - const uint32 register_index = id - kRegisterValueR0; - const uint32 register_value = ppcSnapshot.gpr[register_index]; - wxTextEntryDialog set_value_dialog(this, _("Enter a new value."), wxString::Format(_("Set R%d value"), register_index), wxString::Format("0x%08x", register_value)); - if (set_value_dialog.ShowModal() == wxID_OK) - { - const uint32 value = ConvertString(set_value_dialog.GetValue().ToStdString()); - - if (debugSession = debugger_lockDebugSession(); debugSession) - { - debugSession->gpr[register_index] = value; - debugger_unlockDebugSession(debugSession); - } - - OnUpdateView(); - } - return; - } - - if (kRegisterValueFPR0_0 <= id && id < kRegisterValueFPR0_0 + 32) - { - const uint32 register_index = id - kRegisterValueFPR0_0; - const double register_value = ppcSnapshot.fpr[register_index].fp0; - wxTextEntryDialog set_value_dialog(this, _("Enter a new value."), wxString::Format(_("Set FP0_%d value"), register_index), wxString::Format("%lf", register_value)); - if (set_value_dialog.ShowModal() == wxID_OK) - { - const double value = ConvertString(set_value_dialog.GetValue().ToStdString()); - - if (debugSession = debugger_lockDebugSession(); debugSession) - { - debugSession->fpr[register_index].fp0 = value; - debugger_unlockDebugSession(debugSession); - } - - OnUpdateView(); - } - - return; - } - - if (kRegisterValueFPR1_0 <= id && id < kRegisterValueFPR1_0 + 32) - { - const uint32 register_index = id - kRegisterValueFPR1_0; - const double register_value = ppcSnapshot.fpr[register_index].fp1; - wxTextEntryDialog set_value_dialog(this, _("Enter a new value."), wxString::Format(_("Set FP1_%d value"), register_index), wxString::Format("%lf", register_value)); - if (set_value_dialog.ShowModal() == wxID_OK) - { - const double value = ConvertString(set_value_dialog.GetValue().ToStdString()); - - if (debugSession = debugger_lockDebugSession(); debugSession) - { - debugSession->fpr[register_index].fp1 = value; - debugger_unlockDebugSession(debugSession); - } - - OnUpdateView(); - } - return; - } - - event.Skip(); -} - -void RegisterWindow::OnFPViewModePress(wxCommandEvent& event) -{ - m_show_double_values = !m_show_double_values; - - PPCInterpreter_t* debugSession = debugger_lockDebugSession(); - if (!debugSession) - return; - PPCSnapshot ppcSnapshot = debugger_getSnapshotFromSession(debugSession); - debugger_unlockDebugSession(debugSession); - - for (int i = 0; i < 32; ++i) - { - const auto value0 = dynamic_cast(FindWindow(kRegisterValueFPR0_0 + i)); - const auto value1 = dynamic_cast(FindWindow(kRegisterValueFPR1_0 + i)); - wxASSERT(value0); - wxASSERT(value1); - - if (m_show_double_values) - { - value0->ChangeValue(wxString::Format("%lf", ppcSnapshot.fpr[i].fp0)); - value1->ChangeValue(wxString::Format("%lf", ppcSnapshot.fpr[i].fp1)); - } - else - { - value0->ChangeValue(wxString::Format("%016llx", ppcSnapshot.fpr[i].fp0int)); - value1->ChangeValue(wxString::Format("%016llx", ppcSnapshot.fpr[i].fp1int)); - } - } -} - -void RegisterWindow::OnValueContextMenu(wxContextMenuEvent& event) -{ - wxMenu menu; - - menu.Append(kContextMenuZero, _("&Zero")); - menu.Append(kContextMenuInc, _("&Increment")); - menu.Append(kContextMenuDec, _("&Decrement")); - menu.AppendSeparator(); - menu.Append(kContextMenuCopy, _("&Copy")); - menu.AppendSeparator(); - menu.Append(kContextMenuGotoDisasm, _("&Goto Disasm")); - menu.Append(kContextMenuGotoDump, _("G&oto Dump")); - - m_context_ctrl = dynamic_cast(event.GetEventObject()); - - PopupMenu(&menu); -} - -void RegisterWindow::OnValueContextMenuSelected(wxCommandEvent& event) -{ - wxASSERT(m_context_ctrl); - - switch (event.GetId()) - { - case kContextMenuZero: - break; - case kContextMenuInc: - break; - case kContextMenuDec: - break; - case kContextMenuCopy: - break; - case kContextMenuGotoDisasm: - break; - case kContextMenuGotoDump: - break; - } -} diff --git a/src/gui/wxgui/debugger/RegisterWindow.h b/src/gui/wxgui/debugger/RegisterWindow.h deleted file mode 100644 index 9cb50840..00000000 --- a/src/gui/wxgui/debugger/RegisterWindow.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include - -class DebuggerWindow2; -class wxTextCtrl; - -class RegisterWindow : public wxFrame -{ -public: - RegisterWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size); - - void OnMainMove(const wxPoint& position, const wxSize& main_size); - void OnUpdateView(); - -private: - void OnMouseDClickEvent(wxMouseEvent& event); - void OnFPViewModePress(wxCommandEvent& event); - void OnValueContextMenu(wxContextMenuEvent& event); - void OnValueContextMenuSelected(wxCommandEvent& event); - - void UpdateIntegerRegister(wxTextCtrl* label, wxTextCtrl* value, uint32 registerValue, bool hasChanged); - - PPCSnapshot m_prev_snapshot; - bool m_show_double_values; - wxColour m_changed_color = {0xFF0000FF}; - - wxTextCtrl* m_context_ctrl; -}; diff --git a/src/gui/wxgui/debugger/SymbolCtrl.cpp b/src/gui/wxgui/debugger/SymbolCtrl.cpp deleted file mode 100644 index 8675259c..00000000 --- a/src/gui/wxgui/debugger/SymbolCtrl.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "wxgui/debugger/SymbolCtrl.h" -#include "Cafe/OS/RPL/rpl_symbol_storage.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include -#include - -enum ItemColumns -{ - ColumnName = 0, - ColumnAddress, - ColumnModule, -}; - -SymbolListCtrl::SymbolListCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size) : wxListView(parent, id, pos, size, wxLC_REPORT | wxLC_VIRTUAL) -{ - wxListItem col0; - col0.SetId(ColumnName); - col0.SetText(_("Name")); - col0.SetWidth(200); - InsertColumn(ColumnName, col0); - - wxListItem col1; - col1.SetId(ColumnAddress); - col1.SetText(_("Address")); - col1.SetWidth(75); - col1.SetAlign(wxLIST_FORMAT_RIGHT); - InsertColumn(ColumnAddress, col1); - - wxListItem col2; - col2.SetId(ColumnModule); - col2.SetText(_("Module")); - col2.SetWidth(75); - col2.SetAlign(wxLIST_FORMAT_RIGHT); - InsertColumn(ColumnModule, col2); - - Bind(wxEVT_LIST_ITEM_ACTIVATED, &SymbolListCtrl::OnLeftDClick, this); - Bind(wxEVT_LIST_ITEM_RIGHT_CLICK, &SymbolListCtrl::OnRightClick, this); - - m_list_filter.Clear(); - - OnGameLoaded(); -} - -void SymbolListCtrl::OnGameLoaded() -{ - m_data.clear(); - m_visible_items.clear(); - const auto symbol_map = rplSymbolStorage_lockSymbolMap(); - for (auto const& [address, symbol_info] : symbol_map) - { - if (symbol_info == nullptr || symbol_info->symbolName == nullptr) - continue; - - wxString libNameWX = wxString::FromAscii((const char*)symbol_info->libName); - wxString symbolNameWX = wxString::FromAscii((const char*)symbol_info->symbolName); - wxString searchNameWX = libNameWX + symbolNameWX; - searchNameWX.MakeLower(); - - m_data.try_emplace( - address, - symbolNameWX, - libNameWX, - searchNameWX - ); - } - rplSymbolStorage_unlockSymbolMap(); - - RebuildVisibleItems(); -} - -wxString SymbolListCtrl::OnGetItemText(long item, long column) const -{ - if (item < 0 || item >= static_cast(m_visible_items.size())) - return wxEmptyString; - - const auto& [address, symbol] = *m_visible_items[item]; - if (column == ColumnName) - return symbol.name; - if (column == ColumnAddress) - return wxString::Format("%08x", address); - if (column == ColumnModule) - return symbol.libName; - - return wxEmptyString; -} - - -void SymbolListCtrl::OnLeftDClick(wxListEvent& event) -{ - long selected = GetFirstSelected(); - if (selected == wxNOT_FOUND || selected >= static_cast(m_visible_items.size())) - return; - const auto address = m_visible_items[selected]->first; - if (address == 0) - return; - debugger_jumpToAddressInDisasm(address); -} - -void SymbolListCtrl::OnRightClick(wxListEvent& event) -{ - long selected = GetFirstSelected(); - if (selected == wxNOT_FOUND || selected >= static_cast(m_visible_items.size())) - return; - if (wxClipboard::Get()->Open()) - { - wxClipboard::Get()->SetData(new wxTextDataObject(wxString::Format("0x%08x", m_visible_items[selected]->first))); - wxClipboard::Get()->Close(); - } -} - -void SymbolListCtrl::RebuildVisibleItems() -{ - m_visible_items.clear(); - m_visible_items.reserve(m_data.size()); - const bool has_filter = !m_list_filter.IsEmpty(); - for (auto it = m_data.cbegin(); it != m_data.cend(); ++it) - { - if (has_filter && !it->second.searchName.Contains(m_list_filter)) - continue; - - m_visible_items.emplace_back(it); - } - - SetItemCount(static_cast(m_visible_items.size())); - if (!m_visible_items.empty()) - { - const auto top_item = std::min(GetTopItem(), static_cast(m_visible_items.size() - 1)); - RefreshItems(top_item, std::min(static_cast(m_visible_items.size() - 1), top_item + GetCountPerPage() + 1)); - } - else - Refresh(); -} - -void SymbolListCtrl::ChangeListFilter(wxString filter) -{ - m_list_filter = filter.MakeLower(); - RebuildVisibleItems(); -} diff --git a/src/gui/wxgui/debugger/SymbolCtrl.h b/src/gui/wxgui/debugger/SymbolCtrl.h deleted file mode 100644 index 938361c0..00000000 --- a/src/gui/wxgui/debugger/SymbolCtrl.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -class SymbolListCtrl : public wxListView -{ -public: - SymbolListCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size); - void OnGameLoaded(); - - void ChangeListFilter(wxString filter); - -private: - struct SymbolItem { - SymbolItem() = default; - SymbolItem(const wxString& name, const wxString& libName, const wxString& searchName) : name(name), libName(libName), searchName(searchName) {} - - wxString name; - wxString libName; - wxString searchName; - }; - using SymbolMap = std::map; - - SymbolMap m_data; - wxString m_list_filter; - std::vector m_visible_items; - - wxString OnGetItemText(long item, long column) const override; - void RebuildVisibleItems(); - void OnLeftDClick(wxListEvent& event); - void OnRightClick(wxListEvent& event); -}; diff --git a/src/gui/wxgui/debugger/SymbolWindow.cpp b/src/gui/wxgui/debugger/SymbolWindow.cpp deleted file mode 100644 index 663e599d..00000000 --- a/src/gui/wxgui/debugger/SymbolWindow.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxgui/debugger/SymbolWindow.h" -#include "wxgui/debugger/DebuggerWindow2.h" -#include "Cafe/HW/Espresso/Debugger/Debugger.h" -#include "Cafe/OS/RPL/rpl_symbol_storage.h" - -enum ItemColumns -{ - ColumnName = 0, - ColumnAddress, - ColumnModule, -}; - -SymbolWindow::SymbolWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size) - : wxFrame(&parent, wxID_ANY, _("Symbols"), wxDefaultPosition, wxSize(600, 250), wxSYSTEM_MENU | wxCAPTION | wxCLIP_CHILDREN | wxRESIZE_BORDER | wxFRAME_FLOAT_ON_PARENT) -{ - wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); - m_symbol_ctrl = new SymbolListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); - main_sizer->Add(m_symbol_ctrl, 1, wxEXPAND); - - m_filter = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_NO_VSCROLL); - m_filter->Bind(wxEVT_TEXT, &SymbolWindow::OnFilterChanged, this); - main_sizer->Add(m_filter, 0, wxALL | wxEXPAND, 5); - - this->SetSizer(main_sizer); - this->wxWindowBase::Layout(); - - this->Centre(wxHORIZONTAL); - - if (parent.GetConfig().data().pin_to_main) - OnMainMove(main_position, main_size); -} - -void SymbolWindow::OnMainMove(const wxPoint& main_position, const wxSize& main_size) -{ - wxSize size(420, 250); - this->SetSize(size); - - wxPoint position = main_position; - position.x -= 420; - this->SetPosition(position); -} - -void SymbolWindow::OnGameLoaded() -{ - m_symbol_ctrl->OnGameLoaded(); -} - -void SymbolWindow::OnFilterChanged(wxCommandEvent& event) -{ - m_symbol_ctrl->ChangeListFilter(m_filter->GetValue()); -} diff --git a/src/gui/wxgui/debugger/SymbolWindow.h b/src/gui/wxgui/debugger/SymbolWindow.h deleted file mode 100644 index 505328fa..00000000 --- a/src/gui/wxgui/debugger/SymbolWindow.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "wxgui/debugger/SymbolCtrl.h" -#include - -class DebuggerWindow2; - -class SymbolWindow : public wxFrame -{ -public: - SymbolWindow(DebuggerWindow2& parent, const wxPoint& main_position, const wxSize& main_size); - - void OnMainMove(const wxPoint& position, const wxSize& main_size); - void OnGameLoaded(); - - void OnLeftDClick(wxListEvent& event); - -private: - wxTextCtrl* m_filter; - SymbolListCtrl* m_symbol_ctrl; - - void OnFilterChanged(wxCommandEvent& event); -}; diff --git a/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.cpp b/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.cpp deleted file mode 100644 index dbe17be4..00000000 --- a/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h" -#include "Cafe/Account/Account.h" - -#include -#include -#include -#include -#include -#include -#include -#include "util/helpers/helpers.h" - -wxCreateAccountDialog::wxCreateAccountDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Create new account")) -{ - auto* main_sizer = new wxFlexGridSizer(0, 2, 0, 0); - main_sizer->AddGrowableCol(1); - main_sizer->SetFlexibleDirection(wxBOTH); - main_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - main_sizer->Add(new wxStaticText(this, wxID_ANY, "PersistentId"), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_persistent_id = new wxTextCtrl(this, wxID_ANY, fmt::format("{:x}", Account::GetNextPersistentId())); - m_persistent_id->SetToolTip(_("The persistent id is the internal folder name used for your saves. Only change this if you are importing saves from a Wii U with a specific id")); - main_sizer->Add(m_persistent_id, 1, wxALL | wxEXPAND, 5); - - main_sizer->Add(new wxStaticText(this, wxID_ANY, _("Mii name")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_mii_name = new wxTextCtrl(this, wxID_ANY); - m_mii_name->SetFocus(); - m_mii_name->SetMaxLength(10); - main_sizer->Add(m_mii_name, 1, wxALL | wxEXPAND, 5); - - main_sizer->Add(0, 0, 1, wxEXPAND, 5); - { - auto* button_sizer = new wxBoxSizer(wxHORIZONTAL); - - m_ok_button = new wxButton(this, wxID_ANY, _("OK")); - m_ok_button->Bind(wxEVT_BUTTON, &wxCreateAccountDialog::OnOK, this); - button_sizer->Add(m_ok_button, 0, wxALL, 5); - - m_cancel_buton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancel_buton->Bind(wxEVT_BUTTON, &wxCreateAccountDialog::OnCancel, this); - button_sizer->Add(m_cancel_buton, 0, wxALL, 5); - - main_sizer->Add(button_sizer, 1, wxALIGN_RIGHT, 5); - } - - this->SetSizerAndFit(main_sizer); - this->wxWindowBase::Layout(); -} - -uint32 wxCreateAccountDialog::GetPersistentId() const -{ - const std::string id_string = m_persistent_id->GetValue().ToStdString(); - return ConvertString(id_string, 16); -} - -wxString wxCreateAccountDialog::GetMiiName() const -{ - return m_mii_name->GetValue(); -} - -void wxCreateAccountDialog::OnOK(wxCommandEvent& event) -{ - if(m_persistent_id->IsEmpty()) - { - wxMessageBox(_("No persistent id entered!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - const auto id = GetPersistentId(); - if(id < Account::kMinPersistendId) - { - wxMessageBox(formatWxString(_("The persistent id must be greater than {:x}!"), Account::kMinPersistendId), - _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - const auto& account = Account::GetAccount(id); - if(account.GetPersistentId() == id) - { - const std::wstring msg = fmt::format(fmt::runtime(_("The persistent id {:x} is already in use by account {}!").ToStdWstring()), - account.GetPersistentId(), std::wstring{ account.GetMiiName() }); - wxMessageBox(msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - if(m_mii_name->IsEmpty()) - { - wxMessageBox(_("Account name may not be empty!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - EndModal(wxID_OK); -} - -void wxCreateAccountDialog::OnCancel(wxCommandEvent& event) -{ - EndModal(wxID_CANCEL); -} diff --git a/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h b/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h deleted file mode 100644 index a61cf548..00000000 --- a/src/gui/wxgui/dialogs/CreateAccount/wxCreateAccountDialog.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include - -class wxCreateAccountDialog : public wxDialog -{ -public: - wxCreateAccountDialog(wxWindow* parent); - - [[nodiscard]] uint32 GetPersistentId() const; - [[nodiscard]] wxString GetMiiName() const; -private: - class wxTextCtrl* m_persistent_id; - class wxTextCtrl* m_mii_name; - class wxButton* m_ok_button, * m_cancel_buton; - - void OnOK(wxCommandEvent& event); - void OnCancel(wxCommandEvent& event); -}; \ No newline at end of file diff --git a/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.cpp b/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.cpp deleted file mode 100644 index be463377..00000000 --- a/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.cpp +++ /dev/null @@ -1,330 +0,0 @@ -#include "SaveImportWindow.h" - -#include "pugixml.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include "zip.h" - -#include "Cafe/Account/Account.h" -#include "config/ActiveSettings.h" -#include "Cafe/OS/libs/coreinit/coreinit_Time.h" -#include "util/helpers/helpers.h" -#include "Cafe/HW/Espresso/PPCState.h" -#include "wxgui/helpers/wxHelpers.h" - -SaveImportWindow::SaveImportWindow(wxWindow* parent, uint64 title_id) - : wxDialog(parent, wxID_ANY, _("Import save entry"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxFRAME_TOOL_WINDOW | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), - m_title_id(title_id) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* row1 = new wxFlexGridSizer(0, 2, 0, 0); - row1->AddGrowableCol(1); - - row1->Add(new wxStaticText(this, wxID_ANY, _("Source")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_source_selection = new wxFilePickerCtrl(this, wxID_ANY, wxEmptyString, - _("Select a zipped save file"), - formatWxString("{}|*.zip", _("Save entry (*.zip)"))); - m_source_selection->SetMinSize({ 270, -1 }); - row1->Add(m_source_selection, 1, wxALL | wxEXPAND, 5); - - row1->Add(new wxStaticText(this, wxID_ANY, _("Target")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - const auto& accounts = Account::GetAccounts(); - m_target_selection = new wxComboBox(this, wxID_ANY); - for (const auto& account : accounts) - { - m_target_selection->Append(fmt::format("{:x} ({})", account.GetPersistentId(), - boost::nowide::narrow(account.GetMiiName().data(), account.GetMiiName().size())), - (void*)(uintptr_t)account.GetPersistentId()); - } - row1->Add(m_target_selection, 1, wxALL | wxEXPAND, 5); - - sizer->Add(row1, 0, wxEXPAND, 5); - } - - { - auto* row2 = new wxFlexGridSizer(0, 2, 0, 0); - row2->AddGrowableCol(1); - row2->SetFlexibleDirection(wxBOTH); - row2->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - auto* ok_button = new wxButton(this, wxID_ANY, _("OK")); - ok_button->Bind(wxEVT_BUTTON, &SaveImportWindow::OnImport, this); - row2->Add(ok_button, 0, wxALL, 5); - - auto* cancel_button = new wxButton(this, wxID_ANY, _("Cancel")); - cancel_button->Bind(wxEVT_BUTTON, [this](auto&) - { - m_return_code = wxCANCEL; - Close(); - }); - row2->Add(cancel_button, 0, wxALIGN_RIGHT | wxALL, 5); - - sizer->Add(row2, 1, wxEXPAND, 5); - } - - this->SetSizerAndFit(sizer); - this->Centre(wxBOTH); -} - -void SaveImportWindow::EndModal(int retCode) -{ - wxDialog::EndModal(retCode); - SetReturnCode(m_return_code); -} - -void SaveImportWindow::OnImport(wxCommandEvent& event) -{ - const auto source_path = m_source_selection->GetPath(); - if (source_path.empty()) - return; - - // try to find cemu_meta file to verify targetid - const std::string zipfile = source_path.ToUTF8().data(); - - int ziperr; - auto* zip = zip_open(zipfile.c_str(), ZIP_RDONLY, &ziperr); - if (zip) - { - const sint32 numEntries = zip_get_num_entries(zip, 0); - for (sint32 i = 0; i < numEntries; i++) - { - zip_stat_t sb{}; - if (zip_stat_index(zip, i, 0, &sb) != 0) - continue; - - if (!boost::equals(sb.name, "cemu_meta")) - continue; - - auto* zip_file = zip_fopen_index(zip, i, 0); - if (zip_file == nullptr) - continue; - - auto buffer = std::make_unique(sb.size); - if (zip_fread(zip_file, buffer.get(), sb.size) == sb.size) - { - std::string_view str{ buffer.get(), sb.size }; - // titleId = {:#016x} - if(boost::starts_with(str, "titleId = ")) - { - const uint64_t titleId = ConvertString(str.substr(sizeof("titleId = ") + 1), 16); - if(titleId != 0 && titleId != m_title_id) - { - const auto msg = formatWxString(_("You are trying to import a savegame for a different title than your currently selected one: {:016x} vs {:016x}\nAre you sure that you want to continue?"), titleId, m_title_id); - const auto res = wxMessageBox(msg, _("Error"), wxYES_NO | wxCENTRE | wxICON_WARNING, this); - if(res == wxNO) - { - m_return_code = wxCANCEL; - Close(); - return; - } - } - } - } - - zip_fclose(zip_file); - - break; - } - - zip_close(zip); - } - - // unzip to tmp folder -> using target path directly now - std::error_code ec; - //auto tmp_source = fs::temp_directory_path(ec); - //if(ec) - //{ - // const auto error_msg = formatWxString(_("Error when getting the temp directory path:\n{}"), GetSystemErrorMessage(ec)); - // wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - // return; - //} - - uint32 target_id = 0; - const auto selection = m_target_selection->GetCurrentSelection(); - if (selection != wxNOT_FOUND) - target_id = (uint32)(uintptr_t)m_target_selection->GetClientData(selection); - - if (target_id == 0) - { - target_id = ConvertString(m_target_selection->GetValue().ToStdString(), 16); - if (target_id < Account::kMinPersistendId) - { - const auto msg = formatWxString(_("The given account id is not valid!\nIt must be a hex number bigger or equal than {:08x}"), Account::kMinPersistendId); - wxMessageBox(msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - } - - fs::path target_path = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/user/{:08x}", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF), target_id); - if (fs::exists(target_path)) - { - if (!fs::is_directory(target_path)) - { - const auto msg = formatWxString(_("There's already a file at the target directory:\n{}"), _pathToUtf8(target_path)); - wxMessageBox(msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - m_return_code = wxCANCEL; - Close(); - return; - } - - const auto msg = _("There's already a save game available for the target account, do you want to overwrite it?\nThis will delete the existing save files for the account and replace them."); - const auto result = wxMessageBox(msg, _("Error"), wxYES_NO | wxCENTRE | wxICON_WARNING, this); - if (result == wxNO) - { - m_return_code = wxCANCEL; - Close(); - return; - } - - std::error_code ec; - while (fs::exists(target_path, ec) || ec) - { - fs::remove_all(target_path, ec); - - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to delete the former save game:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return; - } - - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - } - - // extract zip file - - //tmp_source.append("Cemu").append(fmt::format("{}", rand())); - - //tmp_source = getMlcPath(L"usr/save"); - //tmp_source /= fmt::format("{:08x}/{:08x}/user/{:08x}_tmp", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF), target_id); - auto tmp_source = target_path; - - fs::create_directories(tmp_source, ec); - if (ec) - { - const auto error_msg = formatWxString(_("Error when creating the extraction path:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - zip = zip_open(zipfile.c_str(), ZIP_RDONLY, &ziperr); - if (!zip) - { - const auto error_msg = formatWxString(_("Error when opening the import zip file:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - sint32 numEntries = zip_get_num_entries(zip, 0); - for (sint32 i = 0; i < numEntries; i++) - { - zip_stat_t sb{}; - if (zip_stat_index(zip, i, 0, &sb) != 0) - { - cemuLog_log(LogType::Force, "zip stat index failed on {} entry", i); - continue; - } - - if (boost::starts_with(sb.name, "../") || boost::starts_with(sb.name, "..\\")) - continue; // bad path - - if (boost::equals(sb.name, "cemu_meta")) - continue; - - size_t sb_name_len = strlen(sb.name); - if (sb_name_len == 0) - continue; - - const auto path = fs::path(tmp_source).append(sb.name); - if (sb.name[sb_name_len - 1] == '/') - { - fs::create_directories(path, ec); - if (ec) - cemuLog_log(LogType::Force, "can't create directory {}: {}", sb.name, ec.message()); - - continue; - } - - if (sb.size == 0) - continue; - - if (sb.size > (1024 * 1024 * 128)) - continue; // skip unusually huge files - - auto* zip_file = zip_fopen_index(zip, i, 0); - if (zip_file == nullptr) - continue; - - auto buffer = std::make_unique(sb.size); - if (zip_fread(zip_file, buffer.get(), sb.size) == sb.size) - { - std::ofstream file(path, std::ios::out | std::ios::binary); - if (file.is_open()) - file.write(buffer.get(), sb.size); - } - - zip_fclose(zip_file); - } - - zip_close(zip); - // extracted all files to tmp_source - - // edit meta saveinfo.xml - fs::path saveinfo = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/meta/saveinfo.xml", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF)); - if (fs::exists(saveinfo) || fs::is_regular_file(saveinfo)) - { - pugi::xml_document doc; - if (doc.load_file(saveinfo.c_str())) - { - auto info_node = doc.child("info"); - if (info_node) - { - // delete old node if available - auto old_persistend_id_string = fmt::format("{:08x}", target_id); - const auto node_entry = info_node.find_child([&old_persistend_id_string](const pugi::xml_node& node) - { - return boost::iequals(node.attribute("persistentId").as_string(), old_persistend_id_string); - }); - // add save-entry node if not available yet - if (!node_entry) - { - // add new node - auto new_persistend_id_string = fmt::format("{:08x}", target_id); - auto new_node = info_node.append_child("account"); - new_node.append_attribute("persistentId").set_value(new_persistend_id_string.c_str()); - auto timestamp = new_node.append_child("timestamp"); - timestamp.text().set(fmt::format("{:016x}", coreinit::OSGetTime() / ESPRESSO_TIMER_CLOCK).c_str()); // TODO time not initialized yet? - - if(!doc.save_file(saveinfo.c_str())) - cemuLog_log(LogType::Force, "couldn't insert save entry in saveinfo.xml: {}", _pathToUtf8(saveinfo)); - } - } - } - } // todo create saveinfo if doesnt exist - - - /*wxMessageBox(fmt::format("{}\n{}", tmp_source.generic_u8string(), target_path.generic_u8string()), _("Error"), wxOK | wxCENTRE, this); - - fs::rename(tmp_source, target_path, ec); - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to move the extracted save game:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return; - }*/ - - m_target_id = target_id; - m_return_code = wxOK; - Close(); -} diff --git a/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.h b/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.h deleted file mode 100644 index 209df80a..00000000 --- a/src/gui/wxgui/dialogs/SaveImport/SaveImportWindow.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -class SaveImportWindow : public wxDialog -{ -public: - SaveImportWindow(wxWindow* parent, uint64 title_id); - - void EndModal(int retCode) override; - - uint32 GetTargetPersistentId() const { return m_target_id; } -private: - void OnImport(wxCommandEvent& event); - - class wxFilePickerCtrl* m_source_selection; - class wxComboBox* m_target_selection; - - uint32 m_target_id = 0; - const uint64 m_title_id; - const fs::path m_source_file; - int m_return_code = wxCANCEL; -}; - - diff --git a/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.cpp b/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.cpp deleted file mode 100644 index 9e6ed6a7..00000000 --- a/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#include "SaveTransfer.h" - -#include "pugixml.hpp" -#include -#include -#include -#include -#include -#include -#include - -#include "Cafe/Account/Account.h" -#include "config/ActiveSettings.h" -#include "util/helpers/helpers.h" -#include "wxgui/helpers/wxHelpers.h" - -SaveTransfer::SaveTransfer(wxWindow* parent, uint64 title_id, const wxString& source_account, uint32 source_id) -: wxDialog(parent, wxID_ANY, _("Save transfer"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxFRAME_TOOL_WINDOW | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX), - m_title_id(title_id), m_source_id(source_id) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* row1 = new wxFlexGridSizer(0, 2, 0, 0); - row1->AddGrowableCol(1); - - row1->Add(new wxStaticText(this, wxID_ANY, _("Source")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - auto* source_choice = new wxChoice(this, wxID_ANY); - source_choice->SetMinSize({170, -1}); - source_choice->Append(source_account); - source_choice->SetSelection(0); - row1->Add(source_choice, 1, wxALL | wxEXPAND, 5); - - row1->Add(new wxStaticText(this, wxID_ANY, _("Target")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - const auto& accounts = Account::GetAccounts(); - m_target_selection = new wxComboBox(this, wxID_ANY); - for (const auto& account : accounts) - { - if (account.GetPersistentId() == m_source_id) - continue; - - m_target_selection->Append(fmt::format("{:x} ({})", account.GetPersistentId(), - boost::nowide::narrow(account.GetMiiName().data(), account.GetMiiName().size())), - (void*)(uintptr_t)account.GetPersistentId()); - } - row1->Add(m_target_selection, 1, wxALL | wxEXPAND, 5); - - sizer->Add(row1, 0, wxEXPAND, 5); - } - - { - auto* row2 = new wxFlexGridSizer(0, 2, 0, 0); - row2->AddGrowableCol(1); - row2->SetFlexibleDirection(wxBOTH); - row2->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); - - auto* ok_button = new wxButton(this, wxID_ANY, _("OK")); - ok_button->Bind(wxEVT_BUTTON, &SaveTransfer::OnTransfer, this); - row2->Add(ok_button, 0, wxALL, 5); - - auto* cancel_button = new wxButton(this, wxID_ANY, _("Cancel")); - cancel_button->Bind(wxEVT_BUTTON, [this](auto&) - { - m_return_code = wxCANCEL; - Close(); - }); - row2->Add(cancel_button, 0, wxALIGN_RIGHT | wxALL, 5); - - sizer->Add(row2, 1, wxEXPAND, 5); - } - - this->SetSizerAndFit(sizer); - this->Centre(wxBOTH); -} - -void SaveTransfer::EndModal(int retCode) -{ - wxDialog::EndModal(retCode); - SetReturnCode(m_return_code); -} - -void SaveTransfer::OnTransfer(wxCommandEvent& event) -{ - uint32 target_id = 0; - const auto selection = m_target_selection->GetCurrentSelection(); - if(selection != wxNOT_FOUND) - target_id = (uint32)(uintptr_t)m_target_selection->GetClientData(selection); - - if (target_id == 0) - { - target_id = ConvertString(m_target_selection->GetValue().ToStdString(), 16); - if(target_id < Account::kMinPersistendId) - { - const auto msg = formatWxString(_("The given account id is not valid!\nIt must be a hex number bigger or equal than {:08x}"), Account::kMinPersistendId); - wxMessageBox(msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - - } - - const fs::path source_path = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/user/{:08x}", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF), m_source_id); - if (!fs::exists(source_path) || !fs::is_directory(source_path)) - return; - - const fs::path target_path = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/user/{:08x}", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF), target_id); - if (fs::exists(target_path)) - { - if(!fs::is_directory(target_path)) - { - const auto msg = formatWxString(_("There's already a file at the target directory:\n{}"), _pathToUtf8(target_path)); - wxMessageBox(msg, _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - m_return_code = wxCANCEL; - Close(); - return; - } - - const auto msg = _("There's already a save game available for the target account, do you want to overwrite it?\nThis will delete the existing save files for the account and replace them."); - const auto result = wxMessageBox(msg, _("Error"), wxYES_NO | wxCENTRE | wxICON_WARNING, this); - if(result == wxNO) - { - m_return_code = wxCANCEL; - Close(); - return; - } - - std::error_code ec; - while (fs::exists(target_path, ec) || ec) - { - fs::remove_all(target_path, ec); - - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to delete the former save game:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return; - } - - // wait so filesystem doesnt - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - } - - // edit meta saveinfo.xml - bool meta_file_edited = false; - const fs::path saveinfo = ActiveSettings::GetMlcPath("usr/save/{:08x}/{:08x}/meta/saveinfo.xml", (uint32)(m_title_id >> 32), (uint32)(m_title_id & 0xFFFFFFFF)); - if (fs::exists(saveinfo) || fs::is_regular_file(saveinfo)) - { - pugi::xml_document doc; - if (doc.load_file(saveinfo.c_str())) - { - auto info_node = doc.child("info"); - if (info_node) - { - // delete old node if available - auto old_persistend_id_string = fmt::format("{:08x}", target_id); - const auto delete_entry = info_node.find_child([&old_persistend_id_string](const pugi::xml_node& node) - { - return boost::iequals(node.attribute("persistentId").as_string(), old_persistend_id_string); - }); - if (delete_entry) - info_node.remove_child(delete_entry); - - - // move new node - auto new_persistend_id_string = fmt::format("{:08x}", m_source_id); - auto move_entry = info_node.find_child([&new_persistend_id_string](const pugi::xml_node& node) - { - return boost::iequals(node.attribute("persistentId").as_string(), new_persistend_id_string); - }); - if(move_entry) - move_entry.attribute("persistentId").set_value(old_persistend_id_string.c_str()); - else - { - // TODO: create if not found! (shouldnt happen though) - } - - meta_file_edited = doc.save_file(saveinfo.c_str()); - } - } - } - - if (!meta_file_edited) - cemuLog_log(LogType::Force, "SaveTransfer::OnTransfer: couldn't update save entry in saveinfo.xml: {}", _pathToUtf8(saveinfo)); - - std::error_code ec; - fs::rename(source_path, target_path, ec); - if (ec) - { - const auto error_msg = formatWxString(_("Error when trying to move the save game:\n{}"), GetSystemErrorMessage(ec)); - wxMessageBox(error_msg, _("Error"), wxOK | wxCENTRE, this); - return; - } - - m_target_id = target_id; - m_return_code = wxOK; - Close(); -} diff --git a/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.h b/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.h deleted file mode 100644 index 6f208fb8..00000000 --- a/src/gui/wxgui/dialogs/SaveImport/SaveTransfer.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -class wxComboBox; - -class SaveTransfer : public wxDialog -{ -public: - SaveTransfer(wxWindow* parent, uint64 title_id, const wxString& source_account, uint32 source_id); - - void EndModal(int retCode) override; - - uint32 GetTargetPersistentId() const { return m_target_id; } -private: - void OnTransfer(wxCommandEvent& event); - - wxComboBox* m_target_selection; - - uint32 m_target_id = 0; - const uint64 m_title_id; - const uint32 m_source_id; - int m_return_code = wxCANCEL; -}; diff --git a/src/gui/wxgui/helpers/wxControlObject.h b/src/gui/wxgui/helpers/wxControlObject.h deleted file mode 100644 index 5726001f..00000000 --- a/src/gui/wxgui/helpers/wxControlObject.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include - -class wxControlObject : public wxObject -{ -public: - wxControlObject(wxControl* control) - : m_control(control) {} - - template - T* GetControl() const - { - static_assert(std::is_base_of_v, "T must inherit from wxControl"); - return dynamic_cast(m_control); - } - - wxControl* GetControl() const { return m_control; } - -private: - wxControl* m_control; -}; - -class wxControlObjects : public wxObject -{ -public: - wxControlObjects(std::vector controls) - : m_controls(std::move(controls)) {} - - template - T* GetControl(int index) const - { - static_assert(std::is_base_of_v, "T must inherit from wxControl"); - if (index < 0 || index >= m_controls.size()) - return nullptr; - - return dynamic_cast(m_controls[index]); - } - - const std::vector& GetControls() const { return m_controls; } - -private: - std::vector m_controls; -}; diff --git a/src/gui/wxgui/helpers/wxCustomData.h b/src/gui/wxgui/helpers/wxCustomData.h deleted file mode 100644 index bd7b0184..00000000 --- a/src/gui/wxgui/helpers/wxCustomData.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include - -template -class wxCustomData : public wxClientData -{ -public: - wxCustomData() - : m_data({}) {} - - wxCustomData(T data) - : m_data(std::move(data)) { } - - const T& GetData() const { return m_data; } - - /// - /// obtains ownership of the data - /// - /// returns the stored data - T get() { return std::move(m_data); } - - /// - /// access to the data without obtaining ownership - /// - /// returns a reference to the stored data - T& ref() { return m_data; } - - bool operator==(const T& o) const { return m_data == o.m_data; } - bool operator!=(const T& o) const { return !(*this == o); } - -private: - T m_data; -}; - -//template -//class wxCustomObject : public wxObject -//{ -//public: -// wxCustomObject(T data) -// : m_data(std::move(data)) { } -// -// const T& GetData() const { return m_data; } -// -// bool operator==(const T& o) const { return m_data == o.m_data; } -// bool operator!=(const T& o) const { return !(*this == o); } -//private: -// T m_data; -//}; diff --git a/src/gui/wxgui/helpers/wxCustomEvents.cpp b/src/gui/wxgui/helpers/wxCustomEvents.cpp deleted file mode 100644 index 589b7c80..00000000 --- a/src/gui/wxgui/helpers/wxCustomEvents.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "wxgui/helpers/wxCustomEvents.h" - -wxDEFINE_EVENT(wxEVT_SET_STATUS_BAR_TEXT, wxSetStatusBarTextEvent); -wxDEFINE_EVENT(wxEVT_SET_GAUGE_VALUE, wxSetGaugeValue); -wxDEFINE_EVENT(wxEVT_REMOVE_ITEM, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_SET_TEXT, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_ENABLE, wxCommandEvent); \ No newline at end of file diff --git a/src/gui/wxgui/helpers/wxCustomEvents.h b/src/gui/wxgui/helpers/wxCustomEvents.h deleted file mode 100644 index 1eb213be..00000000 --- a/src/gui/wxgui/helpers/wxCustomEvents.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include - -class wxStatusBar; -class wxControl; - -wxDECLARE_EVENT(wxEVT_REMOVE_ITEM, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_SET_TEXT, wxCommandEvent); -wxDECLARE_EVENT(wxEVT_ENABLE, wxCommandEvent); - -class wxSetStatusBarTextEvent; -wxDECLARE_EVENT(wxEVT_SET_STATUS_BAR_TEXT, wxSetStatusBarTextEvent); -class wxSetStatusBarTextEvent : public wxCommandEvent -{ -public: - wxSetStatusBarTextEvent(const wxString& text, int number = 0, wxEventType type = wxEVT_SET_STATUS_BAR_TEXT, int id = 0) - : wxCommandEvent(type, id), m_text(text), m_number(number) {} - - wxSetStatusBarTextEvent(const wxSetStatusBarTextEvent& event) - : wxCommandEvent(event), m_text(event.GetText()), m_number(event.GetNumber()) {} - - - [[nodiscard]] const wxString& GetText() const { return m_text; } - [[nodiscard]] int GetNumber() const { return m_number; } - - wxEvent* Clone() const override - { - return new wxSetStatusBarTextEvent(*this); - } - -private: - const wxString m_text; - const int m_number; -}; -class wxSetGaugeValue; -wxDECLARE_EVENT(wxEVT_SET_GAUGE_VALUE, wxSetGaugeValue); -class wxSetGaugeValue : public wxCommandEvent -{ -public: - wxSetGaugeValue(int value, wxGauge* gauge, wxControl* text_ctrl = nullptr, const wxString& text = wxEmptyString, wxEventType type = wxEVT_SET_GAUGE_VALUE, int id = 0) - : wxCommandEvent(type, id), m_gauge_value(value), m_gauge_range(0), m_text_ctrl(text_ctrl), m_text(text) - { - SetEventObject(gauge); - } - - wxSetGaugeValue(int value, int range, wxGauge* gauge, wxControl* text_ctrl = nullptr, const wxString& text = wxEmptyString, wxEventType type = wxEVT_SET_GAUGE_VALUE, int id = 0) - : wxCommandEvent(type, id), m_gauge_value(value), m_gauge_range(range), m_text_ctrl(text_ctrl), m_text(text) - { - SetEventObject(gauge); - } - - wxSetGaugeValue(const wxSetGaugeValue& event) - : wxCommandEvent(event), m_gauge_value(event.GetValue()), m_gauge_range(event.GetRange()), m_text_ctrl(event.GetTextCtrl()), m_text(event.GetText()) - {} - - [[nodiscard]] int GetValue() const { return m_gauge_value; } - [[nodiscard]] int GetRange() const { return m_gauge_range; } - [[nodiscard]] wxGauge* GetGauge() const { return (wxGauge*)GetEventObject(); } - [[nodiscard]] const wxString& GetText() const { return m_text; } - [[nodiscard]] wxControl* GetTextCtrl() const { return m_text_ctrl; } - - wxEvent* Clone() const override - { - return new wxSetGaugeValue(*this); - } - -private: - const int m_gauge_value, m_gauge_range; - wxControl* m_text_ctrl; - const wxString m_text; -}; - diff --git a/src/gui/wxgui/helpers/wxHelpers.cpp b/src/gui/wxgui/helpers/wxHelpers.cpp deleted file mode 100644 index 0d734372..00000000 --- a/src/gui/wxgui/helpers/wxHelpers.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "wxgui/helpers/wxHelpers.h" - -#include -#include -#include -#include - -#if BOOST_OS_LINUX || BOOST_OS_BSD -#include -#include -#include -#include -#ifdef HAS_WAYLAND -#include -#endif -#endif - -#include "wxgui/helpers/wxControlObject.h" - -void wxAutosizeColumn(wxListCtrlBase* ctrl, int col) -{ - ctrl->SetColumnWidth(col, wxLIST_AUTOSIZE_USEHEADER); - int wh = ctrl->GetColumnWidth(col); - ctrl->SetColumnWidth(col, wxLIST_AUTOSIZE); - int wc = ctrl->GetColumnWidth(col); - if (wh > wc) - ctrl->SetColumnWidth(col, wxLIST_AUTOSIZE_USEHEADER); -} - -void wxAutosizeColumns(wxListCtrlBase* ctrl, int col_start, int col_end) -{ - wxWindowUpdateLocker lock(ctrl); - for (int i = col_start; i <= col_end; ++i) - wxAutosizeColumn(ctrl, i); -} - -void update_slider_text(wxCommandEvent& event, const wxFormatString& format /*= "%d%%"*/) -{ - const auto slider = dynamic_cast(event.GetEventObject()); - wxASSERT(slider); - - auto slider_text = dynamic_cast(event.GetEventUserData())->GetControl(); - wxASSERT(slider_text); - - slider_text->SetLabel(wxString::Format(format, slider->GetValue())); -} - -uint32 fix_raw_keycode(uint32 keycode, uint32 raw_flags) -{ -#if BOOST_OS_WINDOWS - const auto flags = (HIWORD(raw_flags) & 0xFFF); - if(keycode == VK_SHIFT) - { - if(flags == 0x2A) - return 160; - else if (flags == 0x36) - return 161; - } - else if (keycode == VK_CONTROL) - { - if (flags == 0x1d) - return 162; - else if (flags == 0x11d) - return 163; - } - else if (keycode == VK_MENU) - { - if ((flags & 0xFF) == 0x38) - return 164; - else if ((flags & 0xFF) == 0x38) - return 165; - } -#endif - - return keycode; -} - -WindowSystem::WindowHandleInfo initHandleContextFromWxWidgetsWindow(wxWindow* wxw) -{ - WindowSystem::WindowHandleInfo handleInfo; -#if BOOST_OS_WINDOWS - handleInfo.backend = WindowSystem::WindowHandleInfo::Backend::Windows; - handleInfo.surface = reinterpret_cast(wxw->GetHWND()); -#elif BOOST_OS_LINUX || BOOST_OS_BSD - GtkWidget* gtkWidget = (GtkWidget*)wxw->GetHandle(); // returns GtkWidget - gtk_widget_realize(gtkWidget); - GdkWindow* gdkWindow = gtk_widget_get_window(gtkWidget); - GdkDisplay* gdkDisplay = gdk_window_get_display(gdkWindow); - if (GDK_IS_X11_WINDOW(gdkWindow)) - { - handleInfo.backend = WindowSystem::WindowHandleInfo::Backend::X11; - handleInfo.surface = reinterpret_cast(gdk_x11_window_get_xid(gdkWindow)); - handleInfo.display = gdk_x11_display_get_xdisplay(gdkDisplay); - if (!handleInfo.display) - { - cemuLog_log(LogType::Force, "Unable to get xlib display"); - } - } -#ifdef HAS_WAYLAND - else if (GDK_IS_WAYLAND_WINDOW(gdkWindow)) - { - handleInfo.backend = WindowSystem::WindowHandleInfo::Backend::Wayland; - handleInfo.surface = gdk_wayland_window_get_wl_surface(gdkWindow); - handleInfo.display = gdk_wayland_display_get_wl_display(gdkDisplay); - } -#endif - else - { - cemuLog_log(LogType::Force, "Unsuported GTK backend"); - } -#elif BOOST_OS_MACOS - handleInfo.backend = WindowSystem::WindowHandleInfo::Backend::Cocoa; - handleInfo.surface = reinterpret_cast(wxw->GetHandle()); -#endif - return handleInfo; -} diff --git a/src/gui/wxgui/helpers/wxHelpers.h b/src/gui/wxgui/helpers/wxHelpers.h deleted file mode 100644 index fa42338b..00000000 --- a/src/gui/wxgui/helpers/wxHelpers.h +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include "interface/WindowSystem.h" -#include -#include -#include - -template <> -struct fmt::formatter : formatter -{ - template - auto format(const wxString& str, FormatContext& ctx) const - { - return formatter::format(str.ToStdString(), ctx); - } -}; - -class wxTempEnable -{ -public: - wxTempEnable(wxControl* control, bool state = true) - : m_control(control), m_state(state) - { - m_control->Enable(m_state); - } - - wxTempEnable(const wxTempEnable&) = delete; - wxTempEnable(wxTempEnable&&) noexcept = default; - - ~wxTempEnable() - { - if(m_control) - m_control->Enable(!m_state); - } - -private: - wxControl* m_control; - const bool m_state; -}; - -class wxTempDisable : wxTempEnable -{ -public: - wxTempDisable(wxControl* control) - : wxTempEnable(control, false) {} -}; - -template -wxString formatWxString(const wxString& format, TArgs&&...args) -{ - return wxString::FromUTF8(fmt::format(fmt::runtime(format.utf8_string()), std::forward(args)...)); -} - -// executes a function when destroying the obj -template -class wxDTorFunc -{ -public: - wxDTorFunc(TFunction&& func, TArgs&&... args) - { - auto bound = std::bind(std::forward(func), std::forward(args)...); - // m_function = [bound] { bound(); }; - m_function = [bound{ std::move(bound) }]{ bound(); }; - - } - ~wxDTorFunc() - { - m_function(); - } -private: - std::function m_function; -}; - -void wxAutosizeColumn(wxListCtrlBase* ctrl, int col); -void wxAutosizeColumns(wxListCtrlBase* ctrl, int col_start, int col_end); - -template -T get_next_sibling(const T element) -{ - static_assert(std::is_pointer_v && std::is_base_of_v>, "element must be a pointer and inherit from wxControl"); - for (auto sibling = element->GetNextSibling(); sibling; sibling = sibling->GetNextSibling()) - { - if (auto result = dynamic_cast(sibling)) - return result; - } - - return nullptr; -} - -template -T get_prev_sibling(const T element) -{ - static_assert(std::is_pointer_v && std::is_base_of_v>, "element must be a pointer and inherit from wxControl"); - for (auto sibling = element->GetPrevSibling(); sibling; sibling = sibling->GetPrevSibling()) - { - auto result = dynamic_cast(sibling); - if (result) - return result; - } - - return nullptr; -} - -void update_slider_text(wxCommandEvent& event, const wxFormatString& format = "%d%%"); - -uint32 fix_raw_keycode(uint32 keycode, uint32 raw_flags); - -WindowSystem::WindowHandleInfo initHandleContextFromWxWidgetsWindow(wxWindow* wxw); diff --git a/src/gui/wxgui/helpers/wxLogEvent.h b/src/gui/wxgui/helpers/wxLogEvent.h deleted file mode 100644 index 23f6533d..00000000 --- a/src/gui/wxgui/helpers/wxLogEvent.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -class wxLogEvent; -wxDECLARE_EVENT(EVT_LOG, wxLogEvent); - -class wxLogEvent : public wxCommandEvent -{ -public: - wxLogEvent(const wxString& filter, const wxString& message) - : wxCommandEvent(EVT_LOG), m_filter(filter), m_message(message) { } - - wxLogEvent(const wxLogEvent& event) - : wxCommandEvent(event), m_filter(event.GetFilter()), m_message(event.GetMessage()) { } - - wxEvent* Clone() const { return new wxLogEvent(*this); } - - [[nodiscard]] const wxString& GetFilter() const - { - return m_filter; - } - - [[nodiscard]] const wxString& GetMessage() const - { - return m_message; - } - -private: - wxString m_filter; - wxString m_message; -}; diff --git a/src/gui/wxgui/helpers/wxWayland.cpp b/src/gui/wxgui/helpers/wxWayland.cpp deleted file mode 100644 index 6e7ff789..00000000 --- a/src/gui/wxgui/helpers/wxWayland.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "wxgui/helpers/wxWayland.h" - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - -#include - -bool wxWlIsWaylandWindow(wxWindow* window) -{ - GtkWidget* gtkWindow = static_cast(window->GetHandle()); - GdkWindow* gdkWindow = gtk_widget_get_window(gtkWindow); - return GDK_IS_WAYLAND_WINDOW(gdkWindow); -} - -void wxWlSetAppId(wxFrame* frame, const char* applicationId) -{ - GtkWidget* gtkWindow = static_cast(frame->GetHandle()); - gtk_widget_realize(gtkWindow); - GdkWindow* gdkWindow = gtk_widget_get_window(gtkWindow); - static auto gdk_wl_set_app_id = reinterpret_cast(dlsym(nullptr, "gdk_wayland_window_set_application_id")); - if (gdk_wl_set_app_id) - gdk_wl_set_app_id(gdkWindow, applicationId); -} - -#endif // BOOST_OS_LINUX && HAS_WAYLAND diff --git a/src/gui/wxgui/helpers/wxWayland.h b/src/gui/wxgui/helpers/wxWayland.h deleted file mode 100644 index 0f162787..00000000 --- a/src/gui/wxgui/helpers/wxWayland.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#if ( BOOST_OS_LINUX || BOOST_OS_BSD ) && HAS_WAYLAND - -#include -#include -#include -#include -#include -#include "wayland-viewporter-client-protocol.h" - -class wxWlSubsurface -{ - static void registry_add_object(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) - { - auto wlSubsurface = static_cast(data); - - if (!strcmp(interface, wl_subcompositor_interface.name)) - { - wlSubsurface->m_subcompositor = static_cast(wl_registry_bind(registry, name, &wl_subcompositor_interface, 1)); - } - else if (!strcmp(interface, wp_viewporter_interface.name)) - { - wlSubsurface->m_viewporter = static_cast(wl_registry_bind(registry, name, &wp_viewporter_interface, 1)); - } - } - static void registry_remove_object(void* /*data*/, struct wl_registry* /*registry*/, uint32_t /*name*/) {} - - wl_registry_listener m_registry_listener = {®istry_add_object, ®istry_remove_object}; - wl_subcompositor* m_subcompositor; - wl_surface* m_surface; - wl_subsurface* m_subsurface; - wp_viewporter* m_viewporter = NULL; - wp_viewport* m_viewport = NULL; - int32_t m_xPos = 0; - int32_t m_yPos = 0; - int32_t m_width = 0; - int32_t m_height = 0; - -public: - wxWlSubsurface(wxWindow* window) - { - GtkWidget* widget = static_cast(window->GetHandle()); - gtk_widget_realize(widget); - GdkWindow* gdkWindow = gtk_widget_get_window(widget); - GdkDisplay* gdkDisplay = gdk_window_get_display(gdkWindow); - wl_display* display = gdk_wayland_display_get_wl_display(gdkDisplay); - wl_surface* surface = gdk_wayland_window_get_wl_surface(gdkWindow); - wl_compositor* compositor = gdk_wayland_display_get_wl_compositor(gdkDisplay); - wl_registry* registry = wl_display_get_registry(display); - wl_registry_add_listener(registry, &m_registry_listener, this); - wl_display_roundtrip(display); - m_surface = wl_compositor_create_surface(compositor); - if (m_viewporter) - m_viewport = wp_viewporter_get_viewport(m_viewporter, m_surface); - wl_region* region = wl_compositor_create_region(compositor); - wl_surface_set_input_region(m_surface, region); - m_subsurface = wl_subcompositor_get_subsurface(m_subcompositor, m_surface, surface); - wl_subsurface_set_desync(m_subsurface); - auto sRect = window->GetScreenRect(); - setSize(sRect.x, sRect.y, sRect.width, sRect.height); - wl_region_destroy(region); - } - - wl_surface* getSurface() const { return m_surface; } - - void setSize(int32_t xPos, int32_t yPos, int32_t width, int32_t height) - { - if (xPos != m_xPos || m_yPos != yPos || m_width != width || m_height != height) - { - m_xPos = xPos; - m_yPos = yPos; - m_height = height; - m_width = width; - wl_subsurface_set_position(m_subsurface, m_xPos, m_yPos); - if (m_viewport) - wp_viewport_set_destination(m_viewport, m_width, m_height); - wl_surface_commit(m_surface); - } - } - - ~wxWlSubsurface() - { - wl_subsurface_destroy(m_subsurface); - wl_surface_destroy(m_surface); - wl_subcompositor_destroy(m_subcompositor); - } -}; - -bool wxWlIsWaylandWindow(wxWindow* window); -void wxWlSetAppId(wxFrame* frame, const char* application_id); - -#endif // BOOST_OS_LINUX && HAS_WAYLAND diff --git a/src/gui/wxgui/input/HotkeySettings.cpp b/src/gui/wxgui/input/HotkeySettings.cpp deleted file mode 100644 index d84ffa1b..00000000 --- a/src/gui/wxgui/input/HotkeySettings.cpp +++ /dev/null @@ -1,522 +0,0 @@ -#include "wxgui/input/HotkeySettings.h" -#include "Cafe/HW/Latte/Renderer/Renderer.h" -#include "interface/WindowSystem.h" -#include -#include "input/InputManager.h" -#include "HotkeySettings.h" -#include "MainWindow.h" - -#include -#include - -#if BOOST_OS_WINDOWS -#include -#endif - -#if BOOST_OS_LINUX || BOOST_OS_MACOS || BOOST_OS_BSD -#include "resource/embedded/resources.h" -#endif - -std::optional GenerateScreenshotFilename(bool isDRC) -{ - fs::path screendir = ActiveSettings::GetUserDataPath("screenshots"); - // build screenshot name with format Screenshot_YYYY-MM-DD_HH-MM-SS[_GamePad].png - // if the file already exists add a suffix counter (_2.png, _3.png etc) - std::time_t time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - std::tm* tm = std::localtime(&time_t); - - std::string screenshotFileName = fmt::format("Screenshot_{:04}-{:02}-{:02}_{:02}-{:02}-{:02}", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); - if (isDRC) - screenshotFileName.append("_GamePad"); - - fs::path screenshotPath; - for (sint32 i = 0; i < 999; i++) - { - screenshotPath = screendir; - if (i == 0) - screenshotPath.append(fmt::format("{}.png", screenshotFileName)); - else - screenshotPath.append(fmt::format("{}_{}.png", screenshotFileName, i + 1)); - - std::error_code ec; - bool exists = fs::exists(screenshotPath, ec); - - if (!ec && !exists) - return screenshotPath; - } - return std::nullopt; -} - -bool SaveScreenshotToFile(const fs::path& imagePath, const wxImage& image) -{ - std::error_code ec; - fs::create_directories(imagePath.parent_path(), ec); - if (ec) - return false; - - // suspend wxWidgets logging for the lifetime this object, to prevent a message box if wxImage::SaveFile fails - wxLogNull _logNo; - return image.SaveFile(imagePath.wstring()); -} - -bool SaveScreenshotToClipboard(const wxImage& image) -{ - static std::mutex s_clipboardMutex; - bool success = false; - - s_clipboardMutex.lock(); - if (wxTheClipboard->Open()) - { - wxTheClipboard->SetData(new wxImageDataObject(image)); - wxTheClipboard->Close(); - success = true; - } - s_clipboardMutex.unlock(); - - return success; -} - -std::optional SaveScreenshot(std::vector data, int width, int height, bool mainWindow) -{ -#if BOOST_OS_WINDOWS - // on Windows wxWidgets uses OLE API for the clipboard - // to make this work we need to call OleInitialize() on the same thread - OleInitialize(nullptr); -#endif - bool save_screenshot = GetWxGUIConfig().save_screenshot; - wxImage image(width, height, data.data(), true); - if (mainWindow) - { - if (SaveScreenshotToClipboard(image)) - { - if (!save_screenshot) - return _tr("Screenshot saved to clipboard"); - } - else - { - return _tr("Failed to open clipboard"); - } - } - if (save_screenshot) - { - auto imagePath = GenerateScreenshotFilename(mainWindow); - if (imagePath.has_value() && SaveScreenshotToFile(imagePath.value(), image)) - { - if (mainWindow) - return _tr("Screenshot saved"); - } - else - { - return _tr("Failed to save screenshot to file"); - } - } - return std::nullopt; -} - -extern WindowSystem::WindowInfo g_window_info; - -std::unordered_map> HotkeySettings::s_cfgHotkeyToFuncMap; - -struct HotkeyEntry -{ - std::unique_ptr name; - std::unique_ptr keyInput; - std::unique_ptr controllerInput; - sHotkeyCfg& hotkey; - - HotkeyEntry(wxStaticText* name, wxButton* keyInput, wxButton* controllerInput, sHotkeyCfg& hotkey) - : name(name), keyInput(keyInput), controllerInput(controllerInput), hotkey(hotkey) - { - keyInput->SetClientData(&hotkey); - controllerInput->SetClientData(&hotkey); - } -}; - -HotkeySettings::HotkeySettings(wxWindow* parent) - : wxFrame(parent, wxID_ANY, _("Hotkey Settings")) -{ - SetIcon(wxICON(X_HOTKEY_SETTINGS)); - - m_sizer = new wxFlexGridSizer(0, 3, 5, 5); - m_sizer->AddGrowableCol(1); - m_sizer->AddGrowableCol(2); - - m_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_THEME); - m_panel->SetSizer(m_sizer); - - Center(); - - SetActiveController(); - - CreateColumnHeaders(); - - /* global modifier */ - CreateHotkeyRow(_tr("Hotkey modifier"), s_cfgHotkeys.modifiers); - m_hotkeys.at(0).keyInput->Hide(); - - /* hotkeys */ - CreateHotkeyRow(_tr("Toggle fullscreen"), s_cfgHotkeys.toggleFullscreen); - CreateHotkeyRow(_tr("Take screenshot"), s_cfgHotkeys.takeScreenshot); - CreateHotkeyRow(_tr("Toggle fast-forward"), s_cfgHotkeys.toggleFastForward); -#ifdef CEMU_DEBUG_ASSERT - CreateHotkeyRow(_tr("End emulation"), s_cfgHotkeys.endEmulation); -#endif - CreateHotkeyRow(_tr("Exit application"), s_cfgHotkeys.exitApplication); - - m_controllerTimer = new wxTimer(this); - Bind(wxEVT_TIMER, &HotkeySettings::OnControllerTimer, this); - - m_sizer->SetSizeHints(this); -} - -HotkeySettings::~HotkeySettings() -{ - m_controllerTimer->Stop(); - if (m_needToSave) - { - GetConfigHandle().Save(); - } -} - -void HotkeySettings::Init(MainWindow* mainWindowFrame) -{ - s_cfgHotkeyToFuncMap.insert({ - {&s_cfgHotkeys.toggleFullscreen, [](void) { - s_mainWindow->SetFullScreen(!s_mainWindow->IsFullScreen()); - }}, - {&s_cfgHotkeys.toggleFullscreenAlt, [](void) { - s_mainWindow->SetFullScreen(!s_mainWindow->IsFullScreen()); - }}, - {&s_cfgHotkeys.exitFullscreen, [](void) { - s_mainWindow->SetFullScreen(false); - }}, - {&s_cfgHotkeys.takeScreenshot, [](void) { - if (g_renderer) - g_renderer->RequestScreenshot(SaveScreenshot); - }}, - {&s_cfgHotkeys.toggleFastForward, [](void) { - ActiveSettings::SetTimerShiftFactor((ActiveSettings::GetTimerShiftFactor() < 3) ? 3 : 1); - }}, - {&s_cfgHotkeys.exitApplication, [](void) { - auto closeEvent = new wxCloseEvent{wxEVT_CLOSE_WINDOW, s_mainWindow->GetId()}; - closeEvent->SetCanVeto(false); - wxQueueEvent(s_mainWindow, closeEvent); - }}, -#ifdef CEMU_DEBUG_ASSERT - {&s_cfgHotkeys.endEmulation, [](void) { - wxTheApp->CallAfter([]() { - s_mainWindow->EndEmulation(); - }); - }}, -#endif - }); - - s_keyboardHotkeyToFuncMap.reserve(s_cfgHotkeyToFuncMap.size()); - for (const auto& [cfgHotkey, func] : s_cfgHotkeyToFuncMap) - { - auto keyboardHotkey = cfgHotkey->keyboard.raw; - if (keyboardHotkey > sHotkeyCfg::keyboardNone) - { - s_keyboardHotkeyToFuncMap[keyboardHotkey] = func; - } - auto controllerHotkey = cfgHotkey->controller; - if (controllerHotkey > sHotkeyCfg::controllerNone) - { - s_controllerHotkeyToFuncMap[controllerHotkey] = func; - } - } - s_mainWindow = mainWindowFrame; -} - -void HotkeySettings::CreateColumnHeaders(void) -{ - auto* emptySpace = new wxStaticText(m_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); - auto* keyboard = new wxStaticText(m_panel, wxID_ANY, _tr("Keyboard"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); - auto* controller = new wxStaticText(m_panel, wxID_ANY, _tr("Controller"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); - keyboard->SetFont(keyboard->GetFont().Bold().Larger()); - controller->SetFont(controller->GetFont().Bold().Larger()); - - keyboard->SetMinSize(m_minButtonSize); - controller->SetMinSize(m_minButtonSize); - - auto flags = wxSizerFlags().Expand().Border(wxTOP, 10); - m_sizer->Add(emptySpace, flags); - m_sizer->Add(keyboard, flags); - m_sizer->Add(controller, flags); -} - -void HotkeySettings::CreateHotkeyRow(const wxString& label, sHotkeyCfg& cfgHotkey) -{ - auto* name = new wxStaticText(m_panel, wxID_ANY, label); - auto* keyInput = new wxButton(m_panel, wxID_ANY, To_wxString(cfgHotkey.keyboard), wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBU_EXACTFIT); - auto* controllerInput = new wxButton(m_panel, wxID_ANY, To_wxString(cfgHotkey.controller), wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBU_EXACTFIT); - - /* for starting input */ - keyInput->Bind(wxEVT_BUTTON, &HotkeySettings::OnKeyboardHotkeyInputLeftClick, this); - controllerInput->Bind(wxEVT_BUTTON, &HotkeySettings::OnControllerHotkeyInputLeftClick, this); - - /* for cancelling and clearing input */ - keyInput->Bind(wxEVT_RIGHT_UP, &HotkeySettings::OnKeyboardHotkeyInputRightClick, this); - controllerInput->Bind(wxEVT_RIGHT_UP, &HotkeySettings::OnControllerHotkeyInputRightClick, this); - - keyInput->SetMinSize(m_minButtonSize); - controllerInput->SetMinSize(m_minButtonSize); - - const wxColour inputButtonColor = GetBackgroundColour(); - keyInput->SetBackgroundColour(inputButtonColor); - controllerInput->SetBackgroundColour(inputButtonColor); - - auto flags = wxSizerFlags(1).Expand().Border(wxALL, 5).CenterVertical(); - m_sizer->Add(name, flags); - m_sizer->Add(keyInput, flags); - m_sizer->Add(controllerInput, flags); - - m_hotkeys.emplace_back(name, keyInput, controllerInput, cfgHotkey); -} - -void HotkeySettings::OnControllerTimer(wxTimerEvent& event) -{ - if (m_activeController.expired()) - { - m_controllerTimer->Stop(); - return; - } - auto& controller = *m_activeController.lock(); - auto buttons = controller.update_state().buttons; - if (!buttons.IsIdle()) - { - for (const auto& newHotkey : buttons.GetButtonList()) - { - m_controllerTimer->Stop(); - auto* inputButton = static_cast(m_controllerTimer->GetClientData()); - auto& cfgHotkey = *static_cast(inputButton->GetClientData()); - const auto oldHotkey = cfgHotkey.controller; - const bool isModifier = (&cfgHotkey == &s_cfgHotkeys.modifiers); - /* ignore same hotkeys and block duplicate hotkeys */ - if ((newHotkey != oldHotkey) && (isModifier || (newHotkey != s_cfgHotkeys.modifiers.controller)) && - (s_controllerHotkeyToFuncMap.find(newHotkey) == s_controllerHotkeyToFuncMap.end())) - { - m_needToSave |= true; - cfgHotkey.controller = newHotkey; - /* don't bind modifier to map */ - if (!isModifier) - { - s_controllerHotkeyToFuncMap.erase(oldHotkey); - s_controllerHotkeyToFuncMap[newHotkey] = s_cfgHotkeyToFuncMap.at(&cfgHotkey); - } - } - FinalizeInput(inputButton); - return; - } - } -} - -void HotkeySettings::OnKeyboardHotkeyInputLeftClick(wxCommandEvent& event) -{ - auto* inputButton = static_cast(event.GetEventObject()); - if (m_activeInputButton) - { - /* ignore multiple clicks of the same button */ - if (inputButton == m_activeInputButton) return; - RestoreInputButton(); - } - inputButton->Bind(wxEVT_KEY_UP, &HotkeySettings::OnKeyUp, this); - inputButton->SetLabelText(m_editModeHotkeyText); - m_activeInputButton = inputButton; -} - -void HotkeySettings::OnControllerHotkeyInputLeftClick(wxCommandEvent& event) -{ - auto* inputButton = static_cast(event.GetEventObject()); - if (m_activeInputButton) - { - /* ignore multiple clicks of the same button */ - if (inputButton == m_activeInputButton) return; - RestoreInputButton(); - } - m_controllerTimer->Stop(); - if (!SetActiveController()) - { - return; - } - inputButton->SetLabelText(m_editModeHotkeyText); - m_controllerTimer->SetClientData(inputButton); - m_controllerTimer->Start(25); - m_activeInputButton = inputButton; -} - -void HotkeySettings::OnKeyboardHotkeyInputRightClick(wxMouseEvent& event) -{ - if (m_activeInputButton) - { - RestoreInputButton(); - return; - } - auto* inputButton = static_cast(event.GetEventObject()); - auto& cfgHotkey = *static_cast(inputButton->GetClientData()); - uKeyboardHotkey newHotkey{sHotkeyCfg::keyboardNone}; - if (cfgHotkey.keyboard.raw != newHotkey.raw) - { - m_needToSave |= true; - s_keyboardHotkeyToFuncMap.erase(cfgHotkey.keyboard.raw); - cfgHotkey.keyboard = newHotkey; - FinalizeInput(inputButton); - } -} - -void HotkeySettings::OnControllerHotkeyInputRightClick(wxMouseEvent& event) -{ - if (m_activeInputButton) - { - RestoreInputButton(); - return; - } - auto* inputButton = static_cast(event.GetEventObject()); - auto& cfgHotkey = *static_cast(inputButton->GetClientData()); - ControllerHotkey_t newHotkey{sHotkeyCfg::controllerNone}; - if (cfgHotkey.controller != newHotkey) - { - m_needToSave |= true; - s_controllerHotkeyToFuncMap.erase(cfgHotkey.controller); - cfgHotkey.controller = newHotkey; - FinalizeInput(inputButton); - } -} - -bool HotkeySettings::SetActiveController(void) -{ - auto emulatedController = InputManager::instance().get_controller(0); - if (emulatedController.use_count() <= 1) - { - return false; - } - const auto& controllers = emulatedController->get_controllers(); - if (controllers.empty()) - { - return false; - } - m_activeController = controllers.at(0); - return true; -} - -void HotkeySettings::OnKeyUp(wxKeyEvent& event) -{ - auto* inputButton = static_cast(event.GetEventObject()); - auto& cfgHotkey = *static_cast(inputButton->GetClientData()); - if (auto keycode = event.GetKeyCode(); IsValidKeycodeUp(keycode)) - { - auto oldHotkey = cfgHotkey.keyboard; - uKeyboardHotkey newHotkey{}; - newHotkey.key = keycode; - newHotkey.alt = event.AltDown(); - newHotkey.ctrl = event.ControlDown(); - newHotkey.shift = event.ShiftDown(); - if ((newHotkey.raw != oldHotkey.raw) && - (s_keyboardHotkeyToFuncMap.find(newHotkey.raw) == s_keyboardHotkeyToFuncMap.end())) - { - m_needToSave |= true; - cfgHotkey.keyboard = newHotkey; - s_keyboardHotkeyToFuncMap.erase(oldHotkey.raw); - s_keyboardHotkeyToFuncMap[newHotkey.raw] = s_cfgHotkeyToFuncMap.at(&cfgHotkey); - } - } - FinalizeInput(inputButton); -} - -template -void HotkeySettings::FinalizeInput(wxButton* inputButton) -{ - auto& cfgHotkey = *static_cast(inputButton->GetClientData()); - if constexpr (std::is_same_v) - { - inputButton->Unbind(wxEVT_KEY_UP, &HotkeySettings::OnKeyUp, this); - inputButton->SetLabelText(To_wxString(cfgHotkey.keyboard)); - } - else if constexpr (std::is_same_v) - { - inputButton->SetLabelText(To_wxString(cfgHotkey.controller)); - } - m_activeInputButton = nullptr; -} - -template -void HotkeySettings::RestoreInputButton(void) -{ - FinalizeInput(m_activeInputButton); -} - -bool HotkeySettings::IsValidKeycodeUp(int keycode) -{ - switch (keycode) - { - case WXK_NONE: - case WXK_ESCAPE: - case WXK_ALT: - case WXK_CONTROL: - case WXK_SHIFT: - return false; - default: - return true; - } -} - -wxString HotkeySettings::To_wxString(uKeyboardHotkey hotkey) -{ - if (hotkey.raw == sHotkeyCfg::keyboardNone) { - return m_disabledHotkeyText; - } - wxString ret{}; - if (hotkey.alt) - { - ret.append(_("Alt + ")); - } - if (hotkey.ctrl) - { - ret.append(_("Ctrl + ")); - } - if (hotkey.shift) - { - ret.append(_("Shift + ")); - } - ret.append(wxAcceleratorEntry(0, hotkey.key).ToString()); - return ret; -} - -wxString HotkeySettings::To_wxString(ControllerHotkey_t hotkey) -{ - if ((hotkey == sHotkeyCfg::controllerNone) || m_activeController.expired()) { - return m_disabledHotkeyText; - } - return m_activeController.lock()->get_button_name(hotkey); -} - -void HotkeySettings::CaptureInput(wxKeyEvent& event) -{ - uKeyboardHotkey hotkey{}; - hotkey.key = event.GetKeyCode(); - hotkey.alt = event.AltDown(); - hotkey.ctrl = event.ControlDown(); - hotkey.shift = event.ShiftDown(); - const auto it = s_keyboardHotkeyToFuncMap.find(hotkey.raw); - if (it != s_keyboardHotkeyToFuncMap.end()) - it->second(); -} - -void HotkeySettings::CaptureInput(const ControllerState& currentState, const ControllerState& lastState) -{ - const auto& modifier = s_cfgHotkeys.modifiers.controller; - if ((modifier >= 0) && currentState.buttons.GetButtonState(modifier)) - { - for (const auto& buttonId : currentState.buttons.GetButtonList()) - { - const auto it = s_controllerHotkeyToFuncMap.find(buttonId); - if (it == s_controllerHotkeyToFuncMap.end()) - continue; - /* only capture clicks */ - if (lastState.buttons.GetButtonState(buttonId)) - break; - it->second(); - break; - } - } -} diff --git a/src/gui/wxgui/input/HotkeySettings.h b/src/gui/wxgui/input/HotkeySettings.h deleted file mode 100644 index 4ed1e396..00000000 --- a/src/gui/wxgui/input/HotkeySettings.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include -#include "wxCemuConfig.h" -#include "input/api/Controller.h" - -class HotkeyEntry; - -class HotkeySettings : public wxFrame -{ -public: - static void Init(class MainWindow* mainWindow); - - static void CaptureInput(wxKeyEvent& event); - static void CaptureInput(const ControllerState& currentState, const ControllerState& lastState); - - HotkeySettings(wxWindow* parent); - ~HotkeySettings(); - -private: - inline static class MainWindow* s_mainWindow = nullptr; - static std::unordered_map> s_cfgHotkeyToFuncMap; - inline static std::unordered_map> s_keyboardHotkeyToFuncMap{}; - inline static std::unordered_map> s_controllerHotkeyToFuncMap{}; - inline static auto& s_cfgHotkeys = GetWxGUIConfig().hotkeys; - - wxPanel* m_panel; - wxFlexGridSizer* m_sizer; - wxButton* m_activeInputButton{ nullptr }; - wxTimer* m_controllerTimer{ nullptr }; - const wxSize m_minButtonSize{ 250, 45 }; - const wxString m_disabledHotkeyText{ _("----") }; - const wxString m_editModeHotkeyText{ "" }; - - std::vector m_hotkeys; - std::weak_ptr m_activeController{}; - bool m_needToSave = false; - - /* helpers */ - void CreateColumnHeaders(void); - void CreateHotkeyRow(const wxString& label, sHotkeyCfg& cfgHotkey); - wxString To_wxString(uKeyboardHotkey hotkey); - wxString To_wxString(ControllerHotkey_t hotkey); - bool IsValidKeycodeUp(int keycode); - bool SetActiveController(void); - - template - void FinalizeInput(wxButton* inputButton); - - template - void RestoreInputButton(void); - - /* events */ - void OnKeyboardHotkeyInputLeftClick(wxCommandEvent& event); - void OnControllerHotkeyInputLeftClick(wxCommandEvent& event); - void OnKeyboardHotkeyInputRightClick(wxMouseEvent& event); - void OnControllerHotkeyInputRightClick(wxMouseEvent& event); - void OnKeyUp(wxKeyEvent& event); - void OnControllerTimer(wxTimerEvent& event); -}; diff --git a/src/gui/wxgui/input/InputAPIAddWindow.cpp b/src/gui/wxgui/input/InputAPIAddWindow.cpp deleted file mode 100644 index b10a2c75..00000000 --- a/src/gui/wxgui/input/InputAPIAddWindow.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include "wxgui/input/InputAPIAddWindow.h" - -#include "input/InputManager.h" -#include "wxgui/helpers/wxCustomData.h" -#include "wxgui/helpers/wxHelpers.h" -#include "input/api/Controller.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "input/ControllerFactory.h" - -wxDEFINE_EVENT(wxControllersRefreshed, wxCommandEvent); - -using wxTypeData = wxCustomData; -using wxControllerData = wxCustomData; - -InputAPIAddWindow::InputAPIAddWindow(wxWindow* parent, const wxPoint& position, - const std::vector& controllers) - : wxDialog(parent, wxID_ANY, "Add input API", position, wxDefaultSize, wxCAPTION), m_controllers(controllers) -{ - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - - { - auto* api_row = new wxFlexGridSizer(2); - - // API - api_row->Add(new wxStaticText(this, wxID_ANY, _("API")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_input_api = new wxChoice(this, wxID_ANY); - auto& providers = InputManager::instance().get_api_providers(); - for (const auto& p : providers) - { - if (p.empty()) - continue; - - const auto provider = *p.begin(); - m_input_api->Append(wxString::FromUTF8(provider->api_name()), new wxTypeData(provider->api())); - } - - m_input_api->Bind(wxEVT_CHOICE, &InputAPIAddWindow::on_api_selected, this); - api_row->Add(m_input_api, 1, wxALL | wxEXPAND, 5); - - // Controller - api_row->Add(new wxStaticText(this, wxID_ANY, _("Controller")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - m_controller_list = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, - wxCB_READONLY); - m_controller_list->Bind(wxEVT_COMBOBOX_DROPDOWN, &InputAPIAddWindow::on_controller_dropdown, this); - m_controller_list->Bind(wxEVT_COMBOBOX, &InputAPIAddWindow::on_controller_selected, this); - m_controller_list->SetMinSize(wxSize(240, -1)); - m_controller_list->Disable(); - api_row->Add(m_controller_list, 1, wxALL | wxEXPAND, 5); - - sizer->Add(api_row, 0, wxEXPAND, 5); - } - - sizer->Add(new wxStaticLine(this), 0, wxEXPAND); - - { - auto* end_row = new wxBoxSizer(wxHORIZONTAL); - - m_ok_button = new wxButton(this, wxID_ANY, _("Add")); - m_ok_button->Bind(wxEVT_BUTTON, &InputAPIAddWindow::on_add_button, this); - m_ok_button->Disable(); - end_row->Add(m_ok_button, 0, wxALL, 5); - - auto* cancel_button = new wxButton(this, wxID_ANY, _("Cancel")); - cancel_button->Bind(wxEVT_BUTTON, &InputAPIAddWindow::on_close_button, this); - end_row->Add(cancel_button, 0, wxALL, 5); - - sizer->Add(end_row, 0, wxEXPAND, 5); - } - - { - // optional settings - m_settings_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto* panel_sizer = new wxBoxSizer(wxVERTICAL); - panel_sizer->Add(new wxStaticLine(m_settings_panel), 0, wxEXPAND, 0); - - { - auto* row = new wxBoxSizer(wxHORIZONTAL); - // we only have dsu settings atm, so add elements now - row->Add(new wxStaticText(m_settings_panel, wxID_ANY, _("IP")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_ip = new wxTextCtrl(m_settings_panel, wxID_ANY, "127.0.0.1"); - row->Add(m_ip, 0, wxALL, 5); - - row->Add(new wxStaticText(m_settings_panel, wxID_ANY, _("Port")), 0, wxALIGN_CENTER_VERTICAL | wxALL, 5); - m_port = new wxTextCtrl(m_settings_panel, wxID_ANY, "26760"); - row->Add(m_port, 0, wxALL, 5); - - panel_sizer->Add(row, 0, wxEXPAND); - } - - m_settings_panel->SetSizer(panel_sizer); - m_settings_panel->Layout(); - m_settings_panel->Hide(); - - sizer->Add(m_settings_panel, 1, wxEXPAND); - } - - this->SetSizer(sizer); - this->Layout(); - sizer->Fit(this); - - this->Bind(wxControllersRefreshed, &InputAPIAddWindow::on_controllers_refreshed, this); -} - -InputAPIAddWindow::~InputAPIAddWindow() -{ - discard_thread_result(); -} - -void InputAPIAddWindow::on_add_button(wxCommandEvent& event) -{ - const auto selection = m_input_api->GetSelection(); - if (selection == wxNOT_FOUND) - { - cemu_assert_debug(false); - EndModal(wxID_CANCEL); - return; - } - - for (const auto& c : m_controllers) - { - if (*c == *m_controller) - { - wxMessageBox(_("The controller is already added!"), _("Error"), wxOK | wxCENTRE | wxICON_ERROR, this); - return; - } - } - - m_type = static_cast(m_input_api->GetClientObject(selection))->get(); - EndModal(wxID_OK); -} - -void InputAPIAddWindow::on_close_button(wxCommandEvent& event) -{ - EndModal(wxID_CANCEL); -} - -bool InputAPIAddWindow::has_custom_settings() const -{ - const auto selection = m_input_api->GetStringSelection(); - return selection == wxString::FromUTF8(to_string(InputAPI::DSUClient)); -} - -std::unique_ptr InputAPIAddWindow::get_settings() const -{ - if (!has_custom_settings()) - return {}; - - return std::make_unique(m_ip->GetValue().ToStdString(), - ConvertString(m_port->GetValue().ToStdString())); -} - -void InputAPIAddWindow::on_api_selected(wxCommandEvent& event) -{ - discard_thread_result(); - - if (m_input_api->GetSelection() == wxNOT_FOUND) - return; - - m_controller_list->Enable(); - m_controller_list->SetSelection(wxNOT_FOUND); - - const auto selection = m_input_api->GetStringSelection(); - // keyboard is a special case, as theres only one device supported atm - if (selection == wxString::FromUTF8(to_string(InputAPI::Keyboard))) - { - const auto controllers = InputManager::instance().get_api_provider(InputAPI::Keyboard)->get_controllers(); - if (!controllers.empty()) - { - m_controller = controllers[0]; - m_ok_button->Enable(); - - m_controller_list->Clear(); - const auto display_name = controllers[0]->display_name(); - const auto index = m_controller_list->Append(display_name, new wxCustomData(controllers[0])); - m_controller_list->SetSelection(index); - } - } - else - { -#if BOOST_OS_LINUX || BOOST_OS_BSD - // We rely on the wxEVT_COMBOBOX_DROPDOWN event to trigger filling the controller list, - // but on wxGTK the dropdown button cannot be clicked if the list is empty - // so as a quick and dirty workaround we fill the list here - wxCommandEvent tmpCmdEvt; - on_controller_dropdown(tmpCmdEvt); -#endif - } - - const auto show_settings = has_custom_settings(); - // dsu has special settings for ip/port - if (show_settings != m_settings_panel->IsShown()) - { - wxWindowUpdateLocker locker(this); - m_settings_panel->Show(show_settings); - Layout(); - Fit(); - } -} - -void InputAPIAddWindow::on_controller_dropdown(wxCommandEvent& event) -{ - if (m_search_running) - return; - - int selection = m_input_api->GetSelection(); - if (selection == wxNOT_FOUND) - return; - - const auto type = static_cast(m_input_api->GetClientObject(selection))->get(); - auto settings = get_settings(); - - ControllerProviderPtr provider; - if (settings) - provider = InputManager::instance().get_api_provider(type, *settings); - else - provider = InputManager::instance().get_api_provider(type); - - if (!provider) - return; - - std::string selected_uuid; - selection = m_controller_list->GetSelection(); - if (selection != wxNOT_FOUND) - { - // TODO selected_uuid - } - - m_search_running = true; - - wxWindowUpdateLocker lock(m_controller_list); - m_controller_list->Clear(); - - m_controller_list->Append(_("Searching for controllers..."), (wxClientData*)nullptr); - m_controller_list->SetSelection(wxNOT_FOUND); - - m_search_thread_data = std::make_unique(); - std::thread([this, provider, selected_uuid](std::shared_ptr data) - { - auto available_controllers = provider->get_controllers(); - - { - std::lock_guard lock{data->mutex}; - if(!data->discardResult) - { - wxCommandEvent event(wxControllersRefreshed); - event.SetEventObject(m_controller_list); - event.SetClientObject(new wxCustomData(std::move(available_controllers))); - event.SetInt(provider->api()); - event.SetString(selected_uuid); - wxPostEvent(this, event); - m_search_running = false; - } - } - }, m_search_thread_data).detach(); -} - -void InputAPIAddWindow::on_controller_selected(wxCommandEvent& event) -{ - if (m_search_running) - { - return; - } - - const auto selection = m_controller_list->GetSelection(); - if (selection == wxNOT_FOUND) - { - return; - } - - if (auto* controller = (wxControllerData*)m_controller_list->GetClientObject(selection)) - { - m_controller = controller->ref(); - m_ok_button->Enable(); - } -} - -void InputAPIAddWindow::on_controllers_refreshed(wxCommandEvent& event) -{ - const auto type = event.GetInt(); - wxASSERT(0 <= type && type < InputAPI::MAX); - - auto* controllers = dynamic_cast(event.GetEventObject()); - wxASSERT(controllers); - - const auto available_controllers = static_cast>>*>(event. - GetClientObject())->get(); - const auto selected_uuid = event.GetString().ToStdString(); - bool item_selected = false; - - wxWindowUpdateLocker lock(controllers); - controllers->Clear(); - for (const auto& c : available_controllers) - { - const auto display_name = c->display_name(); - const auto uuid = c->uuid(); - const auto index = controllers->Append(display_name, new wxCustomData(c)); - if (!item_selected && selected_uuid == uuid) - { - controllers->SetSelection(index); - item_selected = true; - } - } -} - -void InputAPIAddWindow::discard_thread_result() -{ - m_search_running = false; - if(m_search_thread_data) - { - std::lock_guard lock{m_search_thread_data->mutex}; - m_search_thread_data->discardResult = true; - } -} diff --git a/src/gui/wxgui/input/InputAPIAddWindow.h b/src/gui/wxgui/input/InputAPIAddWindow.h deleted file mode 100644 index 1e17a8db..00000000 --- a/src/gui/wxgui/input/InputAPIAddWindow.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include "input/api/InputAPI.h" - -#include -#include -#include - -#include "wxgui/helpers/wxCustomData.h" -#include "input/api/Controller.h" - -class wxComboBox; -class wxChoice; -class wxTextCtrl; - -using wxAPIType = wxCustomData; - -class InputAPIAddWindow : public wxDialog -{ -public: - InputAPIAddWindow(wxWindow* parent, const wxPoint& position, const std::vector& controllers); - ~InputAPIAddWindow(); - - bool is_valid() const { return m_type.has_value() && m_controller != nullptr; } - InputAPI::Type get_type() const { return m_type.value(); } - std::shared_ptr get_controller() const { return m_controller; } - - bool has_custom_settings() const; - std::unique_ptr get_settings() const; - -private: - void on_add_button(wxCommandEvent& event); - void on_close_button(wxCommandEvent& event); - - - void on_api_selected(wxCommandEvent& event); - - void on_controller_dropdown(wxCommandEvent& event); - void on_controller_selected(wxCommandEvent& event); - void on_controllers_refreshed(wxCommandEvent& event); - - void discard_thread_result(); - - wxChoice* m_input_api; - wxComboBox* m_controller_list; - wxButton* m_ok_button; - - wxPanel* m_settings_panel; - wxTextCtrl* m_ip, * m_port; - - std::optional m_type; - std::shared_ptr m_controller; - - std::vector m_controllers; - std::atomic_bool m_search_running = false; - struct AsyncThreadData - { - std::atomic_bool discardResult = false; - std::mutex mutex; - }; - std::shared_ptr m_search_thread_data; -}; diff --git a/src/gui/wxgui/input/InputSettings2.cpp b/src/gui/wxgui/input/InputSettings2.cpp deleted file mode 100644 index d77e1430..00000000 --- a/src/gui/wxgui/input/InputSettings2.cpp +++ /dev/null @@ -1,976 +0,0 @@ -#include "wxgui/input/InputSettings2.h" - -#include - -#include "input/InputManager.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/helpers/wxControlObject.h" -#include "wxgui/helpers/wxCustomData.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "config/ActiveSettings.h" -#include "wxgui/input/InputAPIAddWindow.h" -#include "input/ControllerFactory.h" - -#ifdef HAS_BLUEZ -#include "wxgui/input/PairingDialog.h" -#endif - -#include "wxgui/input/panels/VPADInputPanel.h" -#include "wxgui/input/panels/ProControllerInputPanel.h" - -#include "wxgui/input/settings/DefaultControllerSettings.h" -#include "wxgui/input/panels/ClassicControllerInputPanel.h" -#include "wxgui/input/panels/WiimoteInputPanel.h" -#include "wxgui/input/settings/WiimoteControllerSettings.h" -#include "util/EventService.h" - -#include "resource/embedded/resources.h" - -bool g_inputConfigWindowHasFocus = false; - -using wxTypeData = wxCustomData; -using wxControllerData = wxCustomData; - -struct ControllerPage -{ - EmulatedControllerPtr m_controller; - - // profiles - wxComboBox* m_profiles; - wxButton* m_profile_load, * m_profile_save, * m_profile_delete; - wxStaticText* m_profile_status; - - // emulated controller - wxComboBox* m_emulated_controller; - - // controller api - wxComboBox* m_controllers; - wxButton* m_controller_api_add, *m_controller_api_remove; - - wxButton* m_controller_settings, * m_controller_calibrate, *m_controller_clear; - wxBitmapButton* m_controller_connected; - - // panel - std::array m_panels{}; -}; -using wxControllerPageData = wxCustomData; - - -InputSettings2::InputSettings2(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Input settings")) -{ - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - g_inputConfigWindowHasFocus = true; - - m_connected = wxHelper::LoadThemedBitmapFromPNG(INPUT_CONNECTED_png, sizeof(INPUT_CONNECTED_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_disconnected = wxHelper::LoadThemedBitmapFromPNG(INPUT_DISCONNECTED_png, sizeof(INPUT_DISCONNECTED_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - m_low_battery = wxHelper::LoadThemedBitmapFromPNG(INPUT_LOW_BATTERY_png, sizeof(INPUT_LOW_BATTERY_png), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - - m_notebook = new wxNotebook(this, wxID_ANY); - for(size_t i = 0; i < InputManager::kMaxController; ++i) - { - auto* page = new wxPanel(m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - page->SetClientObject(nullptr); // force internal type to client object - m_notebook->AddPage(page, formatWxString(_("Controller {}"), i + 1)); - } - - m_notebook->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &InputSettings2::on_controller_page_changed, this); - sizer->Add(m_notebook, 1, wxEXPAND); - - m_notebook->SetSelection(0); - auto* first_page = initialize_page(0); - - // init first/default page for fitting size - auto* page_data = (wxControllerPageData*)first_page->GetClientObject(); - auto* panel = new VPADInputPanel(first_page); - page_data->ref().m_panels[EmulatedController::Type::VPAD] = panel; - - auto* first_page_sizer = dynamic_cast(first_page->GetSizer()); - auto* panel_sizer = first_page_sizer->FindItemAtPosition(wxGBPosition(7, 0))->GetSizer(); - panel_sizer->Add(panel, 0, wxEXPAND); - - panel->Show(); - first_page->Layout(); - - SetSizer(sizer); - Layout(); - Fit(); - - panel->Hide(); - - update_state(); - - Bind(wxEVT_TIMER, &InputSettings2::on_timer, this); - - m_timer = new wxTimer(this); - m_timer->Start(25); - - m_controller_changed = EventService::instance().connect(&InputSettings2::on_controller_changed, this); -} - -InputSettings2::~InputSettings2() -{ - m_controller_changed.disconnect(); - - g_inputConfigWindowHasFocus = false; - m_timer->Stop(); - InputManager::instance().save(); -} - - -wxWindow* InputSettings2::initialize_page(size_t index) -{ - auto* page = m_notebook->GetPage(index); - if (page->GetClientObject()) // already initialized - return page; - - page->Bind(wxEVT_LEFT_UP, &InputSettings2::on_left_click, this); - - ControllerPage page_data{}; - const auto emulated_controller = InputManager::instance().get_controller(index); - page_data.m_controller = emulated_controller; - - wxWindowUpdateLocker lock(page); - auto* sizer = new wxGridBagSizer(); - - { - // profile - sizer->Add(new wxStaticText(page, wxID_ANY, _("Profile")), wxGBPosition(0, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - auto* profiles = new wxComboBox(page, wxID_ANY, kDefaultProfileName); - sizer->Add(profiles, wxGBPosition(0, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL | wxEXPAND, 5); - -#if BOOST_OS_LINUX || BOOST_OS_BSD - // We rely on the wxEVT_COMBOBOX_DROPDOWN event to trigger filling the profile list, - // but on wxGTK the dropdown button cannot be clicked if the list is empty - // so as a quick and dirty workaround we fill the list here - wxCommandEvent tmpCmdEvt; - tmpCmdEvt.SetEventObject(profiles); - on_profile_dropdown(tmpCmdEvt); -#endif - - if (emulated_controller && emulated_controller->has_profile_name()) - { - profiles->SetValue(emulated_controller->get_profile_name()); - } - - auto* load_bttn = new wxButton(page, wxID_ANY, _("Load")); - load_bttn->Disable(); - sizer->Add(load_bttn, wxGBPosition(0, 2), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* save_bttn = new wxButton(page, wxID_ANY, _("Save")); - save_bttn->Disable(); - sizer->Add(save_bttn, wxGBPosition(0, 3), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* delete_bttn = new wxButton(page, wxID_ANY, _("Delete")); - delete_bttn->Disable(); - sizer->Add(delete_bttn, wxGBPosition(0, 4), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* profile_status = new wxStaticText(page, wxID_ANY, _("controller set by gameprofile. changes won't be saved permanently!")); - profile_status->SetMinSize(wxSize(200, -1)); - profile_status->Wrap(200); - sizer->Add(profile_status, wxGBPosition(0, 5), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL | wxRESERVE_SPACE_EVEN_IF_HIDDEN, 5); - - if(InputManager::instance().is_gameprofile_set(index)) - { - profile_status->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - } - else - { - profile_status->Hide(); - } - - load_bttn->Bind(wxEVT_BUTTON, &InputSettings2::on_profile_load, this); - save_bttn->Bind(wxEVT_BUTTON, &InputSettings2::on_profile_save, this); - delete_bttn->Bind(wxEVT_BUTTON, &InputSettings2::on_profile_delete, this); - - profiles->Bind(wxEVT_COMBOBOX_DROPDOWN, &InputSettings2::on_profile_dropdown, this); - profiles->Bind(wxEVT_TEXT, &InputSettings2::on_profile_text_changed, this); - - page_data.m_profiles = profiles; - page_data.m_profile_load = load_bttn; - page_data.m_profile_save = save_bttn; - page_data.m_profile_delete = delete_bttn; - page_data.m_profile_status = profile_status; - } - - sizer->Add(new wxStaticLine(page), wxGBPosition(1, 0), wxGBSpan(1, 6), wxEXPAND); - - { - // emulated controller - sizer->Add(new wxStaticText(page, wxID_ANY, _("Emulated controller")), wxGBPosition(2, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* econtroller_box = new wxComboBox(page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - econtroller_box->SetMinSize(wxSize(200, -1)); - econtroller_box->Bind(wxEVT_COMBOBOX_DROPDOWN, &InputSettings2::on_emulated_controller_dropdown, this); - econtroller_box->Bind(wxEVT_COMBOBOX, &InputSettings2::on_emulated_controller_selected, this); - - econtroller_box->AppendString(_("Disabled")); - econtroller_box->SetSelection(0); - - sizer->Add(econtroller_box, wxGBPosition(2, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL | wxEXPAND, 5); - page_data.m_emulated_controller = econtroller_box; - } - - sizer->Add(new wxStaticLine(page), wxGBPosition(3, 0), wxGBSpan(1, 6), wxEXPAND); - - { - // controller api - sizer->Add(new wxStaticText(page, wxID_ANY, _("Controller")), wxGBPosition(4, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); - - auto* controllers = new wxComboBox(page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - controllers->Bind(wxEVT_COMBOBOX, &InputSettings2::on_controller_selected, this); - controllers->Bind(wxEVT_COMBOBOX_DROPDOWN, &InputSettings2::on_controller_dropdown, this); - controllers->SetMinSize(wxSize(300, -1)); - - page_data.m_controllers = controllers; - - sizer->Add(controllers, wxGBPosition(4, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL | wxEXPAND, 5); - - { - // add/remove buttons - auto* bttn_sizer = new wxBoxSizer(wxHORIZONTAL); - - auto* add_api = new wxButton(page, wxID_ANY, " + ", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - add_api->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_add, this); - bttn_sizer->Add(add_api, 0, wxALL, 5); - - auto* remove_api = new wxButton(page, wxID_ANY, " - ", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); - remove_api->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_remove, this); - bttn_sizer->Add(remove_api, 0, wxALL, 5); - - sizer->Add(bttn_sizer, wxGBPosition(4, 2), wxDefaultSpan, wxEXPAND, 5); - - page_data.m_controller_api_add = add_api; - page_data.m_controller_api_remove = remove_api; - } - -#ifdef HAS_BLUEZ - auto* pairingDialog = new wxButton(page, wxID_ANY, _("Pair Wii/Wii U Controller")); - pairingDialog->Bind(wxEVT_BUTTON, [this](wxEvent&) { - PairingDialog pairing_dialog(this); - pairing_dialog.ShowModal(); - }); - sizer->Add(pairingDialog, wxGBPosition(5, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); -#endif - - // controller - auto* controller_bttns = new wxBoxSizer(wxHORIZONTAL); - auto* settings = new wxButton(page, wxID_ANY, _("Settings")); - settings->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_settings, this); - settings->Disable(); - controller_bttns->Add(settings, 0, wxALL, 5); - - auto* calibrate = new wxButton(page, wxID_ANY, _("Calibrate")); - calibrate->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_calibrate, this); - calibrate->Disable(); - controller_bttns->Add(calibrate, 0, wxALL, 5); - - auto* clear = new wxButton(page, wxID_ANY, _("Clear")); - clear->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_clear, this); - controller_bttns->Add(clear, 0, wxALL, 5); - - sizer->Add(controller_bttns, wxGBPosition(5, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - - auto* connected_button = new wxBitmapButton(page, wxID_ANY, m_disconnected); - connected_button->Bind(wxEVT_BUTTON, &InputSettings2::on_controller_connect, this); - connected_button->SetToolTip(_("Test if the controller is connected")); - sizer->Add(connected_button, wxGBPosition(5, 2), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); // TODO replace with icon - - page_data.m_controller_settings = settings; - page_data.m_controller_calibrate = calibrate; - page_data.m_controller_clear = clear; - page_data.m_controller_connected = connected_button; - - } - - sizer->Add(new wxStaticLine(page), wxGBPosition(6, 0), wxGBSpan(1, 6), wxEXPAND); - - auto* panel_sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(panel_sizer, wxGBPosition(7, 0), wxGBSpan(1, 6), wxEXPAND | wxALL, 5); - - page->SetSizer(sizer); - page->Layout(); - - page->SetClientObject(new wxCustomData(page_data)); - - return page; -} - -std::shared_ptr InputSettings2::get_active_controller() const -{ - auto& page_data = get_current_page_data(); - - const auto selection = page_data.m_controllers->GetSelection(); - if(selection != wxNOT_FOUND) - { - if(auto* controller = (wxControllerData*)page_data.m_controllers->GetClientObject(selection)) - return controller->ref(); - } - - return {}; -} - -bool InputSettings2::has_settings(InputAPI::Type type) -{ - switch(type) - { - case InputAPI::Keyboard: - return false; - default: - return true; - } -} - -void InputSettings2::update_state() -{ - auto* page = m_notebook->GetCurrentPage(); - wxWindowUpdateLocker lock(page); - - auto* page_data_ptr = (wxControllerPageData*)page->GetClientObject(); - wxASSERT(page_data_ptr); - auto& page_data = page_data_ptr->ref(); - - page_data.m_profile_status->Hide(); - - EmulatedControllerPtr emulated_controller = page_data.m_controller; - auto has_controllers = false; - - // update emulated - if(emulated_controller) - { - has_controllers = !emulated_controller->get_controllers().empty(); - - const auto emulated_type = emulated_controller->type(); - int index = page_data.m_emulated_controller->Append(wxString::FromUTF8(emulated_controller->type_to_string(emulated_type))); - page_data.m_emulated_controller->SetSelection(index); - - const auto controller_selection = page_data.m_controllers->GetStringSelection(); - page_data.m_controllers->Clear(); - if (has_controllers) - { - for (const auto& c : emulated_controller->get_controllers()) - { - page_data.m_controllers->Append(fmt::format("{} [{}]", c->display_name(), c->api_name()), new wxCustomData(c)); - } - - if (page_data.m_controllers->GetCount() > 0) - { - page_data.m_controllers->SetSelection(0); - if (!controller_selection.empty()) - page_data.m_controllers->SetStringSelection(controller_selection); - } - } - } - else - { - page_data.m_emulated_controller->SetValue(_("Disabled")); - } - - ControllerPtr controller; - if (page_data.m_controllers->GetSelection() != wxNOT_FOUND) - { - if (const auto data = (wxControllerData*)page_data.m_controllers->GetClientObject(page_data.m_controllers->GetSelection())) - controller = data->ref(); - } - - if (controller && controller->is_connected()) - page_data.m_controller_connected->SetBitmap(m_connected); - else - page_data.m_controller_connected->SetBitmap(m_disconnected); - - // update controller - page_data.m_controller_calibrate->Enable(has_controllers); - page_data.m_controller_api_remove->Enable(has_controllers); - page_data.m_controller_settings->Enable(controller && has_settings(controller->api())); - - // update settings - // update panel - // test if we need to update to correct panel - std::optional active_api{}; - for(auto i = 0; i < EmulatedController::Type::MAX; ++i) - { - if(page_data.m_panels[i] && page_data.m_panels[i]->IsShown()) - { - active_api = (EmulatedController::Type)i; - break; - } - } - - // disabled and no emulated controller - if (!active_api && !emulated_controller) - return; - - // enabled correct panel for active controller - if (active_api && emulated_controller && emulated_controller->type() == active_api.value()) - { - // same controller type panel already shown, refresh content of panels - for (auto* panel : page_data.m_panels) - { - if (panel) - panel->load_controller(page_data.m_controller); - } - return; - } - - // hide all panels - for (auto* panel : page_data.m_panels) - { - if (panel) - panel->Hide(); - } - - // show required panel - if (emulated_controller) - { - auto* sizer = dynamic_cast(page->GetSizer()); - wxASSERT(sizer); - - const auto type = page_data.m_controller->type(); - InputPanel* panel = page_data.m_panels[type]; - if (!panel) - { - switch (type) - { - case EmulatedController::Type::VPAD: - panel = new VPADInputPanel(page); - break; - case EmulatedController::Pro: - panel = new ProControllerInputPanel(page); - break; - case EmulatedController::Classic: - panel = new ClassicControllerInputPanel(page); - break; - case EmulatedController::Wiimote: - panel = new WiimoteInputPanel(page); - break; - default: - cemu_assert_debug(false); - return; - } - - page_data.m_panels[type] = panel; - - auto* panel_sizer = sizer->FindItemAtPosition(wxGBPosition(7, 0))->GetSizer(); - wxASSERT(panel_sizer); - panel_sizer->Add(panel, 0, wxEXPAND); - } - - panel->load_controller(page_data.m_controller); - if (has_controllers) - panel->set_selected_controller(emulated_controller, emulated_controller->get_controllers()[0]); - - panel->Show(); - page->wxWindowBase::Layout(); - page->wxWindow::Update(); - } -} - -void InputSettings2::on_controller_changed() -{ - for(auto i = 0 ; i < m_notebook->GetPageCount(); ++i) - { - auto* page = m_notebook->GetPage(i); - if (!page) - continue; - - auto* page_data_ptr = (wxControllerPageData*)page->GetClientObject(); - if (!page_data_ptr) - continue; - - const auto& page_data = page_data_ptr->ref(); - if (page_data.m_controllers->GetSelection() != wxNOT_FOUND) - { - if (const auto data = (wxControllerData*)page_data.m_controllers->GetClientObject(page_data.m_controllers->GetSelection())) - { - if (const auto controller = data->ref()) - { - if (controller->connect()) - page_data.m_controller_connected->SetBitmap(m_connected); - else - page_data.m_controller_connected->SetBitmap(m_disconnected); - } - } - } - } -} - -void InputSettings2::on_notebook_page_changed(wxBookCtrlEvent& event) -{ - initialize_page(event.GetSelection()); - update_state(); - event.Skip(); -} - -void InputSettings2::on_timer(wxTimerEvent&) -{ - auto& page_data = get_current_page_data(); - if (!page_data.m_controller) { - return; - } - - auto* panel = page_data.m_panels[page_data.m_controller->type()]; - if (!panel) - return; - - auto controller = get_active_controller(); - if (controller) { - panel->on_timer(page_data.m_controller, controller); - } -} - -void InputSettings2::on_left_click(wxMouseEvent& event) -{ - event.Skip(); - - auto& page_data = get_current_page_data(); - if (!page_data.m_controller) { - return; - } - - auto* panel = page_data.m_panels[page_data.m_controller->type()]; - if (!panel) - return; - - panel->on_left_click(event); -} - -void InputSettings2::on_profile_dropdown(wxCommandEvent& event) -{ - auto* profile_names = dynamic_cast(event.GetEventObject()); - wxASSERT(profile_names); - wxWindowUpdateLocker lock(profile_names); - - const auto selected_value = profile_names->GetStringSelection(); - profile_names->Clear(); - - for(const auto& profile : InputManager::get_profiles()) - { - profile_names->Append(wxString::FromUTF8(profile)); - } - - profile_names->SetStringSelection(selected_value); -} - -void InputSettings2::on_profile_text_changed(wxCommandEvent& event) -{ - auto* profile_names = dynamic_cast(event.GetEventObject()); - wxASSERT(profile_names); - - auto& page_data = get_current_page_data(); - - // load_bttn, save_bttn, delete_bttn, profile_status - const auto text = event.GetString(); - const bool valid_name = InputManager::is_valid_profilename(text.utf8_string()); - const bool name_exists = profile_names->FindString(text) != wxNOT_FOUND; - - page_data.m_profile_load->Enable(name_exists); - page_data.m_profile_save->Enable(valid_name); - page_data.m_profile_delete->Enable(name_exists); - page_data.m_profile_status->Hide(); -} - -void InputSettings2::on_profile_load(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - auto* profile_names = page_data.m_profiles; - auto* text = page_data.m_profile_status; - - const auto selection = profile_names->GetValue().utf8_string(); - text->Show(); - if (selection.empty() || !InputManager::is_valid_profilename(selection)) - { - text->SetLabelText(_("invalid profile name")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - text->Refresh(); - return; - } - - const auto page_index = m_notebook->GetSelection(); - if (InputManager::instance().load(page_index, selection)) - { - text->SetLabelText(_("profile loaded")); - text->SetForegroundColour(wxTheColourDatabase->Find("SUCCESS")); - } - else - { - text->SetLabelText(_("couldn't load profile")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - } - - text->Refresh(); - - // update controller info - page_data.m_controller = InputManager::instance().get_controller(page_index); - update_state(); -} - -void InputSettings2::on_profile_save(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - auto* profile_names = page_data.m_profiles; - auto* text = page_data.m_profile_status; - - const auto selection = profile_names->GetValue().utf8_string(); - text->Show(); - if (selection.empty() || !InputManager::is_valid_profilename(selection)) - { - text->SetLabelText(_("invalid profile name")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - text->Refresh(); - return; - } - - if(InputManager::instance().save(m_notebook->GetSelection(), selection)) - { - text->SetLabelText(_("profile saved")); - text->SetForegroundColour(wxTheColourDatabase->Find("SUCCESS")); - } - else - { - text->SetLabelText(_("couldn't save profile")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - } - - text->Refresh(); -} - -void InputSettings2::on_profile_delete(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - auto* profile_names = page_data.m_profiles; - auto* text = page_data.m_profile_status; - - const auto selection = profile_names->GetStringSelection().utf8_string(); - - text->Show(); - if (selection.empty() || !InputManager::is_valid_profilename(selection)) - { - text->SetLabelText(_("invalid profile name")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - text->Refresh(); - return; - } - try - { - const fs::path old_path = ActiveSettings::GetConfigPath("controllerProfiles/{}.txt", selection); - fs::remove(old_path); - - const fs::path path = ActiveSettings::GetConfigPath("controllerProfiles/{}.xml", selection); - fs::remove(path); - - profile_names->ChangeValue(kDefaultProfileName); - text->SetLabelText(_("profile deleted")); - text->SetForegroundColour(wxTheColourDatabase->Find("SUCCESS")); - - page_data.m_profile_load->Disable(); - page_data.m_profile_save->Disable(); - page_data.m_profile_delete->Disable(); - } - catch (const std::exception&) - { - text->SetLabelText(_("can't delete profile")); - text->SetForegroundColour(wxTheColourDatabase->Find("ERROR")); - } - text->Refresh(); -} - - -void InputSettings2::on_controller_page_changed(wxBookCtrlEvent& event) -{ - initialize_page(event.GetSelection()); - update_state(); - event.Skip(); -} - -void InputSettings2::on_emulated_controller_selected(wxCommandEvent& event) -{ - const auto page_index = m_notebook->GetSelection(); - auto& page_data = get_current_page_data(); - - const auto selection = event.GetSelection(); - if(selection == 0) // disabled selected - { - page_data.m_controller = {}; - InputManager::instance().delete_controller(page_index, true); - } - else - { - try - { - const auto type = EmulatedController::type_from_string(event.GetString().utf8_string()); - // same has already been selected - if (page_data.m_controller && page_data.m_controller->type() == type) - return; - - // set new controller - const auto new_controller = InputManager::instance().set_controller(page_index, type); - page_data.m_controller = new_controller; - - // append controllers if some were already added before - if (new_controller->get_controllers().empty()) - { - // test if we had no emulated controller before but still assigned controllers we want to transfer now - for (uint32 i = 0; i < page_data.m_controllers->GetCount(); ++i) - { - if (auto* controller = (wxControllerData*)page_data.m_controllers->GetClientObject(i)) - { - new_controller->add_controller(controller->ref()); - } - } - } - - // set default mappings if any controllers available - for(const auto& c: new_controller->get_controllers()) - { - new_controller->set_default_mapping(c); - } - } - catch (const std::exception&) - { - cemu_assert_debug(false); - } - - } - - update_state(); -} - -void InputSettings2::on_emulated_controller_dropdown(wxCommandEvent& event) -{ - auto* emulated_controllers = dynamic_cast(event.GetEventObject()); - wxASSERT(emulated_controllers); - - wxWindowUpdateLocker lock(emulated_controllers); - - bool is_gamepad_selected = false; - bool is_wpad_selected = false; - const auto selected = emulated_controllers->GetSelection(); - const auto selected_value = emulated_controllers->GetStringSelection(); - if(selected != wxNOT_FOUND) - { - is_gamepad_selected = selected_value == wxString::FromUTF8(EmulatedController::type_to_string(EmulatedController::Type::VPAD)); - is_wpad_selected = !is_gamepad_selected && selected != 0; - } - - const auto [vpad_count, wpad_count] = InputManager::instance().get_controller_count(); - - emulated_controllers->Clear(); - emulated_controllers->AppendString(_("Disabled")); - - if (vpad_count < InputManager::kMaxVPADControllers || is_gamepad_selected) - emulated_controllers->Append(wxString::FromUTF8(EmulatedController::type_to_string(EmulatedController::Type::VPAD))); - - if (wpad_count < InputManager::kMaxWPADControllers || is_wpad_selected) - { - emulated_controllers->AppendString(wxString::FromUTF8(EmulatedController::type_to_string(EmulatedController::Type::Pro))); - emulated_controllers->AppendString(wxString::FromUTF8(EmulatedController::type_to_string(EmulatedController::Type::Classic))); - emulated_controllers->AppendString(wxString::FromUTF8(EmulatedController::type_to_string(EmulatedController::Type::Wiimote))); - } - - emulated_controllers->SetStringSelection(selected_value); -} - -void InputSettings2::on_controller_selected(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - const auto enabled = event.GetSelection() != wxNOT_FOUND; - page_data.m_controller_api_remove->Enable(enabled); - // page_data->ref().m_controller_list->Clear(); - if(enabled) - { - // get selected controller if any todo - if (auto* controller = (wxControllerData*)page_data.m_controllers->GetClientObject(event.GetSelection())) - { - page_data.m_controller_settings->Enable(has_settings(controller->ref()->api())); - - if(page_data.m_controller) - { - page_data.m_panels[page_data.m_controller->type()]->set_selected_controller(page_data.m_controller, controller->ref()); - } - } - - } -} - -void InputSettings2::on_controller_dropdown(wxCommandEvent& event) -{ - if(auto* controllers = dynamic_cast(event.GetEventObject())) - { - if(controllers->GetCount()== 0) - { - on_controller_add(event); - controllers->SetSelection(0); - } - } -} - -ControllerPage& InputSettings2::get_current_page_data() const -{ - auto* page = m_notebook->GetCurrentPage(); - auto* page_data_ptr = (wxControllerPageData*)page->GetClientObject(); - wxASSERT(page_data_ptr); - return page_data_ptr->ref(); -} - -void InputSettings2::on_controller_connect(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - if (page_data.m_controllers->GetSelection() != wxNOT_FOUND) - { - if (const auto data = (wxControllerData*)page_data.m_controllers->GetClientObject(page_data.m_controllers->GetSelection())) - { - if(const auto controller = data->ref()) - { - if(controller->connect()) - page_data.m_controller_connected->SetBitmap(m_connected); - else - page_data.m_controller_connected->SetBitmap(m_disconnected); - } - } - } -} - -void InputSettings2::on_controller_add(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - std::vector controllers; - controllers.reserve(page_data.m_controllers->GetCount()); - for(uint32 i = 0; i < page_data.m_controllers->GetCount(); ++i) - { - if (auto* controller = (wxControllerData*)page_data.m_controllers->GetClientObject(i)) - controllers.emplace_back(controller->ref()); - } - - InputAPIAddWindow wnd(this, wxGetMousePosition() + wxSize(5, 5), controllers); - if (wnd.ShowModal() != wxID_OK) - return; - - wxASSERT(wnd.is_valid()); - - const auto controller = wnd.get_controller(); - - const auto api_type = wnd.get_type(); - controller->connect(); - const int index = page_data.m_controllers->Append(fmt::format("{} [{}]", controller->display_name(), to_string(api_type)), new wxCustomData(controller)); - - page_data.m_controllers->Select(index); - - if(page_data.m_controller) - { - page_data.m_controller->add_controller(controller); - - const auto type = page_data.m_controller->type(); - // if first controller and we got no mappings, add default mappings - if(page_data.m_controller->set_default_mapping(controller)) - page_data.m_panels[type]->load_controller(page_data.m_controller); - - page_data.m_panels[type]->set_selected_controller(page_data.m_controller, controller); - } - - update_state(); -} - -void InputSettings2::on_controller_remove(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - - auto* api_box = page_data.m_controllers; - int selection = api_box->GetSelection(); - if (selection == wxNOT_FOUND) - return; - - if (page_data.m_controller) { - if (auto* controller = (wxControllerData*)page_data.m_controllers->GetClientObject(selection)) - { - page_data.m_controller->remove_controller(controller->ref()); - page_data.m_panels[page_data.m_controller->type()]->load_controller(page_data.m_controller); - } - } - - page_data.m_panels[page_data.m_controller->type()]->reset_colours(); - - api_box->Delete(selection); - api_box->Refresh(); - - update_state(); - - if (api_box->GetCount() > 0) - { - selection = selection > 0 ? (selection - 1) : 0; - api_box->SetSelection(selection); - } - - update_state(); -} - -void InputSettings2::on_controller_calibrate(wxCommandEvent& event) -{ - if(const auto controller = get_active_controller()) - controller->calibrate(); -} - -void InputSettings2::on_controller_clear(wxCommandEvent& event) -{ - auto& page_data = get_current_page_data(); - if (page_data.m_controller) { - const auto type = page_data.m_controller->type(); - - page_data.m_panels[type]->reset_configuration(); - page_data.m_controller->clear_mappings(); - } -} - -void InputSettings2::on_controller_settings(wxCommandEvent& event) -{ - auto controller = get_active_controller(); - if (!controller) - return; - - switch(controller->api()) - { - - case InputAPI::DirectInput: - case InputAPI::XInput: - case InputAPI::GameCube: - case InputAPI::WGIGamepad: - case InputAPI::WGIRawController: - case InputAPI::SDLController: - case InputAPI::DSUClient: - { - DefaultControllerSettings wnd(this, wxGetMousePosition() + wxSize(5, 5), controller); - wnd.ShowModal(); - break; - } - - case InputAPI::Keyboard: break; - - #ifdef SUPPORTS_WIIMOTE - case InputAPI::Wiimote: { - const auto wiimote = std::dynamic_pointer_cast(controller); - wxASSERT(wiimote); - WiimoteControllerSettings wnd(this, wxGetMousePosition() + wxSize(5, 5), wiimote); - wnd.ShowModal(); - break; - } - #endif - } -} diff --git a/src/gui/wxgui/input/InputSettings2.h b/src/gui/wxgui/input/InputSettings2.h deleted file mode 100644 index 1a3c8bb1..00000000 --- a/src/gui/wxgui/input/InputSettings2.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "input/api/InputAPI.h" - -#include - -struct ControllerPage; -class ControllerBase; - -class InputSettings2 : public wxDialog -{ -public: - InputSettings2(wxWindow* parent); - ~InputSettings2(); - -private: - const wxString kDefaultProfileName = _(""); - - wxNotebook* m_notebook; - wxTimer* m_timer; - - wxBitmap m_connected, m_disconnected, m_low_battery; - - wxWindow* initialize_page(size_t index); - - // currently selected controller from active tab - std::shared_ptr get_active_controller() const; - - bool has_settings(InputAPI::Type type); - ControllerPage& get_current_page_data() const; - void update_state(); - - boost::signals2::connection m_controller_changed; - void on_controller_changed(); - - // events - void on_notebook_page_changed(wxBookCtrlEvent& event); - void on_timer(wxTimerEvent& event); - - void on_left_click(wxMouseEvent& event); - - void on_controller_page_changed(wxBookCtrlEvent& event); - - void on_profile_dropdown(wxCommandEvent& event); - void on_profile_text_changed(wxCommandEvent& event); - - void on_profile_load(wxCommandEvent& event); - void on_profile_save(wxCommandEvent& event); - void on_profile_delete(wxCommandEvent& event); - - void on_emulated_controller_selected(wxCommandEvent& event); - void on_emulated_controller_dropdown(wxCommandEvent& event); - - void on_controller_selected(wxCommandEvent& event); - void on_controller_dropdown(wxCommandEvent& event); - void on_controller_connect(wxCommandEvent& event); - - void on_controller_add(wxCommandEvent& event); - void on_controller_remove(wxCommandEvent& event); - - void on_controller_calibrate(wxCommandEvent& event); - void on_controller_clear(wxCommandEvent& event); - void on_controller_settings(wxCommandEvent& event); - - // void on_controller_dropdown(wxCommandEvent& event); - // void on_controllers_refreshed(wxCommandEvent& event); - -}; - diff --git a/src/gui/wxgui/input/PairingDialog.cpp b/src/gui/wxgui/input/PairingDialog.cpp deleted file mode 100644 index 9bf2870f..00000000 --- a/src/gui/wxgui/input/PairingDialog.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "wxgui/wxgui.h" -#include "PairingDialog.h" - -#if BOOST_OS_WINDOWS -#include -#endif -#if BOOST_OS_LINUX -#include -#include -#include -#include -#endif - -wxDECLARE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); - -PairingDialog::PairingDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Pairing..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(350, 20), wxGA_HORIZONTAL); - m_gauge->SetValue(0); - sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); - - auto* rows = new wxFlexGridSizer(0, 2, 0, 0); - rows->AddGrowableCol(1); - - m_text = new wxStaticText(this, wxID_ANY, _("Searching for controllers...")); - rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - { - auto* right_side = new wxBoxSizer(wxHORIZONTAL); - - m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &PairingDialog::OnCancelButton, this); - right_side->Add(m_cancelButton, 0, wxALL, 5); - - rows->Add(right_side, 1, wxALIGN_RIGHT, 5); - } - - sizer->Add(rows, 0, wxALL | wxEXPAND, 5); - - SetSizerAndFit(sizer); - Centre(wxBOTH); - - Bind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); - Bind(wxEVT_PROGRESS_PAIR, &PairingDialog::OnGaugeUpdate, this); - - m_thread = std::thread(&PairingDialog::WorkerThread, this); -} - -PairingDialog::~PairingDialog() -{ - Unbind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); -} - -void PairingDialog::OnClose(wxCloseEvent& event) -{ - event.Skip(); - - m_threadShouldQuit = true; - if (m_thread.joinable()) - m_thread.join(); -} - -void PairingDialog::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} - -void PairingDialog::OnGaugeUpdate(wxCommandEvent& event) -{ - PairingState state = (PairingState)event.GetInt(); - - switch (state) - { - case PairingState::Pairing: - { - m_text->SetLabel(_("Found controller. Pairing...")); - m_gauge->SetValue(50); - break; - } - - case PairingState::Finished: - { - m_text->SetLabel(_("Successfully paired the controller.")); - m_gauge->SetValue(100); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::NoBluetoothAvailable: - { - m_text->SetLabel(_("Failed to find a suitable Bluetooth radio.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::SearchFailed: - { - m_text->SetLabel(_("Failed to find controllers.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::PairingFailed: - { - m_text->SetLabel(_("Failed to pair with the found controller.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::BluetoothUnusable: - { - m_text->SetLabel(_("Please use your system's Bluetooth manager instead.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - default: - { - break; - } - } -} - -#if BOOST_OS_WINDOWS -void PairingDialog::WorkerThread() -{ - const std::wstring wiimoteName = L"Nintendo RVL-CNT-01"; - const std::wstring wiiUProControllerName = L"Nintendo RVL-CNT-01-UC"; - - const GUID bthHidGuid = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}}; - - const BLUETOOTH_FIND_RADIO_PARAMS radioFindParams = - { - .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS)}; - - HANDLE radio = INVALID_HANDLE_VALUE; - HBLUETOOTH_RADIO_FIND radioFind = BluetoothFindFirstRadio(&radioFindParams, &radio); - if (radioFind == nullptr) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } - - BluetoothFindRadioClose(radioFind); - - BLUETOOTH_RADIO_INFO radioInfo = - { - .dwSize = sizeof(BLUETOOTH_RADIO_INFO)}; - - DWORD result = BluetoothGetRadioInfo(radio, &radioInfo); - if (result != ERROR_SUCCESS) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } - - const BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS), - - .fReturnAuthenticated = FALSE, - .fReturnRemembered = FALSE, - .fReturnUnknown = TRUE, - .fReturnConnected = FALSE, - - .fIssueInquiry = TRUE, - .cTimeoutMultiplier = 5, - - .hRadio = radio}; - - BLUETOOTH_DEVICE_INFO info = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_INFO)}; - - while (!m_threadShouldQuit) - { - HBLUETOOTH_DEVICE_FIND deviceFind = BluetoothFindFirstDevice(&searchParams, &info); - if (deviceFind == nullptr) - { - UpdateCallback(PairingState::SearchFailed); - return; - } - - while (!m_threadShouldQuit) - { - if (info.szName == wiimoteName || info.szName == wiiUProControllerName) - { - BluetoothFindDeviceClose(deviceFind); - - UpdateCallback(PairingState::Pairing); - - wchar_t passwd[6] = {radioInfo.address.rgBytes[0], radioInfo.address.rgBytes[1], radioInfo.address.rgBytes[2], radioInfo.address.rgBytes[3], radioInfo.address.rgBytes[4], radioInfo.address.rgBytes[5]}; - DWORD bthResult = BluetoothAuthenticateDevice(nullptr, radio, &info, passwd, 6); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } - - bthResult = BluetoothSetServiceState(radio, &info, &bthHidGuid, BLUETOOTH_SERVICE_ENABLE); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } - - UpdateCallback(PairingState::Finished); - return; - } - - BOOL nextDevResult = BluetoothFindNextDevice(deviceFind, &info); - if (nextDevResult == FALSE) - { - break; - } - } - - BluetoothFindDeviceClose(deviceFind); - } -} -#elif defined(HAS_BLUEZ) -void PairingDialog::WorkerThread() -{ - constexpr static uint8_t LIAC_LAP[] = {0x00, 0x8b, 0x9e}; - - constexpr static auto isWiimoteName = [](std::string_view name) { - return name == "Nintendo RVL-CNT-01" || name == "Nintendo RVL-CNT-01-TR"; - }; - - // Get default BT device - const auto hostId = hci_get_route(nullptr); - if (hostId < 0) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } - - // Search for device - inquiry_info* infos = nullptr; - m_cancelButton->Disable(); - const auto respCount = hci_inquiry(hostId, 7, 4, LIAC_LAP, &infos, IREQ_CACHE_FLUSH); - m_cancelButton->Enable(); - if (respCount <= 0) - { - UpdateCallback(PairingState::SearchFailed); - return; - } - stdx::scope_exit infoFree([&]() { bt_free(infos);}); - - if (m_threadShouldQuit) - return; - - // Open dev to read name - const auto hostDev = hci_open_dev(hostId); - stdx::scope_exit devClose([&]() { hci_close_dev(hostDev);}); - - char nameBuffer[HCI_MAX_NAME_LENGTH] = {}; - - bool foundADevice = false; - // Get device name and compare. Would use product and vendor id from SDP, but many third-party Wiimotes don't store them - for (const auto& devInfo : std::span(infos, respCount)) - { - const auto& addr = devInfo.bdaddr; - const auto err = hci_read_remote_name(hostDev, &addr, HCI_MAX_NAME_LENGTH, nameBuffer, - 2000); - if (m_threadShouldQuit) - return; - if (err || !isWiimoteName(nameBuffer)) - continue; - - L2CapWiimote::AddCandidateAddress(addr); - foundADevice = true; - const auto& b = addr.b; - cemuLog_log(LogType::Force, "Pairing Dialog: Found '{}' with address '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'", - nameBuffer, b[5], b[4], b[3], b[2], b[1], b[0]); - } - if (foundADevice) - UpdateCallback(PairingState::Finished); - else - UpdateCallback(PairingState::SearchFailed); -} -#else -void PairingDialog::WorkerThread() -{ - UpdateCallback(PairingState::BluetoothUnusable); -} -#endif -void PairingDialog::UpdateCallback(PairingState state) -{ - auto* event = new wxCommandEvent(wxEVT_PROGRESS_PAIR); - event->SetInt((int)state); - wxQueueEvent(this, event); -} \ No newline at end of file diff --git a/src/gui/wxgui/input/PairingDialog.h b/src/gui/wxgui/input/PairingDialog.h deleted file mode 100644 index 02cab4fc..00000000 --- a/src/gui/wxgui/input/PairingDialog.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class PairingDialog : public wxDialog -{ -public: - PairingDialog(wxWindow* parent); - ~PairingDialog(); - -private: - enum class PairingState - { - Pairing, - Finished, - NoBluetoothAvailable, - SearchFailed, - PairingFailed, - BluetoothUnusable - }; - - void OnClose(wxCloseEvent& event); - void OnCancelButton(const wxCommandEvent& event); - void OnGaugeUpdate(wxCommandEvent& event); - - void WorkerThread(); - void UpdateCallback(PairingState state); - - wxStaticText* m_text; - wxGauge* m_gauge; - wxButton* m_cancelButton; - - std::thread m_thread; - bool m_threadShouldQuit = false; -}; diff --git a/src/gui/wxgui/input/panels/ClassicControllerInputPanel.cpp b/src/gui/wxgui/input/panels/ClassicControllerInputPanel.cpp deleted file mode 100644 index b1b5205c..00000000 --- a/src/gui/wxgui/input/panels/ClassicControllerInputPanel.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "wxgui/input/panels/ClassicControllerInputPanel.h" - -#include -#include -#include -#include -#include - -#include "wxgui/helpers/wxControlObject.h" -#include "input/emulated/ClassicController.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" - -constexpr ClassicController::ButtonId g_kFirstColumnItems[] = { ClassicController::kButtonId_A, ClassicController::kButtonId_B, ClassicController::kButtonId_X, ClassicController::kButtonId_Y, ClassicController::kButtonId_L, ClassicController::kButtonId_R, ClassicController::kButtonId_ZL, ClassicController::kButtonId_ZR, ClassicController::kButtonId_Plus, ClassicController::kButtonId_Minus }; -constexpr ClassicController::ButtonId g_kSecondColumnItems[] = { ClassicController::kButtonId_StickL_Up, ClassicController::kButtonId_StickL_Down, ClassicController::kButtonId_StickL_Left, ClassicController::kButtonId_StickL_Right }; -constexpr ClassicController::ButtonId g_kThirdColumnItems[] = { ClassicController::kButtonId_StickR_Up, ClassicController::kButtonId_StickR_Down, ClassicController::kButtonId_StickR_Left, ClassicController::kButtonId_StickR_Right }; -constexpr ClassicController::ButtonId g_kFourthRowItems[] = { ClassicController::kButtonId_Up, ClassicController::kButtonId_Down, ClassicController::kButtonId_Left, ClassicController::kButtonId_Right }; - - -ClassicControllerInputPanel::ClassicControllerInputPanel(wxWindow* parent) - : InputPanel(parent) -{ - auto bold_font = GetFont(); - bold_font.MakeBold(); - - auto* main_sizer = new wxGridBagSizer(); - sint32 row = 0; - sint32 column = 0; - for (const auto& id : g_kFirstColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - ////////////////////////////////////////////////////////////////// - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 2), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - row = 0; - column += 3; - - auto text = new wxStaticText(this, wxID_ANY, _("Left Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kSecondColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_left_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_left_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("Right Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kThirdColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_right_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_right_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("D-pad")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (auto id : g_kFourthRowItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - ////////////////////////////////////////////////////////////////// - - SetSizer(main_sizer); - Layout(); -} - -void ClassicControllerInputPanel::add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const ClassicController::ButtonId &button_id) { - sizer->Add( - new wxStaticText(this, wxID_ANY, wxGetTranslation(wxString::FromUTF8(ClassicController::get_button_name(button_id)))), - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - - auto* text_ctrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB); - text_ctrl->SetClientData(reinterpret_cast(button_id)); - text_ctrl->SetMinSize(wxSize(150, -1)); - text_ctrl->SetEditable(false); - text_ctrl->SetBackgroundColour(kKeyColourNormalMode); - bind_hotkey_events(text_ctrl); - sizer->Add(text_ctrl, wxGBPosition(row, column + 1), wxDefaultSpan, wxALL | wxEXPAND, 5); -} diff --git a/src/gui/wxgui/input/panels/ClassicControllerInputPanel.h b/src/gui/wxgui/input/panels/ClassicControllerInputPanel.h deleted file mode 100644 index 8be8c52e..00000000 --- a/src/gui/wxgui/input/panels/ClassicControllerInputPanel.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include "input/emulated/ClassicController.h" -#include "wxgui/input/panels/InputPanel.h" - -class wxInputDraw; - -class ClassicControllerInputPanel : public InputPanel -{ -public: - ClassicControllerInputPanel(wxWindow* parent); - -private: - wxInputDraw* m_left_draw, * m_right_draw; - - void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const ClassicController::ButtonId &button_id); -}; - diff --git a/src/gui/wxgui/input/panels/InputPanel.cpp b/src/gui/wxgui/input/panels/InputPanel.cpp deleted file mode 100644 index e460f0c6..00000000 --- a/src/gui/wxgui/input/panels/InputPanel.cpp +++ /dev/null @@ -1,285 +0,0 @@ -#include "wxgui/input/panels/InputPanel.h" - -#include -#include - -#include "wxgui/helpers/wxHelpers.h" - -InputPanel::InputPanel(wxWindow* parent) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxNO_BORDER | wxWANTS_CHARS) -{ - Bind(wxEVT_LEFT_UP, &InputPanel::on_left_click, this); -} - -void InputPanel::on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) -{ - const auto& state = controller->update_state(); - - if(m_focused_element == wxID_NONE) - { - return; - } - - const auto element = dynamic_cast(FindWindow(m_focused_element)); - wxASSERT(element); - - const auto mapping = reinterpret_cast(element->GetClientData()); - - // reset mapping - if(std::exchange(m_right_down, false) || WindowSystem::IsKeyDown(WindowSystem::PlatformKeyCodes::ESCAPE)) - { - element->SetBackgroundColour(kKeyColourNormalMode); - m_color_backup[element->GetId()] = kKeyColourNormalMode; - - emulated_controller->delete_mapping(mapping); - if(element->IsEmpty()) - reset_focused_element(); - else - element->SetValue(wxEmptyString); - - return; - } - - static bool s_was_idle = true; - if (state.buttons.IsIdle()) - { - s_was_idle = true; - return; - } - - if (!s_was_idle) - { - return; - } - s_was_idle = false; - for(const auto& id : state.buttons.GetButtonList()) - { - if (controller->has_axis()) - { - // test if one axis direction is pressed more than the other - if ((id == kAxisXP || id == kAxisXN) && (state.buttons.GetButtonState(kAxisYP) || state.buttons.GetButtonState(kAxisYN))) - { - if (std::abs(state.axis.y) > std::abs(state.axis.x)) - continue; - } - else if ((id == kAxisYP || id == kAxisYN) && (state.buttons.GetButtonState(kAxisXP) || state.buttons.GetButtonState(kAxisXN))) - { - if (std::abs(state.axis.x) > std::abs(state.axis.y)) - continue; - } - else if ((id == kRotationXP || id == kRotationXN) && (state.buttons.GetButtonState(kRotationYP) || state.buttons.GetButtonState(kRotationYN))) - { - if (std::abs(state.rotation.y) > std::abs(state.rotation.x)) - continue; - } - else if ((id == kRotationYP || id == kRotationYN) && (state.buttons.GetButtonState(kRotationXP) || state.buttons.GetButtonState(kRotationXN))) - { - if (std::abs(state.rotation.x) > std::abs(state.rotation.y)) - continue; - } - else if ((id == kTriggerXP || id == kTriggerXN) && (state.buttons.GetButtonState(kTriggerYP) || state.buttons.GetButtonState(kTriggerYN))) - { - if (std::abs(state.trigger.y) > std::abs(state.trigger.x)) - continue; - } - else if ((id == kTriggerYP || id == kTriggerYN) && (state.buttons.GetButtonState(kTriggerXP) || state.buttons.GetButtonState(kTriggerXN))) - { - if (std::abs(state.trigger.x) > std::abs(state.trigger.y)) - continue; - } - - // ignore too low button values on configuration - if (id >= kButtonAxisStart) - { - if (controller->get_axis_value(id) < 0.33f) { - cemuLog_logDebug(LogType::Force, "skipping since value too low {}", controller->get_axis_value(id)); - s_was_idle = true; - return; - } - } - } - - emulated_controller->set_mapping(mapping, controller, id); - element->SetValue(controller->get_button_name(id)); - element->SetBackgroundColour(kKeyColourNormalMode); - m_color_backup[element->GetId()] = kKeyColourNormalMode; - break; - } - - if (const auto sibling = get_next_sibling(element)) - sibling->SetFocus(); - else // last element reached - { - reset_focused_element(); - this->SetFocusIgnoringChildren(); - } -} - -void InputPanel::reset_configuration() -{ - m_color_backup.clear(); - - wxWindowUpdateLocker lock(this); - for (const auto& c : GetChildren()) - { - if (auto* text = dynamic_cast(c)) - { - text->SetValue(wxEmptyString); - text->SetBackgroundColour(kKeyColourNormalMode); - text->Refresh(); - } - } -} - -void InputPanel::reset_colours() -{ - m_color_backup.clear(); - - wxWindowUpdateLocker lock(this); - for (const auto& c : GetChildren()) - { - if (auto* text = dynamic_cast(c)) - { - text->SetBackgroundColour(kKeyColourNormalMode); - text->Refresh(); - } - } -} - - -void InputPanel::load_controller(const EmulatedControllerPtr& controller) -{ - reset_configuration(); - if(!controller) - { - return; - } - - if(controller->get_controllers().empty()) - { - return; - } - - wxWindowUpdateLocker lock(this); - for (auto* child : this->GetChildren()) - { - const auto text = dynamic_cast(child); - if (text == nullptr) - continue; - - - const auto mapping = reinterpret_cast(text->GetClientData()); - if (mapping <= 0) - continue; - - auto button_name = controller->get_mapping_name(mapping); - text->ChangeValue(button_name); - } -} - -void InputPanel::set_selected_controller(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) -{ - wxWindowUpdateLocker lock(this); - for (auto* child : this->GetChildren()) - { - const auto text = dynamic_cast(child); - if (text == nullptr) - continue; - - if (text->GetId() == m_focused_element) - continue; - - const auto mapping = reinterpret_cast(text->GetClientData()); - if (mapping <= 0) - continue; - - const auto mapping_controller = emulated_controller->get_mapping_controller(mapping); - if (!mapping_controller) - continue; - - text->SetBackgroundColour(*mapping_controller == *controller ? kKeyColourNormalMode : kKeyColourActiveMode); - text->Refresh(); - } -} - -void InputPanel::bind_hotkey_events(wxTextCtrl* text_ctrl) -{ - text_ctrl->Bind(wxEVT_SET_FOCUS, &InputPanel::on_edit_key_focus, this); - text_ctrl->Bind(wxEVT_KILL_FOCUS, &InputPanel::on_edit_key_kill_focus, this); - text_ctrl->Bind(wxEVT_RIGHT_DOWN, &InputPanel::on_right_click, this); -#if BOOST_OS_LINUX - // Bind to a no-op lambda to disable arrow keys navigation - text_ctrl->Bind(wxEVT_KEY_DOWN, [](wxKeyEvent &) {}); -#endif -} - -void InputPanel::on_left_click(wxMouseEvent& event) -{ - if (m_focused_element == wxID_NONE) - return; - - const auto focuses_element = FindWindow(m_focused_element); - wxASSERT(focuses_element); - - wxFocusEvent focus(wxEVT_KILL_FOCUS, m_focused_element); - focus.SetWindow(focuses_element); - focuses_element->GetEventHandler()->ProcessEvent(focus); - - this->SetFocusIgnoringChildren(); -} - -void InputPanel::on_edit_key_focus(wxFocusEvent& event) -{ - auto* text = dynamic_cast(event.GetEventObject()); - wxASSERT(text); - - m_color_backup[text->GetId()] = text->GetBackgroundColour(); - - text->SetBackgroundColour(kKeyColourEditMode); - #if BOOST_OS_WINDOWS - text->HideNativeCaret(); - #endif - text->Refresh(); - - m_focused_element = text->GetId(); - event.Skip(); -} - -void InputPanel::on_edit_key_kill_focus(wxFocusEvent& event) -{ - reset_focused_element(); - event.Skip(); -} - -void InputPanel::on_right_click(wxMouseEvent& event) -{ - m_right_down = true; - if(m_focused_element == wxID_NONE) - { - auto* text = dynamic_cast(event.GetEventObject()); - wxASSERT(text); - text->SetFocus(); - } -} - -bool InputPanel::reset_focused_element() -{ - if (m_focused_element == wxID_NONE) - return false; - - auto* prev_element = dynamic_cast(FindWindow(m_focused_element)); - wxASSERT(prev_element); - - if(m_color_backup.find(prev_element->GetId()) != m_color_backup.cend()) - prev_element->SetBackgroundColour(m_color_backup[prev_element->GetId()]); - else - prev_element->SetBackgroundColour(kKeyColourNormalMode); - -#if BOOST_OS_WINDOWS - prev_element->HideNativeCaret(); -#endif - prev_element->Refresh(); - - m_focused_element = wxID_NONE; - return true; -} diff --git a/src/gui/wxgui/input/panels/InputPanel.h b/src/gui/wxgui/input/panels/InputPanel.h deleted file mode 100644 index 818708eb..00000000 --- a/src/gui/wxgui/input/panels/InputPanel.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include - -#include "input/emulated/EmulatedController.h" -#include "input/api/Controller.h" - -#include "wxgui/wxHelper.h" - -class ControllerBase; -class wxTextCtrl; -class wxComboBox; - -class InputPanel : public wxPanel -{ -public: - const wxColour kKeyColourNormalMode = GetBackgroundColour(); - const wxColour kKeyColourEditMode = wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT); - const wxColour kKeyColourActiveMode = wxHelper::CalculateAccentColour(GetBackgroundColour()); - - InputPanel(wxWindow* parent); - - ControllerPtr get_active_controller() const; - - virtual void on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller); - void on_left_click(wxMouseEvent& event); - - void reset_configuration(); - virtual void load_controller(const EmulatedControllerPtr& controller); - - void set_selected_controller(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller_base); - void reset_colours(); - -protected: - void bind_hotkey_events(wxTextCtrl* text_ctrl); - - void on_edit_key_focus(wxFocusEvent& event); - void on_edit_key_kill_focus(wxFocusEvent& event); - void on_right_click(wxMouseEvent& event); - - bool reset_focused_element(); - - bool m_right_down = false; - int m_focused_element = wxID_NONE; - std::unordered_map m_color_backup; -}; - diff --git a/src/gui/wxgui/input/panels/ProControllerInputPanel.cpp b/src/gui/wxgui/input/panels/ProControllerInputPanel.cpp deleted file mode 100644 index 8b8ac741..00000000 --- a/src/gui/wxgui/input/panels/ProControllerInputPanel.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "wxgui/input/panels/ProControllerInputPanel.h" - -#include -#include -#include -#include - -#include "input/emulated/ProController.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" - -const ProController::ButtonId g_kFirstColumnItems[] = { ProController::kButtonId_A, ProController::kButtonId_B, ProController::kButtonId_X, ProController::kButtonId_Y, ProController::kButtonId_L, ProController::kButtonId_R, ProController::kButtonId_ZL, ProController::kButtonId_ZR, ProController::kButtonId_Plus, ProController::kButtonId_Minus }; -const ProController::ButtonId g_kSecondColumnItems[] = { ProController::kButtonId_StickL, ProController::kButtonId_StickL_Up, ProController::kButtonId_StickL_Down, ProController::kButtonId_StickL_Left, ProController::kButtonId_StickL_Right }; -const ProController::ButtonId g_kThirdColumnItems[] = { ProController::kButtonId_StickR, ProController::kButtonId_StickR_Up, ProController::kButtonId_StickR_Down, ProController::kButtonId_StickR_Left, ProController::kButtonId_StickR_Right }; -const ProController::ButtonId g_kFourthRowItems[] = { ProController::kButtonId_Up, ProController::kButtonId_Down, ProController::kButtonId_Left, ProController::kButtonId_Right }; - - -ProControllerInputPanel::ProControllerInputPanel(wxWindow* parent) - : InputPanel(parent) -{ - auto bold_font = GetFont(); - bold_font.MakeBold(); - - auto main_sizer = new wxGridBagSizer(); - - sint32 row = 0; - sint32 column = 0; - for (auto id : g_kFirstColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - ////////////////////////////////////////////////////////////////// - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 2), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - row = 0; - column += 3; - - auto text = new wxStaticText(this, wxID_ANY, _("Left Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (auto id : g_kSecondColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_left_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_left_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("Right Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (auto id : g_kThirdColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_right_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_right_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("D-pad")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (auto id : g_kFourthRowItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - ////////////////////////////////////////////////////////////////// - - SetSizerAndFit(main_sizer); -} - -void ProControllerInputPanel::add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const ProController::ButtonId &button_id) { - sizer->Add( - new wxStaticText(this, wxID_ANY, wxGetTranslation(wxString::FromUTF8(ProController::get_button_name(button_id)))), - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - - auto text_ctrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB); - text_ctrl->SetClientData((void*)button_id); - text_ctrl->SetMinSize(wxSize(150, -1)); - text_ctrl->SetEditable(false); - text_ctrl->SetBackgroundColour(kKeyColourNormalMode); - bind_hotkey_events(text_ctrl); - sizer->Add(text_ctrl, wxGBPosition(row, column + 1), wxDefaultSpan, wxALL | wxEXPAND, 5); -} - -void ProControllerInputPanel::on_timer(const EmulatedControllerPtr& emulated_controller, - const ControllerPtr& controller_base) -{ - InputPanel::on_timer(emulated_controller, controller_base); - - if (emulated_controller) - { - const auto axis = emulated_controller->get_axis(); - const auto rotation = emulated_controller->get_rotation(); - - m_left_draw->SetAxisValue(axis); - m_right_draw->SetAxisValue(rotation); - } -} diff --git a/src/gui/wxgui/input/panels/ProControllerInputPanel.h b/src/gui/wxgui/input/panels/ProControllerInputPanel.h deleted file mode 100644 index cd82a957..00000000 --- a/src/gui/wxgui/input/panels/ProControllerInputPanel.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include "input/emulated/ProController.h" -#include "wxgui/input/panels/InputPanel.h" -#include "wxgui/components/wxInputDraw.h" - -class ProControllerInputPanel : public InputPanel -{ -public: - ProControllerInputPanel(wxWindow* parent); - - void on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) override; - -private: - wxInputDraw* m_left_draw, * m_right_draw; - - void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const ProController::ButtonId &button_id); -}; diff --git a/src/gui/wxgui/input/panels/VPADInputPanel.cpp b/src/gui/wxgui/input/panels/VPADInputPanel.cpp deleted file mode 100644 index 676d70eb..00000000 --- a/src/gui/wxgui/input/panels/VPADInputPanel.cpp +++ /dev/null @@ -1,206 +0,0 @@ -#include "wxgui/input/panels/VPADInputPanel.h" - -#include -#include -#include -#include -#include -#include - - -#include "wxgui/helpers/wxControlObject.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" -#include "input/emulated/VPADController.h" - -constexpr VPADController::ButtonId g_kFirstColumnItems[] = -{ - VPADController::kButtonId_A, VPADController::kButtonId_B, VPADController::kButtonId_X, VPADController::kButtonId_Y, - VPADController::kButtonId_L, VPADController::kButtonId_R, VPADController::kButtonId_ZL, VPADController::kButtonId_ZR, - VPADController::kButtonId_Plus, VPADController::kButtonId_Minus -}; - -constexpr VPADController::ButtonId g_kSecondColumnItems[] = -{ - VPADController::kButtonId_StickL, VPADController::kButtonId_StickL_Up, VPADController::kButtonId_StickL_Down, VPADController::kButtonId_StickL_Left, VPADController::kButtonId_StickL_Right -}; - -constexpr VPADController::ButtonId g_kThirdColumnItems[] = -{ - VPADController::kButtonId_StickR, VPADController::kButtonId_StickR_Up, VPADController::kButtonId_StickR_Down, VPADController::kButtonId_StickR_Left, VPADController::kButtonId_StickR_Right -}; - -constexpr VPADController::ButtonId g_kFourthRowItems[] = -{ - VPADController::kButtonId_Up, VPADController::kButtonId_Down, VPADController::kButtonId_Left, VPADController::kButtonId_Right -}; - - -VPADInputPanel::VPADInputPanel(wxWindow* parent) - : InputPanel(parent) -{ - auto bold_font = GetFont(); - bold_font.MakeBold(); - - auto* main_sizer = new wxGridBagSizer(); - - sint32 row = 0; - sint32 column = 0; - for (const auto& id : g_kFirstColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - ////////////////////////////////////////////////////////////////// - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 2), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - row = 0; - column += 3; - - auto text = new wxStaticText(this, wxID_ANY, _("Left Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kSecondColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_left_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_left_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("Right Axis")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kThirdColumnItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - row++; - - // input drawer - m_right_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - main_sizer->Add(m_right_draw, wxGBPosition(row, column + 1), wxGBSpan(2, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - // Volume - row = 10; - - text = new wxStaticText(this, wxID_ANY, _("Volume")); - text->Disable(); - main_sizer->Add(text, wxGBPosition(row, column), wxDefaultSpan, wxALL, 5); - - auto*m_volume = new wxSlider(this, wxID_ANY, 0, 0, 100); - m_volume->Disable(); - main_sizer->Add(m_volume, wxGBPosition(row, column + 1), wxDefaultSpan, wxTOP | wxBOTTOM | wxEXPAND, 5); - - const auto volume_text = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", 0)); - volume_text->Disable(); - main_sizer->Add(volume_text, wxGBPosition(row, column + 2), wxDefaultSpan, wxALL, 5); - m_volume->Bind(wxEVT_SLIDER, &VPADInputPanel::OnVolumeChange, this, wxID_ANY, wxID_ANY, new wxControlObject(volume_text)); - - main_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("D-pad")); - text->SetFont(bold_font); - main_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kFourthRowItems) - { - row++; - add_button_row(main_sizer, row, column, id); - } - - // Blow Mic - row = 8; - add_button_row(main_sizer, row, column, VPADController::kButtonId_Mic, _("blow mic")); - row++; - - add_button_row(main_sizer, row, column, VPADController::kButtonId_Screen, _("show screen")); - row++; - - auto toggleScreenText = new wxStaticText(this, wxID_ANY, _("toggle screen")); - main_sizer->Add(toggleScreenText, - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_togglePadViewCheckBox = new wxCheckBox(this, wxID_ANY, {}, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxString toggleScreenTT = _("Makes the \"show screen\" button toggle between the TV and gamepad screens"); - m_togglePadViewCheckBox->SetToolTip(toggleScreenTT); - toggleScreenText->SetToolTip(toggleScreenTT); - main_sizer->Add(m_togglePadViewCheckBox, wxGBPosition(row,column+1), wxDefaultSpan, wxALL | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - SetSizer(main_sizer); - Layout(); -} - -void VPADInputPanel::add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const VPADController::ButtonId &button_id) { - add_button_row(sizer, row, column, button_id, wxGetTranslation(wxString::FromUTF8(VPADController::get_button_name(button_id)))); -} - -void VPADInputPanel::add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, - const VPADController::ButtonId &button_id, const wxString &label) { - sizer->Add( - new wxStaticText(this, wxID_ANY, label), - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - - auto* text_ctrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB); - text_ctrl->SetClientData((void*)button_id); - text_ctrl->SetMinSize(wxSize(150, -1)); - text_ctrl->SetEditable(false); - text_ctrl->SetBackgroundColour(kKeyColourNormalMode); - bind_hotkey_events(text_ctrl); - sizer->Add(text_ctrl, wxGBPosition(row, column + 1), wxDefaultSpan, wxALL | wxEXPAND, 5); -} - -void VPADInputPanel::on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller_base) -{ - InputPanel::on_timer(emulated_controller, controller_base); - - static_cast(emulated_controller.get())->set_screen_toggle(m_togglePadViewCheckBox->GetValue()); - - if(emulated_controller) - { - const auto axis = emulated_controller->get_axis(); - const auto rotation = emulated_controller->get_rotation(); - - m_left_draw->SetAxisValue(axis); - m_right_draw->SetAxisValue(rotation); - } -} - -void VPADInputPanel::OnVolumeChange(wxCommandEvent& event) -{ - -} -void VPADInputPanel::load_controller(const EmulatedControllerPtr& controller) -{ - InputPanel::load_controller(controller); - - const bool isToggle = static_cast(controller.get())->is_screen_active_toggle(); - m_togglePadViewCheckBox->SetValue(isToggle); -} diff --git a/src/gui/wxgui/input/panels/VPADInputPanel.h b/src/gui/wxgui/input/panels/VPADInputPanel.h deleted file mode 100644 index 97b3ad1b..00000000 --- a/src/gui/wxgui/input/panels/VPADInputPanel.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include "wxgui/input/panels/InputPanel.h" - -class wxInputDraw; -class wxCheckBox; - -class VPADInputPanel : public InputPanel -{ -public: - VPADInputPanel(wxWindow* parent); - - void on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) override; - virtual void load_controller(const EmulatedControllerPtr& controller) override; - -private: - void OnVolumeChange(wxCommandEvent& event); - - wxInputDraw* m_left_draw, * m_right_draw; - wxCheckBox* m_togglePadViewCheckBox; - - void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const VPADController::ButtonId &button_id); - void add_button_row(wxGridBagSizer *sizer, sint32 row, sint32 column, const VPADController::ButtonId &button_id, const wxString &label); -}; diff --git a/src/gui/wxgui/input/panels/WiimoteInputPanel.cpp b/src/gui/wxgui/input/panels/WiimoteInputPanel.cpp deleted file mode 100644 index da94852c..00000000 --- a/src/gui/wxgui/input/panels/WiimoteInputPanel.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "wxgui/input/panels/WiimoteInputPanel.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "wxgui/helpers/wxControlObject.h" -#include "input/emulated/WiimoteController.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" - -constexpr WiimoteController::ButtonId g_kFirstColumnItems[] = -{ - WiimoteController::kButtonId_A, WiimoteController::kButtonId_B, WiimoteController::kButtonId_1, WiimoteController::kButtonId_2, WiimoteController::kButtonId_Plus, WiimoteController::kButtonId_Minus, WiimoteController::kButtonId_Home -}; - -constexpr WiimoteController::ButtonId g_kSecondColumnItems[] = -{ - WiimoteController::kButtonId_Up, WiimoteController::kButtonId_Down, WiimoteController::kButtonId_Left, WiimoteController::kButtonId_Right -}; - -constexpr WiimoteController::ButtonId g_kThirdColumnItems[] = -{ - WiimoteController::kButtonId_Nunchuck_C, WiimoteController::kButtonId_Nunchuck_Z, - WiimoteController::kButtonId_None, - WiimoteController::kButtonId_Nunchuck_Up,WiimoteController::kButtonId_Nunchuck_Down,WiimoteController::kButtonId_Nunchuck_Left,WiimoteController::kButtonId_Nunchuck_Right -}; - -WiimoteInputPanel::WiimoteInputPanel(wxWindow* parent) - : InputPanel(parent) -{ - auto bold_font = GetFont(); - bold_font.MakeBold(); - - auto* main_sizer = new wxBoxSizer(wxVERTICAL); - auto* horiz_main_sizer = new wxBoxSizer(wxHORIZONTAL); - - auto* extensions_sizer = new wxBoxSizer(wxHORIZONTAL); - horiz_main_sizer->Add(extensions_sizer, wxSizerFlags(0).Align(wxALIGN_CENTER_VERTICAL)); - - extensions_sizer->Add(new wxStaticText(this, wxID_ANY, _("Extensions:"))); - extensions_sizer->AddSpacer(10); - - m_motion_plus = new wxCheckBox(this, wxID_ANY, _("MotionPlus")); - m_motion_plus->Bind(wxEVT_CHECKBOX, &WiimoteInputPanel::on_extension_change, this); - extensions_sizer->Add(m_motion_plus); - - m_nunchuck = new wxCheckBox(this, wxID_ANY, _("Nunchuck")); - m_nunchuck->Bind(wxEVT_CHECKBOX, &WiimoteInputPanel::on_extension_change, this); - extensions_sizer->Add(m_nunchuck); - - m_classic = new wxCheckBox(this, wxID_ANY, _("Classic")); - m_classic->Bind(wxEVT_CHECKBOX, &WiimoteInputPanel::on_extension_change, this); - m_classic->Hide(); - extensions_sizer->Add(m_classic); - - main_sizer->Add(horiz_main_sizer, 0, wxEXPAND | wxALL, 5); - main_sizer->Add(new wxStaticLine(this), 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 5); - - m_item_sizer = new wxGridBagSizer(); - - sint32 row = 0; - sint32 column = 0; - for (const auto& id : g_kFirstColumnItems) - { - row++; - add_button_row(row, column, id); - } - - m_item_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 2), wxGBSpan(11, 1), wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 3; - - auto text = new wxStaticText(this, wxID_ANY, _("D-pad")); - text->SetFont(bold_font); - m_item_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kSecondColumnItems) - { - row++; - add_button_row(row, column, id); - } - - row = 8; - // Volume - text = new wxStaticText(this, wxID_ANY, _("Volume")); - text->Disable(); - m_item_sizer->Add(text, wxGBPosition(row, column), wxDefaultSpan, wxALL, 5); - - m_volume = new wxSlider(this, wxID_ANY, 0, 0, 100); - m_volume->Disable(); - m_item_sizer->Add(m_volume, wxGBPosition(row, column + 1), wxDefaultSpan, wxTOP | wxBOTTOM | wxEXPAND, 5); - - const auto volume_text = new wxStaticText(this, wxID_ANY, wxString::Format("%d%%", 0)); - volume_text->Disable(); - m_item_sizer->Add(volume_text, wxGBPosition(row, column + 2), wxDefaultSpan, wxALL, 5); - m_volume->Bind(wxEVT_SLIDER, &WiimoteInputPanel::on_volume_change, this, wxID_ANY, wxID_ANY, new wxControlObject(volume_text)); - row++; - - m_item_sizer->Add(new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVERTICAL), wxGBPosition(0, column + 3), wxGBSpan(11, 1), wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 5); - - ////////////////////////////////////////////////////////////////// - - row = 0; - column += 4; - - text = new wxStaticText(this, wxID_ANY, _("Nunchuck")); - text->SetFont(bold_font); - m_item_sizer->Add(text, wxGBPosition(row, column), wxGBSpan(1, 3), wxALL | wxEXPAND, 5); - - for (const auto& id : g_kThirdColumnItems) - { - row++; - if (id == WiimoteController::kButtonId_None) - continue; - - m_item_sizer->Add( - new wxStaticText(this, wxID_ANY, wxGetTranslation(wxString::FromUTF8(WiimoteController::get_button_name(id)))), - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - - auto* text_ctrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB); - text_ctrl->SetClientData((void*)id); - text_ctrl->SetMinSize(wxSize(150, -1)); - text_ctrl->SetEditable(false); - text_ctrl->SetBackgroundColour(kKeyColourNormalMode); - bind_hotkey_events(text_ctrl); - text_ctrl->Enable(m_nunchuck->GetValue()); - m_item_sizer->Add(text_ctrl, wxGBPosition(row, column + 1), wxDefaultSpan, wxALL | wxEXPAND, 5); - - m_nunchuck_items.push_back(text_ctrl); - } - - - // input drawer - m_draw = new wxInputDraw(this, wxID_ANY, wxDefaultPosition, { 60, 60 }); - m_draw->Enable(m_nunchuck->GetValue()); - m_item_sizer->Add(5, 0, wxGBPosition(3, column + 3), wxDefaultSpan, wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - m_item_sizer->Add(m_draw, wxGBPosition(3, column + 4), wxGBSpan(4, 1), wxTOP | wxBOTTOM | wxEXPAND | wxALIGN_CENTER, 5); - - m_nunchuck_items.push_back(m_draw); - - ////////////////////////////////////////////////////////////////// - - main_sizer->Add(m_item_sizer, 1, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 5); - - SetSizer(main_sizer); - Layout(); -} - -void WiimoteInputPanel::add_button_row(sint32 row, sint32 column, const WiimoteController::ButtonId &button_id) { - m_item_sizer->Add( - new wxStaticText(this, wxID_ANY, wxGetTranslation(wxString::FromUTF8(WiimoteController::get_button_name(button_id)))), - wxGBPosition(row, column), - wxDefaultSpan, - wxALL | wxALIGN_CENTER_VERTICAL, 5); - - auto* text_ctrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB); - text_ctrl->SetClientData((void*)button_id); - text_ctrl->SetMinSize(wxSize(150, -1)); - text_ctrl->SetEditable(false); - text_ctrl->SetBackgroundColour(kKeyColourNormalMode); - bind_hotkey_events(text_ctrl); - m_item_sizer->Add(text_ctrl, wxGBPosition(row, column + 1), wxDefaultSpan, wxALL | wxEXPAND, 5); -} - -void WiimoteInputPanel::set_active_device_type(WPADDeviceType type) -{ - m_device_type = type; - - m_motion_plus->SetValue(type == kWAPDevMPLS || type == kWAPDevMPLSFreeStyle || type == kWAPDevMPLSClassic); - switch(type) - { - case kWAPDevFreestyle: - case kWAPDevMPLSFreeStyle: - m_nunchuck->SetValue(true); - m_classic->SetValue(false); - for (const auto& item : m_nunchuck_items) - { - item->Enable(true); - } - break; - - case kWAPDevClassic: - case kWAPDevMPLSClassic: - m_nunchuck->SetValue(false); - m_classic->SetValue(true); - for (const auto& item : m_nunchuck_items) - { - item->Enable(false); - } - break; - - default: - m_nunchuck->SetValue(false); - m_classic->SetValue(false); - for (const auto& item : m_nunchuck_items) - { - item->Enable(false); - } - } -} - -void WiimoteInputPanel::on_volume_change(wxCommandEvent& event) -{ -} - -void WiimoteInputPanel::on_extension_change(wxCommandEvent& event) -{ - if(m_motion_plus->GetValue() && m_nunchuck->GetValue()) - set_active_device_type(kWAPDevMPLSFreeStyle); - else if(m_motion_plus->GetValue() && m_classic->GetValue()) - set_active_device_type(kWAPDevMPLSClassic); - else if (m_motion_plus->GetValue()) - set_active_device_type(kWAPDevMPLS); - else if (m_nunchuck->GetValue()) - set_active_device_type(kWAPDevFreestyle); - else if (m_classic->GetValue()) - set_active_device_type(kWAPDevClassic); - else - set_active_device_type(kWAPDevCore); -} - -void WiimoteInputPanel::on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) -{ - if (emulated_controller) - { - const auto wiimote = std::dynamic_pointer_cast(emulated_controller); - wxASSERT(wiimote); - - wiimote->set_device_type(m_device_type); - } - - InputPanel::on_timer(emulated_controller, controller); - - if (emulated_controller) - { - const auto axis = emulated_controller->get_axis(); - m_draw->SetAxisValue(axis); - } -} - -void WiimoteInputPanel::load_controller(const EmulatedControllerPtr& emulated_controller) -{ - InputPanel::load_controller(emulated_controller); - - if (emulated_controller) { - const auto wiimote = std::dynamic_pointer_cast(emulated_controller); - wxASSERT(wiimote); - set_active_device_type(wiimote->get_device_type()); - } -} diff --git a/src/gui/wxgui/input/panels/WiimoteInputPanel.h b/src/gui/wxgui/input/panels/WiimoteInputPanel.h deleted file mode 100644 index 946f7cd5..00000000 --- a/src/gui/wxgui/input/panels/WiimoteInputPanel.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include "wxgui/input/panels/InputPanel.h" -#include "input/emulated/WiimoteController.h" -#include - -class wxCheckBox; -class wxGridBagSizer; -class wxInputDraw; - -class WiimoteInputPanel : public InputPanel -{ -public: - WiimoteInputPanel(wxWindow* parent); - - void on_timer(const EmulatedControllerPtr& emulated_controller, const ControllerPtr& controller) override; - - void load_controller(const EmulatedControllerPtr& emulated_controller) override; - -private: - wxInputDraw* m_draw; - - WPADDeviceType m_device_type = kWAPDevCore; - void set_active_device_type(WPADDeviceType type); - - void on_volume_change(wxCommandEvent& event); - void on_extension_change(wxCommandEvent& event); - void on_pair_button(wxCommandEvent& event); - - wxGridBagSizer* m_item_sizer; - - wxCheckBox* m_nunchuck, * m_classic; - wxCheckBox* m_motion_plus; - - wxSlider* m_volume; - - std::vector m_nunchuck_items; - - void add_button_row(sint32 row, sint32 column, const WiimoteController::ButtonId &button_id); -}; - - - - diff --git a/src/gui/wxgui/input/settings/DefaultControllerSettings.cpp b/src/gui/wxgui/input/settings/DefaultControllerSettings.cpp deleted file mode 100644 index c10c15ce..00000000 --- a/src/gui/wxgui/input/settings/DefaultControllerSettings.cpp +++ /dev/null @@ -1,318 +0,0 @@ -#include "wxgui/input/settings/DefaultControllerSettings.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "wxgui/helpers/wxControlObject.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" -#include "wxgui/input/InputAPIAddWindow.h" - - -DefaultControllerSettings::DefaultControllerSettings(wxWindow* parent, const wxPoint& position, std::shared_ptr controller) - : wxDialog(parent, wxID_ANY, _("Controller settings"), position, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE), m_controller(std::move(controller)) -{ - m_settings = m_controller->get_settings(); - m_rumble_backup = m_settings.rumble; - - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - - // options - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Options")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - // Motion - m_use_motion = new wxCheckBox(box, wxID_ANY, _("Use motion")); - m_use_motion->SetValue(m_settings.motion); - m_use_motion->Enable(m_controller->has_motion()); - box_sizer->Add(m_use_motion, 0, wxEXPAND | wxALL, 5); - - // Vibration - auto* rumbleSizer = new wxBoxSizer(wxHORIZONTAL); - - const auto rumble = (int)(m_settings.rumble * 100); - rumbleSizer->Add(new wxStaticText(box, wxID_ANY, _("Rumble")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_rumble = new wxSlider(box, wxID_ANY, rumble, 0, 100); - rumbleSizer->Add(m_rumble, 1, wxALL | wxEXPAND, 5); - - const auto text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", rumble)); - rumbleSizer->Add(text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_rumble->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_rumble_change, this, wxID_ANY, wxID_ANY, new wxControlObject(text)); - - box_sizer->Add(rumbleSizer); - - sizer->Add(box_sizer, 1, wxALL|wxEXPAND, 5); - } - - auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); - // Axis - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Axis")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - { - auto* content_sizer = new wxFlexGridSizer(0, 3, 0, 0); - - // Deadzone - const auto deadzone = (int)(m_settings.axis.deadzone * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_axis_deadzone = new wxSlider(box, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_axis_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_axis_deadzone->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_deadzone_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - - - // Range - const auto range = (int)(m_settings.axis.range * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_axis_range = new wxSlider(box, wxID_ANY, range, 50, 200); - content_sizer->Add(m_axis_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_axis_range->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_range_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - - content_sizer->AddSpacer(1); - m_axis_draw = new wxInputDraw(box, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_axis_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - - box_sizer->Add(content_sizer, 1, wxEXPAND, 0); - } - - row_sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - // Rotation - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Rotation")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - { - auto* content_sizer = new wxFlexGridSizer(0, 3, 0, 0); - - // Deadzone - const auto deadzone = (int)(m_settings.rotation.deadzone * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_rotation_deadzone = new wxSlider(box, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_rotation_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_rotation_deadzone->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_deadzone_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - - - // Range - const auto range = (int)(m_settings.rotation.range * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_rotation_range = new wxSlider(box, wxID_ANY, range, 50, 200); - content_sizer->Add(m_rotation_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_rotation_range->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_range_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - - content_sizer->AddSpacer(1); - m_rotation_draw = new wxInputDraw(box, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_rotation_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - - box_sizer->Add(content_sizer, 1, wxEXPAND, 0); - } - - row_sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - // Trigger - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Trigger")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - { - auto* content_sizer = new wxFlexGridSizer(0, 3, 0, 0); - - // Deadzone - const auto deadzone = (int)(m_settings.trigger.deadzone * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_trigger_deadzone = new wxSlider(box, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_trigger_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_trigger_deadzone->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_deadzone_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - - - // Range - const auto range = (int)(m_settings.trigger.range * 100); - content_sizer->Add(new wxStaticText(box, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_trigger_range = new wxSlider(box, wxID_ANY, range, 50, 200); - content_sizer->Add(m_trigger_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(box, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_trigger_range->Bind(wxEVT_SLIDER, &DefaultControllerSettings::on_range_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - - content_sizer->AddSpacer(1); - m_trigger_draw = new wxInputDraw(box, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_trigger_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - - box_sizer->Add(content_sizer, 1, wxEXPAND, 0); - } - - row_sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - sizer->Add(row_sizer); - - { - auto* control_sizer = new wxFlexGridSizer(0, 4, 0, 0); - control_sizer->AddGrowableCol(3); - - auto* ok_button = new wxButton(this, wxID_ANY, _("OK")); - ok_button->Bind(wxEVT_BUTTON, [this](auto&) { update_settings(); EndModal(wxID_OK); }); - control_sizer->Add(ok_button, 0, wxALL, 5); - - control_sizer->Add(0, 0, 0, wxEXPAND, 5); - - auto* cancel_button = new wxButton(this, wxID_ANY, _("Cancel")); - cancel_button->Bind(wxEVT_BUTTON, [this](auto&) { EndModal(wxID_CANCEL); }); - control_sizer->Add(cancel_button, 0, wxALL, 5); - - auto* calibrate_button = new wxButton(this, wxID_ANY, _("Calibrate")); - calibrate_button->Bind(wxEVT_BUTTON, [this](auto&) { m_controller->calibrate(); }); - control_sizer->Add(calibrate_button, 0, wxALL | wxALIGN_RIGHT, 5); - - sizer->Add(control_sizer, 0, wxEXPAND, 5); - } - - - - this->SetSizer(sizer); - this->Layout(); - this->Fit(); - - this->Bind(wxEVT_CLOSE_WINDOW, &DefaultControllerSettings::on_close, this); - - m_timer = new wxTimer(this); - Bind(wxEVT_TIMER, &DefaultControllerSettings::on_timer, this); - m_timer->Start(100); -} - -DefaultControllerSettings::~DefaultControllerSettings() -{ - m_controller->stop_rumble(); - m_timer->Stop(); -} - -void DefaultControllerSettings::update_settings() -{ - // update settings - m_controller->set_settings(m_settings); - if (m_use_motion) - m_controller->set_use_motion(m_use_motion->GetValue()); -} - -void DefaultControllerSettings::on_timer(wxTimerEvent& event) -{ - if (m_rumble_time.has_value() && std::chrono::duration_cast(std::chrono::steady_clock::now() - m_rumble_time.value()).count() > 500 ) - { - m_controller->stop_rumble(); - m_rumble_time = {}; - } - - const auto& default_state = m_controller->get_default_state(); - - auto state = m_controller->raw_state(); - m_controller->apply_axis_setting(state.axis, default_state.axis, m_settings.axis); - m_controller->apply_axis_setting(state.rotation, default_state.rotation, m_settings.rotation); - m_controller->apply_axis_setting(state.trigger, default_state.trigger, m_settings.trigger); - - if (m_controller->api() == InputAPI::SDLController) - { - state.axis.y *= -1; - state.rotation.y *= -1; - } - - m_axis_draw->SetDeadzone(m_settings.axis.deadzone); - m_axis_draw->SetAxisValue(state.axis); - - m_rotation_draw->SetDeadzone(m_settings.rotation.deadzone); - m_rotation_draw->SetAxisValue(state.rotation); - - m_trigger_draw->SetDeadzone(m_settings.trigger.deadzone); - m_trigger_draw->SetAxisValue(state.trigger); -} - -void DefaultControllerSettings::on_close(wxCloseEvent& event) -{ - if (this->GetReturnCode() == 0 || this->GetReturnCode() == wxID_OK) - update_settings(); - - event.Skip(); -} - -void DefaultControllerSettings::on_deadzone_change(wxCommandEvent& event) -{ - update_slider_text(event); - const auto new_value = (float)event.GetInt() / 100.0f; - - if (event.GetEventObject() == m_axis_deadzone) - m_settings.axis.deadzone = new_value; - else if (event.GetEventObject() == m_rotation_deadzone) - m_settings.rotation.deadzone = new_value; - else if (event.GetEventObject() == m_trigger_deadzone) - m_settings.trigger.deadzone = new_value; -} - -void DefaultControllerSettings::on_range_change(wxCommandEvent& event) -{ - update_slider_text(event); - - const auto new_value = (float)event.GetInt() / 100.0f; - - if (event.GetEventObject() == m_axis_range) - m_settings.axis.range = new_value; - else if (event.GetEventObject() == m_rotation_range) - m_settings.rotation.range = new_value; - else if (event.GetEventObject() == m_trigger_range) - m_settings.trigger.range = new_value; -} - -void DefaultControllerSettings::on_rumble_change(wxCommandEvent& event) -{ - update_slider_text(event); - - const auto rumble_value = event.GetInt(); - m_settings.rumble = (float)rumble_value / 100.0f; - - m_controller->set_rumble(m_settings.rumble); - if (rumble_value != 0) - m_controller->start_rumble(); - else - m_controller->stop_rumble(); - - m_controller->set_rumble(m_rumble_backup); - - m_rumble_time = std::chrono::steady_clock::now(); -} diff --git a/src/gui/wxgui/input/settings/DefaultControllerSettings.h b/src/gui/wxgui/input/settings/DefaultControllerSettings.h deleted file mode 100644 index a5cb76ef..00000000 --- a/src/gui/wxgui/input/settings/DefaultControllerSettings.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "input/api/Controller.h" - -class wxCheckBox; -class wxInputDraw; - -class DefaultControllerSettings : public wxDialog -{ -public: - DefaultControllerSettings(wxWindow* parent, const wxPoint& position, ControllerPtr controller); - ~DefaultControllerSettings(); - -private: - void update_settings(); - - ControllerPtr m_controller; - ControllerBase::Settings m_settings; - float m_rumble_backup; - - wxTimer* m_timer; - std::optional m_rumble_time{}; - - wxSlider* m_axis_deadzone, *m_axis_range; - wxSlider* m_rotation_deadzone, *m_rotation_range; - wxSlider* m_trigger_deadzone, *m_trigger_range; - wxSlider* m_rumble; - - wxCheckBox* m_use_motion = nullptr; - - wxInputDraw* m_axis_draw, * m_rotation_draw, *m_trigger_draw; - - void on_timer(wxTimerEvent& event); - void on_close(wxCloseEvent& event); - void on_deadzone_change(wxCommandEvent& event); - void on_range_change(wxCommandEvent& event); - void on_rumble_change(wxCommandEvent& event); -}; diff --git a/src/gui/wxgui/input/settings/WiimoteControllerSettings.cpp b/src/gui/wxgui/input/settings/WiimoteControllerSettings.cpp deleted file mode 100644 index 320a895a..00000000 --- a/src/gui/wxgui/input/settings/WiimoteControllerSettings.cpp +++ /dev/null @@ -1,369 +0,0 @@ -#include "wxgui/input/settings/WiimoteControllerSettings.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "wxgui/helpers/wxControlObject.h" -#include "wxgui/helpers/wxHelpers.h" -#include "wxgui/components/wxInputDraw.h" -#include "wxgui/input/InputAPIAddWindow.h" - -#ifdef SUPPORTS_WIIMOTE - -WiimoteControllerSettings::WiimoteControllerSettings(wxWindow* parent, const wxPoint& position, std::shared_ptr controller) - : wxDialog(parent, wxID_ANY, _("Controller settings"), position, wxDefaultSize, - wxDEFAULT_DIALOG_STYLE), m_controller(std::move(controller)) -{ - m_settings = m_controller->get_settings(); - m_rumble_backup = m_settings.rumble; - m_packet_delay_backup = m_controller->get_packet_delay(); - - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - - auto* sizer = new wxBoxSizer(wxVERTICAL); - - // extension info - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Connected extension")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - m_extension_text = new wxStaticText(box, wxID_ANY, _("None")); - box_sizer->Add(m_extension_text, 0, wxALL, 5); - - sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - // options - { - auto* box = new wxStaticBox(this, wxID_ANY, _("Options")); - auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); - - { - auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); - // Rumble - m_rumble = new wxCheckBox(box, wxID_ANY, _("Rumble")); - m_rumble->SetValue(m_settings.rumble > 0); - row_sizer->Add(m_rumble, 0, wxALL, 5); - - m_rumble->Bind(wxEVT_CHECKBOX, &WiimoteControllerSettings::on_rumble_change, this); - - // Motion - m_use_motion = new wxCheckBox(box, wxID_ANY, _("Use motion")); - m_use_motion->SetValue(m_settings.motion); - m_use_motion->Enable(m_controller->has_motion()); - row_sizer->Add(m_use_motion, 0, wxALL, 5); - - box_sizer->Add(row_sizer); - } - { - auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); - - // Delay - row_sizer->Add(new wxStaticText(box, wxID_ANY, _("Packet delay")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_package_delay = new wxSlider(box, wxID_ANY, m_packet_delay_backup, 1, 100); - row_sizer->Add(m_package_delay, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(box, wxID_ANY, wxString::Format("%dms", m_packet_delay_backup)); - row_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_package_delay->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_delay_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - - box_sizer->Add(row_sizer); - } - - sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - // Nunchuck - { - m_nunchuck_settings = new wxStaticBox(this, wxID_ANY, _("Nunchuck")); - auto* box_sizer = new wxStaticBoxSizer(m_nunchuck_settings, wxVERTICAL); - - { - auto* content_sizer = new wxFlexGridSizer(0, 3, 0, 0); - - // Deadzone - const auto deadzone = (int)(m_settings.axis.deadzone * 100); - content_sizer->Add(new wxStaticText(m_nunchuck_settings, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_nunchuck_deadzone = new wxSlider(m_nunchuck_settings, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_nunchuck_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(m_nunchuck_settings, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_nunchuck_deadzone->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - - - // Range - const auto range = (int)(m_settings.axis.range * 100); - content_sizer->Add(new wxStaticText(m_nunchuck_settings, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_nunchuck_range = new wxSlider(m_nunchuck_settings, wxID_ANY, range, 50, 200); - content_sizer->Add(m_nunchuck_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(m_nunchuck_settings, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_nunchuck_range->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - - content_sizer->AddSpacer(1); - m_nunchuck_draw = new wxInputDraw(m_nunchuck_settings, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_nunchuck_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - - box_sizer->Add(content_sizer, 1, wxEXPAND, 0); - } - - sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - // Classic - { - m_classic_settings = new wxStaticBox(this, wxID_ANY, _("Classic")); - auto* box_sizer = new wxStaticBoxSizer(m_classic_settings, wxVERTICAL); - - { - auto* content_sizer = new wxFlexGridSizer(0, 6, 0, 0); - - // Deadzone - { - // Axis - const auto deadzone = (int)(m_settings.axis.deadzone * 100); - content_sizer->Add(new wxStaticText(m_classic_settings, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_classic_axis_deadzone = new wxSlider(m_classic_settings, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_classic_axis_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(m_classic_settings, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_classic_axis_deadzone->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - } - { - // Range - const auto deadzone = (int)(m_settings.rotation.deadzone * 100); - content_sizer->Add(new wxStaticText(m_classic_settings, wxID_ANY, _("Deadzone")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_classic_rotation_deadzone = new wxSlider(m_classic_settings, wxID_ANY, deadzone, 0, 100); - content_sizer->Add(m_classic_rotation_deadzone, 1, wxALL | wxEXPAND, 5); - - const auto deadzone_text = new wxStaticText(m_classic_settings, wxID_ANY, wxString::Format("%d%%", deadzone)); - content_sizer->Add(deadzone_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_classic_rotation_deadzone->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(deadzone_text)); - } - - // Range - { - // Axis - const auto range = (int)(m_settings.axis.range * 100); - content_sizer->Add(new wxStaticText(m_classic_settings, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_classic_axis_range = new wxSlider(m_classic_settings, wxID_ANY, range, 50, 200); - content_sizer->Add(m_classic_axis_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(m_classic_settings, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_classic_axis_range->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - } - { - // Rotation - const auto range = (int)(m_settings.rotation.range * 100); - content_sizer->Add(new wxStaticText(m_classic_settings, wxID_ANY, _("Range")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - m_classic_rotation_range = new wxSlider(m_classic_settings, wxID_ANY, range, 50, 200); - content_sizer->Add(m_classic_rotation_range, 1, wxALL | wxEXPAND, 5); - - const auto range_text = new wxStaticText(m_classic_settings, wxID_ANY, wxString::Format("%d%%", range)); - content_sizer->Add(range_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - m_classic_rotation_range->Bind(wxEVT_SLIDER, &WiimoteControllerSettings::on_slider_change, this, wxID_ANY, wxID_ANY, - new wxControlObject(range_text)); - } - - content_sizer->AddSpacer(1); - m_classic_axis_draw = new wxInputDraw(m_classic_settings, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_classic_axis_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - content_sizer->AddSpacer(1); - - content_sizer->AddSpacer(1); - m_classic_rotation_draw = new wxInputDraw(m_classic_settings, wxID_ANY, wxDefaultPosition, { 60, 60 }); - content_sizer->Add(m_classic_rotation_draw, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER, 5); - - box_sizer->Add(content_sizer, 1, wxEXPAND, 0); - } - - sizer->Add(box_sizer, 0, wxALL | wxEXPAND, 5); - } - - { - auto* control_sizer = new wxFlexGridSizer(0, 4, 0, 0); - control_sizer->AddGrowableCol(3); - - auto* ok_button = new wxButton(this, wxID_ANY, _("OK")); - ok_button->Bind(wxEVT_BUTTON, [this](auto&) { update_settings(); EndModal(wxID_OK); }); - control_sizer->Add(ok_button, 0, wxALL, 5); - - control_sizer->Add(0, 0, 0, wxEXPAND, 5); - - auto* cancel_button = new wxButton(this, wxID_ANY, _("Cancel")); - cancel_button->Bind(wxEVT_BUTTON, [this](auto&) { EndModal(wxID_CANCEL); }); - control_sizer->Add(cancel_button, 0, wxALL, 5); - - auto* calibrate_button = new wxButton(this, wxID_ANY, _("Calibrate")); - calibrate_button->Bind(wxEVT_BUTTON, [this](auto&) { m_controller->calibrate(); }); - control_sizer->Add(calibrate_button, 0, wxALL | wxALIGN_RIGHT, 5); - - sizer->Add(control_sizer, 0, wxEXPAND, 5); - } - - - - this->SetSizer(sizer); - this->Layout(); - this->Fit(); - - this->Bind(wxEVT_CLOSE_WINDOW, &WiimoteControllerSettings::on_close, this); - - m_timer = new wxTimer(this); - Bind(wxEVT_TIMER, &WiimoteControllerSettings::on_timer, this); - m_timer->Start(100); -} - -WiimoteControllerSettings::~WiimoteControllerSettings() -{ - m_controller->stop_rumble(); - m_timer->Stop(); -} - -void WiimoteControllerSettings::update_settings() -{ - // update settings - m_controller->set_settings(m_settings); - if (m_use_motion) - m_controller->set_use_motion(m_use_motion->GetValue()); - - m_controller->set_packet_delay(m_package_delay->GetValue()); -} - -void WiimoteControllerSettings::on_timer(wxTimerEvent& event) -{ - if (m_rumble_time.has_value() && std::chrono::duration_cast(std::chrono::steady_clock::now() - m_rumble_time.value()).count() > 500) - { - m_controller->stop_rumble(); - m_rumble_time = {}; - } - - const auto& default_state = m_controller->get_default_state(); - - auto state = m_controller->raw_state(); - m_controller->apply_axis_setting(state.axis, default_state.axis, m_settings.axis); - m_controller->apply_axis_setting(state.rotation, default_state.rotation, m_settings.rotation); - m_controller->apply_axis_setting(state.trigger, default_state.trigger, m_settings.trigger); - - if (m_nunchuck_settings->IsEnabled()) - { - m_nunchuck_draw->SetDeadzone(m_settings.axis.deadzone); - m_nunchuck_draw->SetAxisValue(state.axis); - } - - if (m_classic_settings->IsEnabled()) - { - m_classic_axis_draw->SetDeadzone(m_settings.axis.deadzone); - m_classic_axis_draw->SetAxisValue(state.axis); - - m_classic_rotation_draw->SetDeadzone(m_settings.rotation.deadzone); - m_classic_rotation_draw->SetAxisValue(state.rotation); - } - - wxString label; - switch (m_controller->get_extension()) - { - case NativeWiimoteController::Nunchuck: - label = _("Nunchuck"); - m_nunchuck_settings->Enable(); - m_classic_settings->Disable(); - break; - case NativeWiimoteController::Classic: - label = _("Classic"); - m_nunchuck_settings->Disable(); - m_classic_settings->Enable(); - break; - default: - m_nunchuck_settings->Disable(); - m_classic_settings->Disable(); - } - - if(m_controller->is_mpls_attached()) - { - const bool empty = label.empty(); - if (!empty) - label.Append(" ("); - - label.Append(_("MotionPlus")); - - if (!empty) - label.Append(")"); - } - - if(label.empty()) - { - label = _("None"); - } - - m_extension_text->SetLabelText(label); -} - -void WiimoteControllerSettings::on_close(wxCloseEvent& event) -{ - if (this->GetReturnCode() == 0 || this->GetReturnCode() == wxID_OK) - update_settings(); - - event.Skip(); -} - -void WiimoteControllerSettings::on_slider_change(wxCommandEvent& event) -{ - update_slider_text(event); - const auto new_value = (float)event.GetInt() / 100.0f; - - auto* obj = event.GetEventObject(); - if (obj == m_nunchuck_deadzone || obj == m_classic_axis_deadzone) - m_settings.axis.deadzone = new_value; - else if (obj == m_nunchuck_range || obj == m_classic_axis_range) - m_settings.axis.range = new_value; - else if (obj == m_classic_rotation_deadzone) - m_settings.rotation.deadzone = new_value; - else if (obj == m_classic_rotation_range) - m_settings.rotation.range = new_value; -} - -void WiimoteControllerSettings::on_rumble_change(wxCommandEvent& event) -{ - const auto rumble_value = m_rumble->GetValue(); - m_settings.rumble = rumble_value ? 1.0f : 0.0f; - - m_controller->set_rumble(m_settings.rumble); - if (rumble_value) - m_controller->start_rumble(); - else - m_controller->stop_rumble(); - - m_controller->set_rumble(m_rumble_backup); - - m_rumble_time = std::chrono::steady_clock::now(); -} - -void WiimoteControllerSettings::on_delay_change(wxCommandEvent& event) -{ - update_slider_text(event, "%dms"); -} - -#endif \ No newline at end of file diff --git a/src/gui/wxgui/input/settings/WiimoteControllerSettings.h b/src/gui/wxgui/input/settings/WiimoteControllerSettings.h deleted file mode 100644 index d5214efe..00000000 --- a/src/gui/wxgui/input/settings/WiimoteControllerSettings.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#ifdef SUPPORTS_WIIMOTE - -#include -#include -#include - -#include "input/api/Controller.h" -#include "input/api/Wiimote/NativeWiimoteController.h" - -class wxStaticBox; -class wxStaticText; -class wxCheckBox; -class wxInputDraw; - -class WiimoteControllerSettings : public wxDialog -{ -public: - WiimoteControllerSettings(wxWindow* parent, const wxPoint& position, std::shared_ptr controller); - ~WiimoteControllerSettings(); - -private: - void update_settings(); - - std::shared_ptr m_controller; - ControllerBase::Settings m_settings; - float m_rumble_backup; - uint32 m_packet_delay_backup; - - wxTimer* m_timer; - std::optional m_rumble_time{}; - - wxStaticText* m_extension_text; - - wxSlider* m_package_delay; - wxCheckBox* m_rumble = nullptr; - wxCheckBox* m_use_motion = nullptr; - - wxStaticBox* m_nunchuck_settings; - wxSlider* m_nunchuck_deadzone, * m_nunchuck_range; - wxInputDraw* m_nunchuck_draw; - - wxStaticBox* m_classic_settings; - wxSlider* m_classic_axis_deadzone, * m_classic_axis_range; - wxInputDraw* m_classic_axis_draw; - wxSlider* m_classic_rotation_deadzone, * m_classic_rotation_range; - wxInputDraw* m_classic_rotation_draw; - - - - - - void on_timer(wxTimerEvent& event); - void on_close(wxCloseEvent& event); - void on_slider_change(wxCommandEvent& event); - void on_rumble_change(wxCommandEvent& event); - void on_delay_change(wxCommandEvent& event); -}; - -#endif \ No newline at end of file diff --git a/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp b/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp deleted file mode 100644 index 5f7da298..00000000 --- a/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp +++ /dev/null @@ -1,466 +0,0 @@ -#include "wxgui/wxgui.h" -#include "Cafe/OS/libs/coreinit/coreinit_Thread.h" -#include "Cafe/OS/libs/coreinit/coreinit_Scheduler.h" -#include "DebugPPCThreadsWindow.h" -#include "Cafe/OS/RPL/rpl.h" -#include "Cafe/OS/RPL/rpl_symbol_storage.h" - -#include "wxgui/components/wxProgressDialogManager.h" - -#include -#include -#include - -enum -{ - // options - REFRESH_ID = wxID_HIGHEST + 1, - AUTO_REFRESH_ID, - CLOSE_ID, - GPLIST_ID, - - // list context menu options - THREADLIST_MENU_BOOST_PRIO_1, - THREADLIST_MENU_BOOST_PRIO_5, - THREADLIST_MENU_DECREASE_PRIO_1, - THREADLIST_MENU_DECREASE_PRIO_5, - THREADLIST_MENU_SUSPEND, - THREADLIST_MENU_RESUME, - THREADLIST_MENU_DUMP_STACK_TRACE, - THREADLIST_MENU_PROFILE_THREAD, -}; - -wxBEGIN_EVENT_TABLE(DebugPPCThreadsWindow, wxFrame) - EVT_BUTTON(CLOSE_ID, DebugPPCThreadsWindow::OnCloseButton) - EVT_BUTTON(REFRESH_ID, DebugPPCThreadsWindow::OnRefreshButton) - EVT_CLOSE(DebugPPCThreadsWindow::OnClose) - wxEND_EVENT_TABLE() - - DebugPPCThreadsWindow::DebugPPCThreadsWindow(wxFrame& parent) - : wxFrame(&parent, wxID_ANY, _("PPC threads"), wxDefaultPosition, wxSize(930, 280), - wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_thread_list = new wxListView(this, GPLIST_ID, wxPoint(0, 0), wxSize(930, 240), wxLC_REPORT); - - m_thread_list->SetFont(wxFont(8, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, "Courier New")); // wxSystemSettings::GetFont(wxSYS_OEM_FIXED_FONT)); - - // add columns - wxListItem col0; - col0.SetId(0); - col0.SetText("Address"); - col0.SetWidth(75); - m_thread_list->InsertColumn(0, col0); - wxListItem col1; - col1.SetId(1); - col1.SetText("Entry"); - col1.SetWidth(75); - m_thread_list->InsertColumn(1, col1); - wxListItem col2; - col2.SetId(2); - col2.SetText("Stack"); - col2.SetWidth(145); - m_thread_list->InsertColumn(2, col2); - wxListItem col3; - col3.SetId(3); - col3.SetText("PC"); - col3.SetWidth(120); - m_thread_list->InsertColumn(3, col3); - wxListItem colLR; - colLR.SetId(4); - colLR.SetText("LR"); - colLR.SetWidth(75); - m_thread_list->InsertColumn(4, colLR); - wxListItem col4; - col4.SetId(5); - col4.SetText("State"); - col4.SetWidth(90); - m_thread_list->InsertColumn(5, col4); - wxListItem col5; - col5.SetId(6); - col5.SetText("Affinity"); - col5.SetWidth(70); - m_thread_list->InsertColumn(6, col5); - wxListItem colPriority; - colPriority.SetId(7); - colPriority.SetText("Priority"); - colPriority.SetWidth(80); - m_thread_list->InsertColumn(7, colPriority); - wxListItem col6; - col6.SetId(8); - col6.SetText("SliceStart"); - col6.SetWidth(110); - m_thread_list->InsertColumn(8, col6); - wxListItem col7; - col7.SetId(9); - col7.SetText("SumWakeTime"); - col7.SetWidth(110); - m_thread_list->InsertColumn(9, col7); - wxListItem col8; - col8.SetId(10); - col8.SetText("ThreadName"); - col8.SetWidth(180); - m_thread_list->InsertColumn(10, col8); - wxListItem col9; - col9.SetId(11); - col9.SetText("GPR"); - col9.SetWidth(180); - m_thread_list->InsertColumn(11, col9); - wxListItem col10; - col10.SetId(12); - col10.SetText("Extra info"); - col10.SetWidth(180); - m_thread_list->InsertColumn(12, col10); - - sizer->Add(m_thread_list, 1, wxEXPAND | wxALL, 5); - - auto* row = new wxBoxSizer(wxHORIZONTAL); - wxButton* button = new wxButton(this, REFRESH_ID, _("Refresh"), wxPoint(0, 0), wxSize(80, 26)); - row->Add(button, 0, wxALL, 5); - - m_auto_refresh = new wxCheckBox(this, AUTO_REFRESH_ID, _("Auto refresh")); - m_auto_refresh->SetValue(true); - row->Add(m_auto_refresh, 0, wxEXPAND | wxALL, 5); - - sizer->Add(row, 0, wxEXPAND | wxALL, 5); - - m_thread_list->Bind(wxEVT_RIGHT_DOWN, &DebugPPCThreadsWindow::OnThreadListRightClick, this); - - SetSizer(sizer); - - RefreshThreadList(); - - m_timer = new wxTimer(this); - this->Bind(wxEVT_TIMER, &DebugPPCThreadsWindow::OnTimer, this); - m_timer->Start(250); -} - -DebugPPCThreadsWindow::~DebugPPCThreadsWindow() -{ - m_timer->Stop(); -} - -void DebugPPCThreadsWindow::OnCloseButton(wxCommandEvent& event) -{ - Close(); -} - -void DebugPPCThreadsWindow::OnRefreshButton(wxCommandEvent& event) -{ - RefreshThreadList(); -} - -void DebugPPCThreadsWindow::OnClose(wxCloseEvent& event) -{ - Close(); -} - -void DebugPPCThreadsWindow::OnTimer(wxTimerEvent& event) -{ - if (m_auto_refresh->IsChecked()) - RefreshThreadList(); -} - -#define _r(__idx) _swapEndianU32(cafeThread->context.gpr[__idx]) - -void DebugPPCThreadsWindow::RefreshThreadList() -{ - wxWindowUpdateLocker lock(m_thread_list); - - long selected_thread = 0; - const int selection = m_thread_list->GetFirstSelected(); - if (selection != wxNOT_FOUND) - selected_thread = m_thread_list->GetItemData(selection); - - const int scrollPos = m_thread_list->GetScrollPos(0); - m_thread_list->DeleteAllItems(); - - if (activeThreadCount > 0) - { - __OSLockScheduler(); - srwlock_activeThreadList.LockWrite(); - for (sint32 i = 0; i < activeThreadCount; i++) - { - MPTR threadItrMPTR = activeThread[i]; - OSThread_t* cafeThread = (OSThread_t*)memory_getPointerFromVirtualOffset(threadItrMPTR); - - wxListItem item; - item.SetId(i); - item.SetText(wxString::Format("%08X", threadItrMPTR)); - m_thread_list->InsertItem(item); - m_thread_list->SetItemData(item, (long)threadItrMPTR); - // entry point - m_thread_list->SetItem(i, 1, wxString::Format("%08X", cafeThread->entrypoint.GetMPTR())); - // stack base (low) - m_thread_list->SetItem(i, 2, wxString::Format("%08X - %08X", cafeThread->stackEnd.GetMPTR(), cafeThread->stackBase.GetMPTR())); - // pc - RPLStoredSymbol* symbol = rplSymbolStorage_getByAddress(cafeThread->context.srr0); - wxString pcLabel; - if (symbol) - pcLabel = wxString::Format("%s (0x%08x)", (const char*)symbol->symbolName, cafeThread->context.srr0); - else - pcLabel = wxString::Format("%08X", cafeThread->context.srr0); - m_thread_list->SetItem(i, 3, pcLabel); - // lr - m_thread_list->SetItem(i, 4, wxString::Format("%08X", _swapEndianU32(cafeThread->context.lr))); - // state - OSThread_t::THREAD_STATE threadState = cafeThread->state; - wxString threadStateStr = "UNDEFINED"; - if (cafeThread->suspendCounter != 0) - threadStateStr = "SUSPENDED"; - else if (threadState == OSThread_t::THREAD_STATE::STATE_NONE) - threadStateStr = "NONE"; - else if (threadState == OSThread_t::THREAD_STATE::STATE_READY) - threadStateStr = "READY"; - else if (threadState == OSThread_t::THREAD_STATE::STATE_RUNNING) - threadStateStr = "RUNNING"; - else if (threadState == OSThread_t::THREAD_STATE::STATE_WAITING) - threadStateStr = "WAITING"; - else if (threadState == OSThread_t::THREAD_STATE::STATE_MORIBUND) - threadStateStr = "MORIBUND"; - m_thread_list->SetItem(i, 5, threadStateStr); - // affinity - uint8 affinity = cafeThread->attr & 7; - uint8 affinityReal = cafeThread->context.affinity; - wxString affinityLabel; - if (affinity != affinityReal) - affinityLabel = wxString::Format("(!) %d%d%d real: %d%d%d", (affinity >> 0) & 1, (affinity >> 1) & 1, (affinity >> 2) & 1, (affinityReal >> 0) & 1, (affinityReal >> 1) & 1, (affinityReal >> 2) & 1); - else - affinityLabel = wxString::Format("%d%d%d", (affinity >> 0) & 1, (affinity >> 1) & 1, (affinity >> 2) & 1); - m_thread_list->SetItem(i, 6, affinityLabel); - // priority - sint32 effectivePriority = cafeThread->effectivePriority; - m_thread_list->SetItem(i, 7, wxString::Format("%d", effectivePriority)); - // last awake in cycles - uint64 lastWakeUpTime = cafeThread->wakeUpTime; - m_thread_list->SetItem(i, 8, wxString::Format("%" PRIu64, lastWakeUpTime)); - // awake time in cycles - uint64 awakeTime = cafeThread->totalCycles; - m_thread_list->SetItem(i, 9, wxString::Format("%" PRIu64, awakeTime)); - // thread name - const char* threadName = "NULL"; - if (!cafeThread->threadName.IsNull()) - threadName = cafeThread->threadName.GetPtr(); - m_thread_list->SetItem(i, 10, threadName); - // GPR - m_thread_list->SetItem(i, 11, wxString::Format("r3 %08x r4 %08x r5 %08x r6 %08x r7 %08x", _r(3), _r(4), _r(5), _r(6), _r(7))); - // waiting condition / extra info - coreinit::OSMutex* mutex = cafeThread->waitingForMutex; - wxString extraInfoLabel; - if (mutex) - extraInfoLabel = wxString::Format("Mutex 0x%08x (Held by thread 0x%08X Lock-Count: %d)", memory_getVirtualOffsetFromPointer(mutex), mutex->owner.GetMPTR(), (uint32)mutex->lockCount); - - // OSSetThreadCancelState - if (cafeThread->requestFlags & OSThread_t::REQUEST_FLAG_CANCEL) - extraInfoLabel += "[Cancel requested]"; - - m_thread_list->SetItem(i, 12, extraInfoLabel); - - if (selected_thread != 0 && selected_thread == (long)threadItrMPTR) - { - m_thread_list->Select(i); - m_thread_list->Focus(i); - } - } - srwlock_activeThreadList.UnlockWrite(); - __OSUnlockScheduler(); - } - - m_thread_list->SetScrollPos(0, scrollPos, true); -} - -void DebugPPCThreadsWindow::DumpStackTrace(OSThread_t* thread) -{ - cemuLog_log(LogType::Force, "Dumping stack trace for thread {0:08x} LR: {1:08x}", memory_getVirtualOffsetFromPointer(thread), _swapEndianU32(thread->context.lr)); - DebugLogStackTrace(thread, _swapEndianU32(thread->context.gpr[1])); -} - -void DebugPPCThreadsWindow::PresentProfileResults(OSThread_t* thread, const std::unordered_map& samples) -{ - std::vector> sortedSamples; - // count samples - uint32 totalSampleCount = 0; - for (auto& sample : samples) - totalSampleCount += sample.second; - cemuLog_log(LogType::Force, "--- Thread {:08x} profile results with {:} samples captured ---", - MEMPTR(thread).GetMPTR(), totalSampleCount); - cemuLog_log(LogType::Force, "Exclusive time, grouped by function:"); - // print samples grouped by function - sortedSamples.clear(); - for (auto& sample : samples) - { - RPLStoredSymbol* symbol = rplSymbolStorage_getByClosestAddress(sample.first); - VAddr sampleAddr = sample.first; - if (symbol) - sampleAddr = symbol->address; - auto it = std::find_if(sortedSamples.begin(), sortedSamples.end(), - [sampleAddr](const std::pair& a) { return a.first == sampleAddr; }); - if (it != sortedSamples.end()) - it->second += sample.second; - else - sortedSamples.push_back(std::make_pair(sampleAddr, sample.second)); - } - std::sort(sortedSamples.begin(), sortedSamples.end(), - [](const std::pair& a, const std::pair& b) { return a.second > b.second; }); - for (auto& sample : sortedSamples) - { - if (sample.second < 3) - continue; - VAddr sampleAddr = sample.first; - RPLStoredSymbol* symbol = rplSymbolStorage_getByClosestAddress(sample.first); - std::string strName; - if (symbol) - { - strName = fmt::format("{}.{}+0x{:x}", (const char*)symbol->libName, (const char*)symbol->symbolName, - sampleAddr - symbol->address); - } - else - strName = "Unknown"; - cemuLog_log(LogType::Force, "[{:08x}] {:8.2f}% (Samples: {:5}) Symbol: {}", sample.first, - (double)(sample.second * 100) / (double)totalSampleCount, sample.second, strName); - } -} - -void DebugPPCThreadsWindow::ProfileThreadWorker(OSThread_t* thread) -{ - wxProgressDialogManager progressDialog(this); - progressDialog.Create(_("Profiling thread"), - _("Capturing samples..."), - 1000, // range - wxPD_CAN_SKIP); - - std::unordered_map samples; - // loop for one minute - uint64 startTime = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - uint32 totalSampleCount = 0; - while (true) - { - // suspend thread - coreinit::OSSuspendThread(thread); - // wait until thread is not running anymore - __OSLockScheduler(); - while (coreinit::OSIsThreadRunningNoLock(thread)) - { - __OSUnlockScheduler(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - __OSLockScheduler(); - } - uint32 sampleIP = thread->context.srr0; - __OSUnlockScheduler(); - coreinit::OSResumeThread(thread); - // count sample - samples[sampleIP]++; - totalSampleCount++; - if ((totalSampleCount % 50) == 0) - { - wxString msg = formatWxString(_("Capturing samples... ({:})\nResults will be written to log.txt\n"), totalSampleCount); - if (totalSampleCount < 30000) - msg.Append(_("Click Skip button for early results with lower accuracy")); - else - msg.Append(_("Click Skip button to finish")); - progressDialog.Update(totalSampleCount * 1000 / 30000, msg); - if (progressDialog.IsCancelledOrSkipped()) - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - PresentProfileResults(thread, samples); - progressDialog.Destroy(); -} - -void DebugPPCThreadsWindow::ProfileThread(OSThread_t* thread) -{ - std::thread profileThread(&DebugPPCThreadsWindow::ProfileThreadWorker, this, thread); - profileThread.detach(); -} - -void DebugPPCThreadsWindow::OnThreadListPopupClick(wxCommandEvent& evt) -{ - MPTR threadMPTR = (MPTR)(size_t)static_cast(evt.GetEventObject())->GetClientData(); - OSThread_t* osThread = (OSThread_t*)memory_getPointerFromVirtualOffset(threadMPTR); - __OSLockScheduler(); - if (!coreinit::__OSIsThreadActive(osThread)) - { - __OSUnlockScheduler(); - return; - } - __OSUnlockScheduler(); - // handle command - switch (evt.GetId()) - { - case THREADLIST_MENU_BOOST_PRIO_5: - osThread->basePriority = osThread->basePriority - 5; - break; - case THREADLIST_MENU_BOOST_PRIO_1: - osThread->basePriority = osThread->basePriority - 1; - break; - case THREADLIST_MENU_DECREASE_PRIO_5: - osThread->basePriority = osThread->basePriority + 5; - break; - case THREADLIST_MENU_DECREASE_PRIO_1: - osThread->basePriority = osThread->basePriority + 1; - break; - case THREADLIST_MENU_SUSPEND: - coreinit::OSSuspendThread(osThread); - break; - case THREADLIST_MENU_RESUME: - coreinit::OSResumeThread(osThread); - break; - case THREADLIST_MENU_DUMP_STACK_TRACE: - DumpStackTrace(osThread); - break; - case THREADLIST_MENU_PROFILE_THREAD: - ProfileThread(osThread); - break; - } - coreinit::__OSUpdateThreadEffectivePriority(osThread); - // update thread list - RefreshThreadList(); -} - -void DebugPPCThreadsWindow::OnThreadListRightClick(wxMouseEvent& event) -{ - // Get the item index - int hitTestFlag; - int itemIndex = m_thread_list->HitTest(event.GetPosition(), hitTestFlag); - if (itemIndex == wxNOT_FOUND) - return; - // select item - m_thread_list->Focus(itemIndex); - long sel = m_thread_list->GetFirstSelected(); - if (sel != wxNOT_FOUND) - m_thread_list->Select(sel, false); - m_thread_list->Select(itemIndex); - // check if thread is still on the list of active threads - MPTR threadMPTR = (MPTR)m_thread_list->GetItemData(itemIndex); - __OSLockScheduler(); - if (!coreinit::__OSIsThreadActive(MEMPTR(threadMPTR))) - { - __OSUnlockScheduler(); - return; - } - __OSUnlockScheduler(); - // create menu entry - wxMenu menu; - menu.SetClientData((void*)(size_t)threadMPTR); - menu.Append(THREADLIST_MENU_BOOST_PRIO_5, _("Boost priority (-5)")); - menu.Append(THREADLIST_MENU_BOOST_PRIO_1, _("Boost priority (-1)")); - menu.AppendSeparator(); - menu.Append(THREADLIST_MENU_DECREASE_PRIO_5, _("Decrease priority (+5)")); - menu.Append(THREADLIST_MENU_DECREASE_PRIO_1, _("Decrease priority (+1)")); - menu.AppendSeparator(); - menu.Append(THREADLIST_MENU_RESUME, _("Resume")); - menu.Append(THREADLIST_MENU_SUSPEND, _("Suspend")); - menu.AppendSeparator(); - menu.Append(THREADLIST_MENU_DUMP_STACK_TRACE, _("Write stack trace to log")); - menu.Append(THREADLIST_MENU_PROFILE_THREAD, _("Profile thread")); - menu.Bind(wxEVT_COMMAND_MENU_SELECTED, &DebugPPCThreadsWindow::OnThreadListPopupClick, this); - PopupMenu(&menu); -} - -void DebugPPCThreadsWindow::Close() -{ - this->Destroy(); -} diff --git a/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.h b/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.h deleted file mode 100644 index 5b502e32..00000000 --- a/src/gui/wxgui/windows/PPCThreadsViewer/DebugPPCThreadsWindow.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -class wxListView; - -class DebugPPCThreadsWindow: public wxFrame -{ -public: - DebugPPCThreadsWindow(wxFrame& parent); - ~DebugPPCThreadsWindow(); - - void OnCloseButton(wxCommandEvent& event); - void OnRefreshButton(wxCommandEvent& event); - void OnClose(wxCloseEvent& event); - void RefreshThreadList(); - void OnThreadListPopupClick(wxCommandEvent &evt); - void OnThreadListRightClick(wxMouseEvent& event); - - void Close(); - -private: - void ProfileThread(struct OSThread_t* thread); - void ProfileThreadWorker(OSThread_t* thread); - void PresentProfileResults(OSThread_t* thread, const std::unordered_map& samples); - void DumpStackTrace(struct OSThread_t* thread); - - wxListView* m_thread_list; - wxCheckBox* m_auto_refresh; - wxTimer* m_timer; - - void OnTimer(wxTimerEvent& event); - - wxDECLARE_EVENT_TABLE(); - - -}; diff --git a/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.cpp b/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.cpp deleted file mode 100644 index b49adb14..00000000 --- a/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.cpp +++ /dev/null @@ -1,377 +0,0 @@ -#include "wxgui/wxgui.h" -#include "wxHelper.h" -#include "TextureRelationWindow.h" -#include "Cafe/HW/Latte/Core/LatteTexture.h" - -enum -{ - // options - REFRESH_ID, - CLOSE_ID, - TEX_LIST_A_ID, - TEX_LIST_B_ID, - CHECKBOX_SHOW_ONLY_ACTIVE, - CHECKBOX_SHOW_VIEWS, -}; - -wxBEGIN_EVENT_TABLE(TextureRelationViewerWindow, wxFrame) -EVT_BUTTON(CLOSE_ID, TextureRelationViewerWindow::OnCloseButton) -EVT_BUTTON(REFRESH_ID, TextureRelationViewerWindow::OnRefreshButton) -EVT_CHECKBOX(CHECKBOX_SHOW_ONLY_ACTIVE, TextureRelationViewerWindow::OnCheckbox) -EVT_CHECKBOX(CHECKBOX_SHOW_VIEWS, TextureRelationViewerWindow::OnCheckbox) - -EVT_CLOSE(TextureRelationViewerWindow::OnClose) -wxEND_EVENT_TABLE() - -wxListCtrl* textureRelationListA; -bool isTextureViewerOpen = false; - -void openTextureViewer(wxFrame& parentFrame) -{ - if (isTextureViewerOpen) - return; - auto frame = new TextureRelationViewerWindow(parentFrame); - frame->Show(true); -} - -TextureRelationViewerWindow::TextureRelationViewerWindow(wxFrame& parent) - : wxFrame(&parent, wxID_ANY, _("Texture cache"), wxDefaultPosition, wxSize(1000, 480), wxCLOSE_BOX | wxCLIP_CHILDREN | wxCAPTION | wxRESIZE_BORDER) -{ - isTextureViewerOpen = true; - - this->showOnlyActive = false; - this->showTextureViews = true; - - wxPanel* mainPane = new wxPanel(this); - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - textureRelationListA = new wxListCtrl(mainPane, TEX_LIST_A_ID, wxPoint(0, 0), wxSize(1008, 440), wxLC_REPORT); - - textureRelationListA->SetFont(wxFont(8, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, "Courier New"));//wxSystemSettings::GetFont(wxSYS_OEM_FIXED_FONT)); - - // add columns - wxListItem colType; - sint32 columnIndex = 0; - colType.SetId(columnIndex); columnIndex++; - colType.SetText("Type"); - colType.SetWidth(85); - textureRelationListA->InsertColumn(columnIndex-1, colType); - wxListItem colPhysAddr; - colPhysAddr.SetId(columnIndex); columnIndex++; - colPhysAddr.SetText("PhysAddr"); - colPhysAddr.SetWidth(80); - textureRelationListA->InsertColumn(columnIndex-1, colPhysAddr); - wxListItem colPhysMipAddr; - colPhysMipAddr.SetId(columnIndex); columnIndex++; - colPhysMipAddr.SetText("MipPAddr"); - colPhysMipAddr.SetWidth(80); - textureRelationListA->InsertColumn(columnIndex-1, colPhysMipAddr); - wxListItem colDim; - colDim.SetId(columnIndex); columnIndex++; - colDim.SetText("Dim"); - colDim.SetWidth(80); - textureRelationListA->InsertColumn(columnIndex-1, colDim); - wxListItem colResolution; - colResolution.SetId(columnIndex); columnIndex++; - colResolution.SetText("Resolution"); - colResolution.SetWidth(110); - textureRelationListA->InsertColumn(columnIndex-1, colResolution); - wxListItem colFormat; - colFormat.SetId(columnIndex); columnIndex++; - colFormat.SetText("Format"); - colFormat.SetWidth(70); - textureRelationListA->InsertColumn(columnIndex-1, colFormat); - wxListItem colPitch; - colPitch.SetId(columnIndex); columnIndex++; - colPitch.SetText("Pitch"); - colPitch.SetWidth(80); - textureRelationListA->InsertColumn(columnIndex-1, colPitch); - wxListItem colTilemode; - colTilemode.SetId(columnIndex); columnIndex++; - colTilemode.SetText("Tilemode"); - colTilemode.SetWidth(80); - textureRelationListA->InsertColumn(columnIndex-1, colTilemode); - wxListItem colSliceRange; - colSliceRange.SetId(columnIndex); columnIndex++; - colSliceRange.SetText("SliceRange"); - colSliceRange.SetWidth(90); - textureRelationListA->InsertColumn(columnIndex-1, colSliceRange); - wxListItem colMipRange; - colMipRange.SetId(columnIndex); columnIndex++; - colMipRange.SetText("MipRange"); - colMipRange.SetWidth(90); - textureRelationListA->InsertColumn(columnIndex-1, colMipRange); - wxListItem colAge; - colAge.SetId(columnIndex); columnIndex++; - colAge.SetText("Last access"); - colAge.SetWidth(90); - textureRelationListA->InsertColumn(columnIndex - 1, colAge); - wxListItem colOverwriteRes; - colOverwriteRes.SetId(columnIndex); columnIndex++; - colOverwriteRes.SetText("OverwriteRes"); - colOverwriteRes.SetWidth(110); - textureRelationListA->InsertColumn(columnIndex - 1, colOverwriteRes); - - wxBoxSizer* sizerBottom = new wxBoxSizer(wxHORIZONTAL); - - sizer->Add(textureRelationListA, 1, wxEXPAND | wxBOTTOM, 0); - wxButton* button = new wxButton(mainPane, REFRESH_ID, _("Refresh")); - sizerBottom->Add(button, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM | wxTOP | wxLEFT, 10); - - wxCheckBox* checkboxShowOnlyActive = new wxCheckBox(mainPane, CHECKBOX_SHOW_ONLY_ACTIVE, _("Show only active")); - sizerBottom->Add(checkboxShowOnlyActive, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM | wxTOP | wxLEFT, 10); - - wxCheckBox* checkboxShowViews = new wxCheckBox(mainPane, CHECKBOX_SHOW_VIEWS, _("Show views")); - sizerBottom->Add(checkboxShowViews, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM | wxTOP | wxLEFT, 10); - checkboxShowViews->SetValue(true); - - textureRelationListA->Bind(wxEVT_RIGHT_DOWN, &TextureRelationViewerWindow::OnTextureListRightClick, this); - - sizer->Add( - sizerBottom, - 0, // vertically unstretchable - wxALIGN_LEFT); - mainPane->SetSizer(sizer); - - RefreshTextureList(); -} - -TextureRelationViewerWindow::~TextureRelationViewerWindow() -{ - isTextureViewerOpen = false; -} - -void TextureRelationViewerWindow::OnCloseButton(wxCommandEvent& event) -{ - Close(); -} - -void TextureRelationViewerWindow::OnRefreshButton(wxCommandEvent& event) -{ - RefreshTextureList(); -} - -void TextureRelationViewerWindow::OnCheckbox(wxCommandEvent& event) -{ - if (event.GetId() == CHECKBOX_SHOW_ONLY_ACTIVE) - { - showOnlyActive = event.IsChecked(); - RefreshTextureList(); - } - else if (event.GetId() == CHECKBOX_SHOW_VIEWS) - { - showTextureViews = event.IsChecked(); - RefreshTextureList(); - } -} - -void TextureRelationViewerWindow::OnClose(wxCloseEvent& event) -{ - Close(); -} - -void TextureRelationViewerWindow::_setTextureRelationListItemTexture(wxListCtrl* uiList, sint32 rowIndex, struct LatteTextureInformation* texInfo) -{ - wxString typeLabel; - // count number of alternative views for base view - sint32 alternativeViewCount = texInfo->alternativeViewCount; - if (texInfo->isUpdatedOnGPU) - typeLabel = "TEXTURE*"; - else - typeLabel = "TEXTURE"; - if (alternativeViewCount > 0) - { - typeLabel += wxString::Format("(%d)", alternativeViewCount + 1); - } - wxListItem item; - item.SetId(rowIndex); - item.SetText(typeLabel); - item.SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - uiList->InsertItem(item); - - sint32 columnIndex = 1; - // phys address - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%08X", texInfo->physAddress)); - columnIndex++; - // phys mip address - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%08X", texInfo->physMipAddress)); - columnIndex++; - // dim - wxString dimLabel; - if (texInfo->dim == Latte::E_DIM::DIM_2D) - dimLabel = "2D"; - else if (texInfo->dim == Latte::E_DIM::DIM_2D_ARRAY) - dimLabel = "2D_ARRAY"; - else if (texInfo->dim == Latte::E_DIM::DIM_3D) - dimLabel = "3D"; - else if (texInfo->dim == Latte::E_DIM::DIM_CUBEMAP) - dimLabel = "CUBEMAP"; - else if (texInfo->dim == Latte::E_DIM::DIM_1D) - dimLabel = "1D"; - else if (texInfo->dim == Latte::E_DIM::DIM_2D_MSAA) - dimLabel = "2D_MSAA"; - else if (texInfo->dim == Latte::E_DIM::DIM_2D_ARRAY_MSAA) - dimLabel = "2D_MS_ARRAY"; - else - dimLabel = "UKN"; - uiList->SetItem(rowIndex, columnIndex, dimLabel); - columnIndex++; - // resolution - wxString resolutionLabel; - if (texInfo->depth == 1) - resolutionLabel = wxString::Format("%dx%d", texInfo->width, texInfo->height); - else - resolutionLabel = wxString::Format("%dx%dx%d", texInfo->width, texInfo->height, texInfo->depth); - uiList->SetItem(rowIndex, columnIndex, resolutionLabel); - columnIndex++; - // format - wxString formatLabel; - if(texInfo->isDepth) - formatLabel = wxString::Format("%04x(d)", (uint32)texInfo->format); - else - formatLabel = wxString::Format("%04x", (uint32)texInfo->format); - uiList->SetItem(rowIndex, columnIndex, formatLabel); - columnIndex++; - // pitch - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%d", texInfo->pitch)); - columnIndex++; - // tilemode - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%d", (int)texInfo->tileMode)); - columnIndex++; - // sliceRange - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // mipRange - uiList->SetItem(rowIndex, columnIndex, texInfo->mipLevels == 1 ? "1 mip" : wxString::Format("%d mips", texInfo->mipLevels)); - columnIndex++; - // last access - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%uus", (GetTickCount() - texInfo->lastAccessTick + 499) / 1000)); - columnIndex++; - // overwrite resolution - wxString overwriteResLabel; - if (texInfo->overwriteInfo.hasResolutionOverwrite) - { - if(texInfo->overwriteInfo.depth != 1 || texInfo->depth != 1) - overwriteResLabel = wxString::Format("%dx%dx%d", texInfo->overwriteInfo.width, texInfo->overwriteInfo.height, texInfo->overwriteInfo.depth); - else - overwriteResLabel = wxString::Format("%dx%d", texInfo->overwriteInfo.width, texInfo->overwriteInfo.height); - } - uiList->SetItem(rowIndex, columnIndex, overwriteResLabel); - columnIndex++; -} - -void TextureRelationViewerWindow::_setTextureRelationListItemView(wxListCtrl* uiList, sint32 rowIndex, struct LatteTextureInformation* texInfo, struct LatteTextureViewInformation* viewInfo) -{ - // count number of alternative views - sint32 alternativeViewCount = 0; // todo - // set type string - // find and handle highlight entry - wxListItem item; - item.SetId(rowIndex); - item.SetText(alternativeViewCount == 0 ? "> VIEW" : wxString::Format("> VIEW(%d)", alternativeViewCount+1)); - item.SetBackgroundColour(wxHelper::CalculateAccentColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW))); - uiList->InsertItem(item); - //uiList->SetItemPtrData(item, (wxUIntPtr)viewInfo); - sint32 columnIndex = 1; - // phys address - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // phys mip address - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // dim - wxString dimLabel; - if (viewInfo->dim == Latte::E_DIM::DIM_2D) - dimLabel = "2D"; - else if (viewInfo->dim == Latte::E_DIM::DIM_2D_ARRAY) - dimLabel = "2D_ARRAY"; - else if (viewInfo->dim == Latte::E_DIM::DIM_3D) - dimLabel = "3D"; - else if (viewInfo->dim == Latte::E_DIM::DIM_CUBEMAP) - dimLabel = "CUBEMAP"; - else if (viewInfo->dim == Latte::E_DIM::DIM_1D) - dimLabel = "1D"; - else if (viewInfo->dim == Latte::E_DIM::DIM_2D_MSAA) - dimLabel = "2D_MSAA"; - else if (viewInfo->dim == Latte::E_DIM::DIM_2D_ARRAY_MSAA) - dimLabel = "2D_ARRAY_MSAA"; - else - dimLabel = "UKN"; - uiList->SetItem(rowIndex, columnIndex, dimLabel); - columnIndex++; - // resolution - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // format - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%04x", (uint32)viewInfo->format)); - columnIndex++; - // pitch - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // tilemode - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; - // sliceRange - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%d-%d", viewInfo->firstSlice, viewInfo->firstSlice+ viewInfo->numSlice-1)); - columnIndex++; - // mipRange - uiList->SetItem(rowIndex, columnIndex, wxString::Format("%d-%d", viewInfo->firstMip, viewInfo->firstMip + viewInfo->numMip - 1)); - columnIndex++; - // last access - uiList->SetItem(rowIndex, columnIndex, ""); - columnIndex++; -} - -void TextureRelationViewerWindow::RefreshTextureList() -{ - int scrollPos = textureRelationListA->GetScrollPos(wxVERTICAL); - textureRelationListA->DeleteAllItems(); - - std::vector texCache = LatteTexture_QueryCacheInfo(); - - // sort by physAddr in ascending order - for (sint32 i1 = 0; i1 < texCache.size(); i1++) - { - for (sint32 i2 = i1+1; i2 < texCache.size(); i2++) - { - if (texCache[i1].physAddress > texCache[i2].physAddress) - { - std::swap(texCache[i1], texCache[i2]); - } - } - } - - textureRelationListA->Freeze(); - - sint32 rowIndex = 0; - uint32 currentTick = GetTickCount(); - for (auto& tex : texCache) - { - uint32 timeSinceLastAccess = currentTick - tex.lastAccessTick; - if (showOnlyActive && timeSinceLastAccess > 3000) - continue; // hide textures which haven't been updated in more than 3 seconds - - _setTextureRelationListItemTexture(textureRelationListA, rowIndex, &tex); - rowIndex++; - if (showTextureViews) - { - for (auto& view : tex.views) - { - _setTextureRelationListItemView(textureRelationListA, rowIndex, &tex, &view); - rowIndex++; - } - } - } - textureRelationListA->Thaw(); - long itemCount = textureRelationListA->GetItemCount(); - if (itemCount > 0) - textureRelationListA->EnsureVisible(std::min(itemCount - 1, scrollPos + textureRelationListA->GetCountPerPage() - 1)); -} - -void TextureRelationViewerWindow::OnTextureListRightClick(wxMouseEvent& event) -{ -} - -void TextureRelationViewerWindow::Close() -{ - this->Destroy(); -} diff --git a/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.h b/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.h deleted file mode 100644 index f429a0ed..00000000 --- a/src/gui/wxgui/windows/TextureRelationViewer/TextureRelationWindow.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -class wxListCtrl; - -class TextureRelationViewerWindow : public wxFrame -{ -public: - TextureRelationViewerWindow(wxFrame& parent); - ~TextureRelationViewerWindow(); - - void OnCloseButton(wxCommandEvent& event); - void OnRefreshButton(wxCommandEvent& event); - void OnCheckbox(wxCommandEvent& event); - void OnClose(wxCloseEvent& event); - void RefreshTextureList(); - void OnTextureListRightClick(wxMouseEvent& event); - - void Close(); - -private: - - wxDECLARE_EVENT_TABLE(); - - void _setTextureRelationListItemTexture(wxListCtrl* uiList, sint32 rowIndex, struct LatteTextureInformation* texInfo); - void _setTextureRelationListItemView(wxListCtrl* uiList, sint32 rowIndex, struct LatteTextureInformation* texInfo, struct LatteTextureViewInformation* viewInfo); - - bool showOnlyActive; - bool showTextureViews; -}; - -void openTextureViewer(wxFrame& parentFrame); diff --git a/src/gui/wxgui/wxCemuConfig.cpp b/src/gui/wxgui/wxCemuConfig.cpp deleted file mode 100644 index 3795c3cf..00000000 --- a/src/gui/wxgui/wxCemuConfig.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "wxCemuConfig.h" -#include "Common/precompiled.h" -#include "config/CemuConfig.h" -#include "config/XMLConfig.h" -#include "util/helpers/helpers.h" -#include - -XMLWxCemuConfig_t g_wxConfig(&GetConfigHandle); - -void wxCemuConfig::AddRecentlyLaunchedFile(std::string_view file) -{ - recent_launch_files.insert(recent_launch_files.begin(), std::string(file)); - RemoveDuplicatesKeepOrder(recent_launch_files); - while (recent_launch_files.size() > kMaxRecentEntries) - recent_launch_files.pop_back(); -} - -void wxCemuConfig::AddRecentNfcFile(std::string_view file) -{ - recent_nfc_files.insert(recent_nfc_files.begin(), std::string(file)); - RemoveDuplicatesKeepOrder(recent_nfc_files); - while (recent_nfc_files.size() > kMaxRecentEntries) - recent_nfc_files.pop_back(); -} - -void wxCemuConfig::Load(XMLConfigParser& parser) -{ - language = parser.get("language", wxLANGUAGE_DEFAULT); - msw_theme = parser.get("msw_theme", msw_theme); - use_discord_presence = parser.get("use_discord_presence", true); - fullscreen_menubar = parser.get("fullscreen_menubar", false); - feral_gamemode = parser.get("feral_gamemode", false); - check_update = parser.get("check_update", check_update); - receive_untested_updates = parser.get("receive_untested_updates", receive_untested_updates); - save_screenshot = parser.get("save_screenshot", save_screenshot); - did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning); - did_show_graphic_pack_download = parser.get("gp_download", did_show_graphic_pack_download); - did_show_macos_disclaimer = parser.get("macos_disclaimer", did_show_macos_disclaimer); - fullscreen = parser.get("fullscreen", fullscreen); - - window_position.x = parser.get("window_position").get("x", -1); - window_position.y = parser.get("window_position").get("y", -1); - - window_size.x = parser.get("window_size").get("x", -1); - window_size.y = parser.get("window_size").get("y", -1); - window_maximized = parser.get("window_maximized", false); - - pad_open = parser.get("open_pad", false); - pad_position.x = parser.get("pad_position").get("x", -1); - pad_position.y = parser.get("pad_position").get("y", -1); - - pad_size.x = parser.get("pad_size").get("x", -1); - pad_size.y = parser.get("pad_size").get("y", -1); - pad_maximized = parser.get("pad_maximized", false); - - auto gamelist = parser.get("GameList"); - game_list_style = gamelist.get("style", 0); - game_list_column_order = gamelist.get("order", ""); - - show_icon_column = parser.get("show_icon_column", true); - - // return default width if value in config file out of range - auto loadColumnSize = [&gamelist](const char* name, uint32 defaultWidth) { - sint64 val = gamelist.get(name, DefaultColumnSize::name); - if (val < 0 || val > (sint64)std::numeric_limits::max) - return defaultWidth; - return static_cast(val); - }; - column_width.name = loadColumnSize("name_width", DefaultColumnSize::name); - column_width.version = loadColumnSize("version_width", DefaultColumnSize::version); - column_width.dlc = loadColumnSize("dlc_width", DefaultColumnSize::dlc); - column_width.game_time = loadColumnSize("game_time_width", DefaultColumnSize::game_time); - column_width.game_started = loadColumnSize("game_started_width", DefaultColumnSize::game_started); - column_width.region = loadColumnSize("region_width", DefaultColumnSize::region); - column_width.title_id = loadColumnSize("title_id", DefaultColumnSize::title_id); - - recent_launch_files.clear(); - auto launch_parser = parser.get("RecentLaunchFiles"); - for (auto element = launch_parser.get("Entry"); element.valid(); element = launch_parser.get("Entry", element)) - { - const std::string path = element.value(""); - if (path.empty()) - continue; - - try - { - recent_launch_files.emplace_back(path); - } catch (const std::exception&) - { - cemuLog_log(LogType::Force, "config load error: can't load recently launched game file: {}", path); - } - } - - recent_nfc_files.clear(); - auto nfc_parser = parser.get("RecentNFCFiles"); - for (auto element = nfc_parser.get("Entry"); element.valid(); element = nfc_parser.get("Entry", element)) - { - const std::string path = element.value(""); - if (path.empty()) - continue; - try - { - recent_nfc_files.emplace_back(path); - } catch (const std::exception&) - { - cemuLog_log(LogType::Force, "config load error: can't load recently launched nfc file: {}", path); - } - } - - // hotkeys - auto xml_hotkeys = parser.get("Hotkeys"); - hotkeys.modifiers = xml_hotkeys.get("modifiers", sHotkeyCfg{}); - hotkeys.exitFullscreen = xml_hotkeys.get("ExitFullscreen", sHotkeyCfg{uKeyboardHotkey{WXK_ESCAPE}}); - hotkeys.toggleFullscreen = xml_hotkeys.get("ToggleFullscreen", sHotkeyCfg{uKeyboardHotkey{WXK_F11}}); - hotkeys.toggleFullscreenAlt = xml_hotkeys.get("ToggleFullscreenAlt", sHotkeyCfg{uKeyboardHotkey{WXK_CONTROL_M, true}}); // ALT+ENTER - hotkeys.takeScreenshot = xml_hotkeys.get("TakeScreenshot", sHotkeyCfg{uKeyboardHotkey{WXK_F12}}); - hotkeys.toggleFastForward = xml_hotkeys.get("ToggleFastForward", sHotkeyCfg{}); - hotkeys.exitApplication = xml_hotkeys.get("ExitApplication", sHotkeyCfg{}); -#ifdef CEMU_DEBUG_ASSERT - hotkeys.endEmulation = xml_hotkeys.get("EndEmulation", sHotkeyCfg{uKeyboardHotkey{WXK_F5}}); -#endif -} - -void wxCemuConfig::Save(XMLConfigParser& config) -{ - // general settings - config.set("language", language); - config.set("msw_theme", msw_theme); - config.set("use_discord_presence", use_discord_presence); - config.set("fullscreen_menubar", fullscreen_menubar); - config.set("feral_gamemode", feral_gamemode); - config.set("check_update", check_update); - config.set("receive_untested_updates", receive_untested_updates); - config.set("save_screenshot", save_screenshot); - config.set("vk_warning", did_show_vulkan_warning); - config.set("gp_download", did_show_graphic_pack_download); - config.set("macos_disclaimer", did_show_macos_disclaimer); - config.set("fullscreen", fullscreen); - - auto wpos = config.set("window_position"); - wpos.set("x", window_position.x); - wpos.set("y", window_position.y); - auto wsize = config.set("window_size"); - wsize.set("x", window_size.x); - wsize.set("y", window_size.y); - config.set("window_maximized", window_maximized); - - config.set("open_pad", pad_open); - auto ppos = config.set("pad_position"); - ppos.set("x", pad_position.x); - ppos.set("y", pad_position.y); - auto psize = config.set("pad_size"); - psize.set("x", pad_size.x); - psize.set("y", pad_size.y); - config.set("pad_maximized", pad_maximized); - config.set("show_icon_column", show_icon_column); - - auto gamelist = config.set("GameList"); - gamelist.set("style", game_list_style); - gamelist.set("order", game_list_column_order); - gamelist.set("name_width", column_width.name); - gamelist.set("version_width", column_width.version); - gamelist.set("dlc_width", column_width.dlc); - gamelist.set("game_time_width", column_width.game_time); - gamelist.set("game_started_width", column_width.game_started); - gamelist.set("region_width", column_width.region); - gamelist.set("title_id", column_width.title_id); - - auto launch_files_parser = config.set("RecentLaunchFiles"); - for (const auto& entry : recent_launch_files) - { - launch_files_parser.set("Entry", entry.c_str()); - } - - auto nfc_files_parser = config.set("RecentNFCFiles"); - for (const auto& entry : recent_nfc_files) - { - nfc_files_parser.set("Entry", entry.c_str()); - } - - // hotkeys - auto xml_hotkeys = config.set("Hotkeys"); - xml_hotkeys.set("modifiers", hotkeys.modifiers); - xml_hotkeys.set("ExitFullscreen", hotkeys.exitFullscreen); - xml_hotkeys.set("ToggleFullscreen", hotkeys.toggleFullscreen); - xml_hotkeys.set("ToggleFullscreenAlt", hotkeys.toggleFullscreenAlt); - xml_hotkeys.set("TakeScreenshot", hotkeys.takeScreenshot); - xml_hotkeys.set("ToggleFastForward", hotkeys.toggleFastForward); - xml_hotkeys.set("ExitApplication", hotkeys.exitApplication); -} diff --git a/src/gui/wxgui/wxCemuConfig.h b/src/gui/wxgui/wxCemuConfig.h deleted file mode 100644 index 9fede16c..00000000 --- a/src/gui/wxgui/wxCemuConfig.h +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once - -#include "config/XMLConfig.h" -#include "util/math/vector2.h" - -#include "wxgui.h" - -namespace DefaultColumnSize -{ - enum : uint32 - { - name = 500u, - version = 60u, - dlc = 50u, - game_time = 140u, - game_started = 160u, - region = 80u, - title_id = 160u - }; -}; - -enum class MSWThemeOption : int -{ - kAuto = 0, - kLight = 1, - kDark = 2, -}; -ENABLE_ENUM_ITERATORS(MSWThemeOption, MSWThemeOption::kAuto, MSWThemeOption::kDark); - -typedef union -{ - struct - { - uint16 key : 13; // enough bits for all keycodes - uint16 alt : 1; - uint16 ctrl : 1; - uint16 shift : 1; - }; - uint16 raw; -} uKeyboardHotkey; - -typedef sint16 ControllerHotkey_t; - -struct sHotkeyCfg -{ - static constexpr uint8 keyboardNone{WXK_NONE}; - static constexpr sint8 controllerNone{-1}; // no enums to work with, but buttons start from 0 - - uKeyboardHotkey keyboard{keyboardNone}; - ControllerHotkey_t controller{controllerNone}; - - /* for defaults */ - sHotkeyCfg(const uKeyboardHotkey& keyboard = {keyboardNone}, const ControllerHotkey_t& controller = {controllerNone}) : keyboard(keyboard), controller(controller) {}; - - /* for reading from xml */ - sHotkeyCfg(const char* xml_values) - { - std::istringstream iss(xml_values); - iss >> keyboard.raw >> controller; - } -}; - -template<> -struct fmt::formatter : formatter -{ - template - auto format(const sHotkeyCfg c, FormatContext& ctx) const - { - std::string xml_values = fmt::format("{} {}", c.keyboard.raw, c.controller); - return formatter::format(xml_values, ctx); - } -}; - -struct wxCemuConfig -{ - ConfigValue language{wxLANGUAGE_DEFAULT}; - ConfigValue msw_theme { static_cast(MSWThemeOption::kAuto) }; - ConfigValue use_discord_presence{true}; - ConfigValue fullscreen{ false }; - ConfigValue fullscreen_menubar{false}; - ConfigValue feral_gamemode{false}; - - // max 15 entries - static constexpr size_t kMaxRecentEntries = 15; - std::vector recent_launch_files; - std::vector recent_nfc_files; - - Vector2i window_position{-1, -1}; - Vector2i window_size{-1, -1}; - ConfigValue window_maximized; - - ConfigValue pad_open; - Vector2i pad_position{-1, -1}; - Vector2i pad_size{-1, -1}; - ConfigValue pad_maximized; - - ConfigValue check_update{true}; - ConfigValue receive_untested_updates{false}; - ConfigValue save_screenshot{true}; - - ConfigValue did_show_vulkan_warning{false}; - ConfigValue did_show_graphic_pack_download{false}; // no longer used but we keep the config value around in case people downgrade Cemu. Despite the name this was used for the Getting Started dialog - ConfigValue did_show_macos_disclaimer{false}; // no longer used - - ConfigValue show_icon_column{true}; - - int game_list_style = 0; - std::string game_list_column_order; - struct - { - uint32 name = DefaultColumnSize::name; - uint32 version = DefaultColumnSize::version; - uint32 dlc = DefaultColumnSize::dlc; - uint32 game_time = DefaultColumnSize::game_time; - uint32 game_started = DefaultColumnSize::game_started; - uint32 region = DefaultColumnSize::region; - uint32 title_id = 0; - } column_width{}; - - // hotkeys - struct - { - sHotkeyCfg modifiers; - sHotkeyCfg toggleFullscreen; - sHotkeyCfg toggleFullscreenAlt; - sHotkeyCfg exitFullscreen; - sHotkeyCfg takeScreenshot; - sHotkeyCfg toggleFastForward; - sHotkeyCfg exitApplication; -#ifdef CEMU_DEBUG_ASSERT - sHotkeyCfg endEmulation; -#endif - } hotkeys{}; - - void AddRecentlyLaunchedFile(std::string_view file); - void AddRecentNfcFile(std::string_view file); - - void Load(XMLConfigParser& parser); - void Save(XMLConfigParser& parser); -}; - -typedef XMLChildConfig XMLWxCemuConfig_t; - -extern XMLWxCemuConfig_t g_wxConfig; - -inline XMLWxCemuConfig_t& GetWxGuiConfigHandle() -{ - return g_wxConfig; -} - -inline wxCemuConfig& GetWxGUIConfig() -{ - return GetWxGuiConfigHandle().Data(); -} diff --git a/src/gui/wxgui/wxHelper.h b/src/gui/wxgui/wxHelper.h deleted file mode 100644 index 9bbaddac..00000000 --- a/src/gui/wxgui/wxHelper.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once -#include -#include -#include - -namespace wxHelper -{ - inline fs::path MakeFSPath(const wxString& str) - { - auto tmpUtf8 = str.ToUTF8(); - auto sv = std::basic_string_view((const char8_t*)tmpUtf8.data(), tmpUtf8.length()); - return fs::path(sv); - } - - inline wxString FromPath(const fs::path& path) - { - std::string str = _pathToUtf8(path); - return wxString::FromUTF8(str); - } - - inline wxColour CalculateAccentColour(const wxColour& bgColour) - { - const uint32 bgLightness = (bgColour.GetRed() + bgColour.GetGreen() + bgColour.GetBlue()) / 3; - const bool isDarkTheme = bgLightness < 128; - wxColour bgColourSecondary = bgColour.ChangeLightness(isDarkTheme ? 110 : 90); // color for even rows - // for very light themes we'll use a blue tint to match the older Windows Cemu look - if (bgLightness > 250) - bgColourSecondary = wxColour(bgColour.Red() - 13, bgColour.Green() - 6, bgColour.Blue() - 2); - return bgColourSecondary; - } - - static wxBitmap LoadThemedBitmapFromPNG(const uint8* data, size_t size, const wxColour& tint) - { - wxMemoryInputStream strm(data, size); - wxImage img(strm, wxBITMAP_TYPE_PNG); - img.Replace(0x00, 0x00, 0x00, tint.Red(), tint.Green(), tint.Blue()); - return wxBitmap(img); - } -}; diff --git a/src/gui/wxgui/wxWindowSystem.cpp b/src/gui/wxgui/wxWindowSystem.cpp deleted file mode 100644 index bb730db8..00000000 --- a/src/gui/wxgui/wxWindowSystem.cpp +++ /dev/null @@ -1,352 +0,0 @@ -#include "input/HotkeySettings.h" -#include "interface/WindowSystem.h" - -#include "helpers/wxHelpers.h" - -#if BOOST_OS_LINUX || BOOST_OS_BSD -#include -#include -#include -#include -#include -#ifdef HAS_WAYLAND -#include -#endif -#endif - -#if BOOST_OS_MACOS -#include -#endif - -#include "wxgui/wxgui.h" -#include "wxgui/CemuApp.h" -#include "wxgui/MainWindow.h" -#include "Cafe/HW/Latte/Core/Latte.h" -#include "config/ActiveSettings.h" -#include "config/NetworkSettings.h" -#include "config/CemuConfig.h" -#include "Cafe/HW/Latte/Renderer/Renderer.h" -#include "Cafe/CafeSystem.h" - -WindowSystem::WindowInfo g_window_info{}; - -std::shared_mutex g_mutex; -MainWindow* g_mainFrame = nullptr; - -#if BOOST_OS_WINDOWS -void _wxLaunch() -{ - SetThreadName("MainThread_UI"); - wxEntry(); -} -#endif - -void WindowSystem::Create() -{ - SetThreadName("cemu"); -#if BOOST_OS_WINDOWS - // on Windows wxWidgets there is a bug where wxDirDialog->ShowModal will deadlock in Windows internals somehow - // moving the UI thread off the main thread fixes this - std::thread t = std::thread(_wxLaunch); - t.join(); -#else - int argc = 0; - char* argv[1]{}; - wxEntry(argc, argv); -#endif -} - -void WindowSystem::ShowErrorDialog(std::string_view message, std::string_view title, std::optional /*errorId*/) -{ - wxString caption; - if (title.empty()) - caption = wxASCII_STR(wxMessageBoxCaptionStr); - else - caption = wxString::FromUTF8(title); - wxMessageBox(wxString::FromUTF8(message), caption, wxOK | wxCENTRE | wxICON_ERROR); -} - -WindowSystem::WindowInfo& WindowSystem::GetWindowInfo() -{ - return g_window_info; -} - -void WindowSystem::UpdateWindowTitles(bool isIdle, bool isLoading, double fps) -{ - std::string windowText; - windowText = BUILD_VERSION_WITH_NAME_STRING; - - if (isIdle) - { - if (g_mainFrame) - g_mainFrame->AsyncSetTitle(windowText); - return; - } - if (isLoading) - { - windowText.append(" - Loading..."); - if (g_mainFrame) - g_mainFrame->AsyncSetTitle(windowText); - return; - } - - const char* renderer = ""; - if (g_renderer) - { - switch (g_renderer->GetType()) - { - case RendererAPI::OpenGL: - renderer = "[OpenGL]"; - break; - case RendererAPI::Vulkan: - renderer = "[Vulkan]"; - break; - case RendererAPI::Metal: - renderer = "[Metal]"; - break; - default: break; - } - } - - // get GPU vendor/mode - const char* graphicMode = "[Generic]"; - if (LatteGPUState.glVendor == GLVENDOR_AMD) - graphicMode = "[AMD GPU]"; - else if (LatteGPUState.glVendor == GLVENDOR_INTEL) - graphicMode = "[Intel GPU]"; - else if (LatteGPUState.glVendor == GLVENDOR_NVIDIA) - graphicMode = "[NVIDIA GPU]"; - else if (LatteGPUState.glVendor == GLVENDOR_APPLE) - graphicMode = "[Apple GPU]"; - - const uint64 titleId = CafeSystem::GetForegroundTitleId(); - windowText.append(fmt::format(" - FPS: {:.2f} {} {} [TitleId: {:08x}-{:08x}]", (double)fps, renderer, graphicMode, (uint32)(titleId >> 32), (uint32)(titleId & 0xFFFFFFFF))); - - if (ActiveSettings::IsOnlineEnabled()) - { - if (ActiveSettings::GetNetworkService() == NetworkService::Nintendo) - windowText.append(" [Online]"); - else if (ActiveSettings::GetNetworkService() == NetworkService::Pretendo) - windowText.append(" [Online-Pretendo]"); - else if (ActiveSettings::GetNetworkService() == NetworkService::Custom) - windowText.append(" [Online-" + GetNetworkConfig().networkname.GetValue() + "]"); - } - windowText.append(" "); - windowText.append(CafeSystem::GetForegroundTitleName()); - // append region - CafeConsoleRegion region = CafeSystem::GetForegroundTitleRegion(); - uint16 titleVersion = CafeSystem::GetForegroundTitleVersion(); - if (region == CafeConsoleRegion::JPN) - windowText.append(fmt::format(" [JP v{}]", titleVersion)); - else if (region == CafeConsoleRegion::USA) - windowText.append(fmt::format(" [US v{}]", titleVersion)); - else if (region == CafeConsoleRegion::EUR) - windowText.append(fmt::format(" [EU v{}]", titleVersion)); - else - windowText.append(fmt::format(" [v{}]", titleVersion)); - - std::shared_lock lock(g_mutex); - if (g_mainFrame) - { - g_mainFrame->AsyncSetTitle(windowText); - auto* pad = g_mainFrame->GetPadView(); - if (pad) - pad->AsyncSetTitle(fmt::format("{} - FPS: {:.02f}", _("GamePad View").utf8_string(), fps)); - } -} - -void WindowSystem::GetWindowSize(int& w, int& h) -{ - w = g_window_info.width; - h = g_window_info.height; -} - -void WindowSystem::GetPadWindowSize(int& w, int& h) -{ - if (g_window_info.pad_open) - { - w = g_window_info.pad_width; - h = g_window_info.pad_height; - } - else - { - w = 0; - h = 0; - } -} - -void WindowSystem::GetWindowPhysSize(int& w, int& h) -{ - w = g_window_info.phys_width; - h = g_window_info.phys_height; -} - -void WindowSystem::GetPadWindowPhysSize(int& w, int& h) -{ - if (g_window_info.pad_open) - { - w = g_window_info.phys_pad_width; - h = g_window_info.phys_pad_height; - } - else - { - w = 0; - h = 0; - } -} - -double WindowSystem::GetWindowDPIScale() -{ - return g_window_info.dpi_scale; -} - -double WindowSystem::GetPadDPIScale() -{ - return g_window_info.pad_open ? g_window_info.pad_dpi_scale.load() : 1.0; -} - -bool WindowSystem::IsPadWindowOpen() -{ - return g_window_info.pad_open; -} - -bool WindowSystem::IsKeyDown(uint32 key) -{ - return g_window_info.get_keystate(key); -} - -bool WindowSystem::IsKeyDown(PlatformKeyCodes platformKey) -{ - uint32 key = 0; - - switch (platformKey) - { -#if BOOST_OS_WINDOWS - case PlatformKeyCodes::LCONTROL: - key = VK_LCONTROL; - break; - case PlatformKeyCodes::RCONTROL: - key = VK_RCONTROL; - break; - case PlatformKeyCodes::TAB: - key = VK_TAB; - break; - case PlatformKeyCodes::ESCAPE: - key = VK_ESCAPE; - break; -#elif BOOST_OS_LINUX || BOOST_OS_BSD - case PlatformKeyCodes::LCONTROL: - key = GDK_KEY_Control_L; - break; - case PlatformKeyCodes::RCONTROL: - key = GDK_KEY_Control_R; - break; - case PlatformKeyCodes::TAB: - key = GDK_KEY_Tab; - break; - case PlatformKeyCodes::ESCAPE: - key = GDK_KEY_Escape; - break; -#elif BOOST_OS_MACOS - case PlatformKeyCodes::LCONTROL: - key = kVK_Control; - break; - case PlatformKeyCodes::RCONTROL: - key = kVK_RightControl; - break; - case PlatformKeyCodes::TAB: - key = kVK_Tab; - break; - case PlatformKeyCodes::ESCAPE: - key = kVK_Escape; - break; -#endif - default: - return false; - } - - return WindowSystem::IsKeyDown(key); -} - -std::string WindowSystem::GetKeyCodeName(uint32 button) -{ -#if BOOST_OS_WINDOWS - LONG scan_code = MapVirtualKeyA((UINT)button, MAPVK_VK_TO_VSC_EX); - if (HIBYTE(scan_code)) - scan_code |= 0x100; - - // because MapVirtualKey strips the extended bit for some keys - switch (button) - { - case VK_LEFT: - case VK_UP: - case VK_RIGHT: - case VK_DOWN: // arrow keys - case VK_PRIOR: - case VK_NEXT: // page up and page down - case VK_END: - case VK_HOME: - case VK_INSERT: - case VK_DELETE: - case VK_DIVIDE: // numpad slash - case VK_NUMLOCK: - { - scan_code |= 0x100; // set extended bit - break; - } - } - - scan_code <<= 16; - - char key_name[128]; - if (GetKeyNameTextA(scan_code, key_name, std::size(key_name)) != 0) - return key_name; - else - return fmt::format("key_{}", button); -#elif BOOST_OS_LINUX || BOOST_OS_BSD - return gdk_keyval_name(button); -#else - return fmt::format("key_{}", button); -#endif -} - -bool WindowSystem::InputConfigWindowHasFocus() -{ - return g_inputConfigWindowHasFocus; -} - -void WindowSystem::NotifyGameLoaded() -{ - std::shared_lock lock(g_mutex); - if (g_mainFrame) - { - g_mainFrame->OnGameLoaded(); - g_mainFrame->UpdateSettingsAfterGameLaunch(); - } -} - -void WindowSystem::NotifyGameExited() -{ - std::shared_lock lock(g_mutex); - if (g_mainFrame) - g_mainFrame->RestoreSettingsAfterGameExited(); -} - -void WindowSystem::RefreshGameList() -{ - std::shared_lock lock(g_mutex); - if (g_mainFrame) - { - g_mainFrame->RequestGameListRefresh(); - } -} - -void WindowSystem::CaptureInput(const ControllerState& currentState, const ControllerState& lastState) -{ - HotkeySettings::CaptureInput(currentState, lastState); -} - -bool WindowSystem::IsFullScreen() -{ - return g_window_info.is_fullscreen; -} diff --git a/src/gui/wxgui/wxcomponents/checktree.cpp b/src/gui/wxgui/wxcomponents/checktree.cpp deleted file mode 100644 index d7d56b69..00000000 --- a/src/gui/wxgui/wxcomponents/checktree.cpp +++ /dev/null @@ -1,613 +0,0 @@ -#include "wxgui/wxcomponents/checktree.h" - -#include -#include -#include -#include - -wxDEFINE_EVENT(wxEVT_CHECKTREE_FOCUS, wxTreeEvent); -wxDEFINE_EVENT(wxEVT_CHECKTREE_CHOICE, wxTreeEvent); - -//IMPLEMENT_DYNAMIC_CLASS(wxCheckTree, wxTreeCtrl) - -bool on_check_or_label(int flags) -{ - return flags & (wxTREE_HITTEST_ONITEMSTATEICON | wxTREE_HITTEST_ONITEMLABEL) ? true : false; -} - -bool on_check(int flags) -{ - return flags & (wxTREE_HITTEST_ONITEMSTATEICON) ? true : false; -} - -bool on_label(int flags) -{ - return flags & (wxTREE_HITTEST_ONITEMLABEL) ? true : false; -} - -void unhighlight(wxTreeCtrl* m_treeCtrl1, wxTreeItemId& id) -{ - if (!id.IsOk()) - return; - - const int state = m_treeCtrl1->GetItemState(id); - if (wxCheckTree::UNCHECKED <= state && state < wxCheckTree::UNCHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, wxCheckTree::UNCHECKED); - } - else if (wxCheckTree::CHECKED <= state && state < wxCheckTree::CHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, wxCheckTree::CHECKED); - } -} - - -void mohighlight(wxTreeCtrl* m_treeCtrl1, wxTreeItemId& id, bool toggle) -{ - if (!id.IsOk()) - return; - - const int i = m_treeCtrl1->GetItemState(id); - if (i < 0) - return; - - bool is_checked = false; - if (wxCheckTree::UNCHECKED <= i && i < wxCheckTree::UNCHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, toggle ? wxCheckTree::CHECKED_MOUSE_OVER : wxCheckTree::UNCHECKED_MOUSE_OVER); - is_checked = true; - } - else if (wxCheckTree::CHECKED <= i && i < wxCheckTree::CHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, toggle ? wxCheckTree::UNCHECKED_MOUSE_OVER : wxCheckTree::CHECKED_MOUSE_OVER); - is_checked = false; - } - - if (toggle) - { - wxTreeEvent event2(wxEVT_CHECKTREE_CHOICE, m_treeCtrl1, id); - event2.SetExtraLong(is_checked ? 1 : 0); - m_treeCtrl1->ProcessWindowEvent(event2); - } -} - -void ldhighlight(wxTreeCtrl* m_treeCtrl1, wxTreeItemId& id) -{ - if (!id.IsOk()) - return; - - const int i = m_treeCtrl1->GetItemState(id); - if (wxCheckTree::UNCHECKED <= i && i < wxCheckTree::UNCHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, wxCheckTree::UNCHECKED_LEFT_DOWN); - } - else if (wxCheckTree::CHECKED <= i && i < wxCheckTree::CHECKED_DISABLED) - { - m_treeCtrl1->SetItemState(id, wxCheckTree::CHECKED_LEFT_DOWN); - } -} - -wxIMPLEMENT_CLASS(wxCheckTree, wxTreeCtrl); - -wxCheckTree::wxCheckTree() -{ - Init(); -} - -wxCheckTree::wxCheckTree(wxWindow* parent, const wxWindowID id, const wxPoint& pos, const wxSize& size, long style) - : wxTreeCtrl(parent, id, pos, size, style) -{ - Init(); -} - - -void wxCheckTree::Init() -{ - int width = wxRendererNative::Get().GetCheckBoxSize(this).GetWidth(); - int height = wxRendererNative::Get().GetCheckBoxSize(this).GetHeight(); - - auto states = new wxImageList(width, height, true); - - wxBitmap unchecked_bmp(width, height); - wxBitmap unchecked_mouse_over_bmp(width, height); - wxBitmap unchecked_left_down_bmp(width, height); - wxBitmap unchecked_disabled_bmp(width, height); - wxBitmap checked_bmp(width, height); - wxBitmap checked_mouse_over_bmp(width, height); - wxBitmap checked_left_down_bmp(width, height); - wxBitmap checked_disabled_bmp(width, height); - - wxMemoryDC renderer_dc; - - // Unchecked - renderer_dc.SelectObject(unchecked_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_NONE); - - // Unchecked Mouse Over - renderer_dc.SelectObject(unchecked_mouse_over_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CURRENT); - - // Unchecked and Disabled - renderer_dc.SelectObject(unchecked_disabled_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_DISABLED); - - // Unchecked Left Down - renderer_dc.SelectObject(unchecked_left_down_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CURRENT | wxCONTROL_PRESSED); - - // Checked - renderer_dc.SelectObject(checked_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CHECKED); - - // Checked Mouse Over - renderer_dc.SelectObject(checked_mouse_over_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CHECKED | wxCONTROL_CURRENT); - - // Checked Left Down - renderer_dc.SelectObject(checked_left_down_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CHECKED | wxCONTROL_CURRENT | wxCONTROL_PRESSED); - - // Checked and Disabled - renderer_dc.SelectObject(checked_disabled_bmp); - renderer_dc.SetBackground(*wxTheBrushList->FindOrCreateBrush(GetBackgroundColour())); - renderer_dc.Clear(); - wxRendererNative::Get().DrawCheckBox(this, renderer_dc, wxRect(0, 0, width, height), wxCONTROL_CHECKED | wxCONTROL_DISABLED); - - renderer_dc.SelectObject(wxNullBitmap); - - states->Add(unchecked_bmp); - states->Add(unchecked_mouse_over_bmp); - states->Add(unchecked_left_down_bmp); - states->Add(unchecked_disabled_bmp); - states->Add(checked_bmp); - states->Add(checked_mouse_over_bmp); - states->Add(checked_left_down_bmp); - states->Add(checked_disabled_bmp); - - AssignStateImageList(states); - - Connect(wxEVT_TREE_SEL_CHANGING, wxTreeEventHandler( wxCheckTree::On_Tree_Sel_Changed ), nullptr, this); - - Connect(wxEVT_CHAR, wxKeyEventHandler( wxCheckTree::On_Char ), nullptr, this); - Connect(wxEVT_KEY_DOWN, wxKeyEventHandler( wxCheckTree::On_KeyDown ), nullptr, this); - Connect(wxEVT_KEY_UP, wxKeyEventHandler( wxCheckTree::On_KeyUp ), nullptr, this); - - Connect(wxEVT_ENTER_WINDOW, wxMouseEventHandler( wxCheckTree::On_Mouse_Enter_Tree ), nullptr, this); - Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler( wxCheckTree::On_Mouse_Leave_Tree ), nullptr, this); - Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler( wxCheckTree::On_Left_DClick ), nullptr, this); - Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler( wxCheckTree::On_Left_Down ), nullptr, this); - Connect(wxEVT_LEFT_UP, wxMouseEventHandler( wxCheckTree::On_Left_Up ), nullptr, this); - Connect(wxEVT_MOTION, wxMouseEventHandler( wxCheckTree::On_Mouse_Motion ), nullptr, this); - Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler( wxCheckTree::On_Mouse_Wheel ), nullptr, this); - - Connect(wxEVT_SET_FOCUS, wxFocusEventHandler( wxCheckTree::On_Tree_Focus_Set ), nullptr, this); - Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler( wxCheckTree::On_Tree_Focus_Lost ), nullptr, this); -} - -void wxCheckTree::Sort(const wxTreeItemId& node, bool recursive) -{ - if (recursive) - { - wxTreeItemIdValue cookie; - for(auto it = GetFirstChild(node, cookie); it.IsOk(); it = GetNextChild(it, cookie)) - { - Sort(it, true); - } - } - - if(GetChildrenCount(node, false) > 0) - this->SortChildren(node); -} - -int wxCheckTree::OnCompareItems(const wxTreeItemId& item1, const wxTreeItemId& item2) -{ - const bool check1 = GetChildrenCount(item1, false) == 0; - const bool check2 = GetChildrenCount(item2, false) == 0; - - if (!check1 && check2) - return -1; - - if (check1 && !check2) - return 1; - - return GetItemText(item1).Lower().compare(GetItemText(item2).Lower()); -} - - -void wxCheckTree::SetItemTextColour(const wxTreeItemId& item, const wxColour& col) -{ - const auto it = m_colors.find(item); - if (it == m_colors.end()) - m_colors.emplace(std::pair(item, col)); - else - m_colors[item] = col; - - wxTreeCtrl::SetItemTextColour(item, col); -} - -bool wxCheckTree::EnableCheckBox(const wxTreeItemId& item, bool enable) -{ - if (!item.IsOk()) - return false; - - const int state = GetItemState(item); - - if (state < 0 || state > CHECKED_DISABLED) - return false; - - if (enable) - { - if (state == UNCHECKED_DISABLED) - SetItemState(item, UNCHECKED); - else if (state == CHECKED_DISABLED) - SetItemState(item, CHECKED); - - const auto it = m_colors.find(item); - if (it != m_colors.end()) - { - SetItemTextColour(item, it->second); - m_colors.erase(it); - } - - return true; - } - if (state == UNCHECKED_DISABLED || state == CHECKED_DISABLED) - { - //don't disable a second time or we'll lose the - //text color information. - return true; - } - - if (state == UNCHECKED || state == UNCHECKED_MOUSE_OVER || state == UNCHECKED_LEFT_DOWN) - SetItemState(item, UNCHECKED_DISABLED); - else if (state == CHECKED || state == CHECKED_MOUSE_OVER || state == CHECKED_LEFT_DOWN) - SetItemState(item, CHECKED_DISABLED); - - const wxColour col = GetItemTextColour(item); - SetItemTextColour(item, wxColour(161, 161, 146)); - m_colors[item] = col; - return true; -} - -bool wxCheckTree::DisableCheckBox(const wxTreeItemId& item) -{ - return EnableCheckBox(item, false); -} - -void wxCheckTree::MakeCheckable(const wxTreeItemId& item, bool state) -{ - if (!item.IsOk()) - return; - - const int i = GetItemState(item); - if (i < 0 || i > CHECKED_DISABLED) - SetItemState(item, state ? CHECKED : UNCHECKED); -} - -bool wxCheckTree::IsCheckable(const wxTreeItemId& item) -{ - if (!item.IsOk()) - return false; - - const int i = GetItemState(item); - return i >= 0 && i <= CHECKED_DISABLED; -} - -void wxCheckTree::Check(const wxTreeItemId& item, bool state) -{ - if (!item.IsOk()) - return; - - const int old_state = GetItemState(item); - if (UNCHECKED <= old_state && old_state <= CHECKED_DISABLED) - { - const bool enable = !(old_state == UNCHECKED_DISABLED || old_state == CHECKED_DISABLED); - int check = enable ? CHECKED : CHECKED_DISABLED; - int uncheck = enable ? UNCHECKED : UNCHECKED_DISABLED; - const int new_state = state ? check : uncheck; - - if (new_state != old_state) - { - SetItemState(item, new_state); - } - } -} - -void wxCheckTree::Uncheck(const wxTreeItemId& item) -{ - Check(item, false); -} - -void wxCheckTree::On_Tree_Sel_Changed(wxTreeEvent& event) -{ - wxTreeItemId id = event.GetItem(); - - unhighlight(this, last_kf); - mohighlight(this, id, false); - last_kf = id; - - event.Skip(); -} - -void wxCheckTree::On_Char(wxKeyEvent& event) -{ - if (!GetSelection().IsOk() && GetCount() > 0) - { - //If there is no selection, any keypress should just select the first item - wxTreeItemIdValue cookie; - const auto new_item = HasFlag(wxTR_HIDE_ROOT) ? GetFirstChild(GetRootItem(), cookie) : GetRootItem(); - SelectItem(new_item); - last_kf = new_item; - return; - } - - event.Skip(); -} - -void wxCheckTree::On_KeyDown(wxKeyEvent& event) -{ - if (event.GetKeyCode() == WXK_SPACE) - { - last_kf = this->GetSelection(); - ldhighlight(this, last_kf); - } - - event.Skip(); -} - -void wxCheckTree::On_KeyUp(wxKeyEvent& event) -{ - if (event.GetKeyCode() == WXK_SPACE) - { - //last_kf = this->GetSelection(); - mohighlight(this, last_kf, true); - } - else if (event.GetKeyCode() == WXK_ESCAPE) - { - unhighlight(this, last_kf); - last_kf = {}; - Unselect(); - } - - event.Skip(); -} - - -void wxCheckTree::On_Mouse_Enter_Tree(wxMouseEvent& event) -{ - if (event.LeftIsDown()) - { - mouse_entered_tree_with_left_down = true; - } -} - -void wxCheckTree::On_Mouse_Leave_Tree(wxMouseEvent& event) -{ - unhighlight(this, last_mouse_over); - unhighlight(this, last_left_down); - last_mouse_over = {}; - last_left_down = {}; -} - -void wxCheckTree::On_Left_DClick(wxMouseEvent& event) -{ - int flags; - - HitTest(event.GetPosition(), flags); - - //double clicks on buttons can be annoying, so we'll ignore those - //but all other double clicks will just have 1 more click added to them - - //without this, the check boxes are not as responsive as they should be - if (!(flags & wxTREE_HITTEST_ONITEMBUTTON)) - { - On_Left_Down(event); - On_Left_Up(event); - } -} - -void wxCheckTree::On_Left_Down(wxMouseEvent& event) -{ - int flags; - wxTreeItemId id = HitTest(event.GetPosition(), flags); - if (!id.IsOk()) - return; - - int i = GetItemState(id); - - if (id.IsOk() && i >= 0 && on_check(flags)) - { - last_left_down = id; - ldhighlight(this, id); - } - else if (on_label(flags)) - event.Skip(); -} - -void wxCheckTree::On_Left_Up(wxMouseEvent& event) -{ - SetFocus(); - - int flags; - wxTreeItemId id = HitTest(event.GetPosition(), flags); - if (!id.IsOk()) - return; - - int i = GetItemState(id); - - if (mouse_entered_tree_with_left_down) - { - mouse_entered_tree_with_left_down = false; - - if (i >= 0 && on_check(flags)) - { - mohighlight(this, id, false); - last_mouse_over = id; - } - } - else if (id.IsOk()) - { - if (flags & wxTREE_HITTEST_ONITEMBUTTON && ItemHasChildren(id)) - { - if (IsExpanded(id)) - { - Collapse(id); - } - else - { - Expand(id); - } - } - else if (i >= 0 && on_check(flags)) - { - if (id != last_left_down) - { - unhighlight(this, last_left_down); - mohighlight(this, id, false); - } - else - { - mohighlight(this, id, true); - } - - last_left_down = wxTreeItemId(); - last_mouse_over = id; - } - else if (on_label(flags)) - { - event.Skip(); - } - else - { - unhighlight(this, last_left_down); - unhighlight(this, last_mouse_over); - last_left_down = wxTreeItemId(); - last_mouse_over = wxTreeItemId(); - } - } - else - { - //id is not ok - unhighlight(this, last_left_down); - unhighlight(this, last_mouse_over); - last_left_down = wxTreeItemId(); - last_mouse_over = wxTreeItemId(); - } -} - -void wxCheckTree::On_Mouse_Motion(wxMouseEvent& event) -{ - if (mouse_entered_tree_with_left_down) - { - //just ignore everything until the left button is released - return; - } - - int flags; - wxTreeItemId id = HitTest(event.GetPosition(), flags); - - if (!id.IsOk()) - { - unhighlight(this, last_mouse_over); - last_mouse_over = {}; - } - else if (event.LeftIsDown() && last_left_down.IsOk()) - { - //to match the behavior of ordinary check boxes, - //if we've moved to a new item while holding the mouse button down - //we want to set the item where the left down click occured to have - //mouse over highlight. And if we return to the box where the - //left down occured, we want to return it to the having the left down highlight - - //I don't understand why this is the behavior - //of ordinary check boxes, but I'm goin to match it anyway. - - if (id == last_left_down) - { - const auto state = GetItemState(last_left_down); - if (state != UNCHECKED_LEFT_DOWN && state != CHECKED_LEFT_DOWN) - { - ldhighlight(this, last_left_down); - } - } - else - mohighlight(this, last_left_down, false); - } - else - { - //4 cases 1 we're still on the same item, but we've moved off the state icon or label - // 2 we're still on the same item and on the state icon or label - do nothing - // 3 we're on a new item but not on its state icon or label (or the new item has no state) - // 4 we're on a new item, it has a state icon, and we're on the state icon or label - - const int state = GetItemState(id); - if (id == last_mouse_over) - { - if (state < 0 || !on_check(flags)) - { - unhighlight(this, last_mouse_over); - last_mouse_over = {}; - } - } - else - { - if (state < 0 || !on_check(flags)) - { - unhighlight(this, last_mouse_over); - last_mouse_over = {}; - } - else - { - unhighlight(this, last_mouse_over); - mohighlight(this, id, false); - last_mouse_over = id; - } - } - } -} - -void wxCheckTree::On_Mouse_Wheel(wxMouseEvent& event) -{ - event.Skip(); -} - -void wxCheckTree::On_Tree_Focus_Set(wxFocusEvent& event) -{ - //event.Skip(); - - //skipping this event will set the last selected item - //to be highlighted and I want the tree items to only be - //highlighted by keyboard actions. -} - -void wxCheckTree::On_Tree_Focus_Lost(wxFocusEvent& event) -{ - unhighlight(this, last_kf); - Unselect(); - event.Skip(); -} - -void wxCheckTree::SetFocusFromKbd() -{ - if (last_kf.IsOk()) - SelectItem(last_kf); - - wxTreeEvent event2(wxEVT_CHECKTREE_FOCUS, this, wxTreeItemId()); - ProcessWindowEvent(event2); - - wxWindow::SetFocusFromKbd(); -} diff --git a/src/gui/wxgui/wxcomponents/checktree.h b/src/gui/wxgui/wxcomponents/checktree.h deleted file mode 100644 index eb620b8b..00000000 --- a/src/gui/wxgui/wxcomponents/checktree.h +++ /dev/null @@ -1,105 +0,0 @@ -#ifndef checktree_H_INCLUDED -#define checktree_H_INCLUDED - -// credits to: https://forums.wxwidgets.org/viewtopic.php?t=39582 - -#include "wx/treectrl.h" -#include - -#define WXMAKINGDLL_CHECKTREE - -// dll export macros -#ifdef WXMAKINGDLL_CHECKTREE - #define WXDLLIMPEXP_CHECKTREE WXEXPORT -#elif defined(WXUSING_CHECKTREE_SOURCE) - #define WXDLLIMPEXP_CHECKTREE -#elif defined(WXUSINGDLL) - #define WXDLLIMPEXP_CHECKTREE WXIMPORT -#else // not making nor using DLL - #define WXDLLIMPEXP_CHECKTREE -#endif - -#include -// wxDECLARE_CLASS, wxIMPLEMENT_CLASS for sorting? - -class WXDLLIMPEXP_CHECKTREE wxCheckTree : public wxTreeCtrl -{ - wxDECLARE_ABSTRACT_CLASS(wxCheckTree); - - public: - wxCheckTree(); - wxCheckTree(wxWindow *parent, const wxWindowID id, - const wxPoint& pos, const wxSize& size, - long style); - - - void Init(); - void Sort(const wxTreeItemId& node, bool recursive); - - int OnCompareItems(const wxTreeItemId& item1, const wxTreeItemId& item2) override; - - //methods overriden from base class: - void SetFocusFromKbd() override; - void SetItemTextColour(const wxTreeItemId &item, const wxColour &col) override; - - //interaction with the check boxes: - bool EnableCheckBox(const wxTreeItemId &item, bool enable = true ); - bool DisableCheckBox(const wxTreeItemId &item); - void Check(const wxTreeItemId &item,bool state=true); - void Uncheck(const wxTreeItemId &item); - void MakeCheckable(const wxTreeItemId &item, bool state = false); - bool IsCheckable(const wxTreeItemId& item); - - enum - { - UNCHECKED, - UNCHECKED_MOUSE_OVER, - UNCHECKED_LEFT_DOWN, - UNCHECKED_DISABLED, - CHECKED, - CHECKED_MOUSE_OVER, - CHECKED_LEFT_DOWN, - CHECKED_DISABLED - }; - - private: - //event handlers - void On_Tree_Sel_Changed( wxTreeEvent& event ); - - void On_Char( wxKeyEvent& event ); - void On_KeyDown( wxKeyEvent& event ); - void On_KeyUp( wxKeyEvent& event ); - - void On_Mouse_Enter_Tree( wxMouseEvent& event ); - void On_Mouse_Leave_Tree( wxMouseEvent& event ); - void On_Left_DClick( wxMouseEvent& event ); - void On_Left_Down( wxMouseEvent& event ); - void On_Left_Up( wxMouseEvent& event ); - void On_Mouse_Motion( wxMouseEvent& event ); - void On_Mouse_Wheel( wxMouseEvent& event ); - - void On_Tree_Focus_Set( wxFocusEvent& event ); - void On_Tree_Focus_Lost( wxFocusEvent& event ); - - //private data: - std::map m_colors; - - bool mouse_entered_tree_with_left_down = false; - - wxTreeItemId last_mouse_over{}; - wxTreeItemId last_left_down{}; - wxTreeItemId last_kf{}; - - - // NB: due to an ugly wxMSW hack you _must_ use DECLARE_DYNAMIC_CLASS() - // if you want your overloaded OnCompareItems() to be called. - // OTOH, if you don't want it you may omit the next line - this will - // make default (alphabetical) sorting much faster under wxMSW. - //DECLARE_DYNAMIC_CLASS(wxCheckTree) -}; - -wxDECLARE_EXPORTED_EVENT(WXDLLIMPEXP_CHECKTREE, wxEVT_CHECKTREE_CHOICE, wxTreeEvent); -wxDECLARE_EXPORTED_EVENT(WXDLLIMPEXP_CHECKTREE, wxEVT_CHECKTREE_FOCUS, wxTreeEvent); - - -#endif // checktree_H_INCLUDED diff --git a/src/gui/wxgui/wxgui.h b/src/gui/wxgui/wxgui.h deleted file mode 100644 index bb4352d1..00000000 --- a/src/gui/wxgui/wxgui.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#define wxNO_UNSAFE_WXSTRING_CONV 1 - -#include -#ifndef WX_PRECOMP -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern bool g_inputConfigWindowHasFocus; - -inline bool SendSliderEvent(wxSlider* slider, int new_value) -{ - wxCommandEvent cevent(wxEVT_SLIDER, slider->GetId()); - cevent.SetInt(new_value); - cevent.SetEventObject(slider); - return slider->HandleWindowEvent(cevent); -} diff --git a/src/resource/MacOSXBundleInfo.plist.in b/src/resource/MacOSXBundleInfo.plist.in deleted file mode 100644 index ccd1c922..00000000 --- a/src/resource/MacOSXBundleInfo.plist.in +++ /dev/null @@ -1,69 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} - CFBundleIdentifier - ${MACOSX_BUNDLE_GUI_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${MACOSX_BUNDLE_BUNDLE_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - ${MACOSX_BUNDLE_SHORT_VERSION_STRING} - CFBundleSignature - ???? - CFBundleVersion - ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - NSHumanReadableCopyright - ${MACOSX_BUNDLE_COPYRIGHT} - LSApplicationCategoryType - ${MACOSX_BUNDLE_CATEGORY} - LSMinimumSystemVersion - ${MACOSX_MINIMUM_SYSTEM_VERSION} - CFBundleLocalizations - - ca - de - en - es - fr - he - hu - it - ja - ko - nb - nl - pl - pt - ru - sv - tr - uk - zh - - CFBundleDocumentTypes - - - CFBundleTypeExtensions - - ${MACOSX_BUNDLE_TYPE_EXTENSION} - - CFBundleTypeName - Wii U File - CFBundleTypeRole - Viewer - - - - diff --git a/src/resource/cemu.icns b/src/resource/cemu.icns deleted file mode 100644 index c6d8c5b5088fc39792a697af8f771de09b5b61c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 817145 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 F{|%$?3Bv#Y diff --git a/bin/resources/sv/cemu.mo b/bin/resources/sv/cemu.mo deleted file mode 100644 index c8fd68ee6a01d9e46e65852673262b289852d362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67980 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 diff --git a/bin/resources/tr/cemu.mo b/bin/resources/tr/cemu.mo deleted file mode 100644 index 5b9b47bad64883aa9e80f9c297395b15c082a73a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28711 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 diff --git a/bin/resources/zh/cemu.mo b/bin/resources/zh/cemu.mo deleted file mode 100644 index 3e6369713ad379e558631b4db68b02fde8dab833..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60704 zcmcG%2YejG`TxI^P!tHgLs%0qGT6d4CBzitj=@E-WJ(Hfmd@5;NvG%}7fNCq+>2~* zx4{J)o8kgCxX2|5A(iwFfrLbNk|5uZkc5QP|NAq~?A}Q>$@l;N{r-Bfe(#i>o%+l( z&&(cgoO<+H90$*Wr@#d=IKz|AKnYasBN*=R)^hsONe^ zY zL&f_Uq>BV^diUR;@^jQc%iqaR?bih=ovu*jD1plVO;GvxBvkt(JSRY*G*9Gy#=aYeF~ll>!IS!hRV+) zQ19F1{dYj6x6iv@f_nceQ0e>>s(kN2z2}I*Rv#aR>St#|J%1_G`%0kV4}urM+o8%e z(fiMV%ExM`dVUTn-Z!D%`xaC={|r9_{|PUEM-8#?E>Q374po2GLDl<6_(^yVJPJM! zRUa=xrTbNQEc`yyb3cb4h5rNf{0TSHm*55PL3jr|2mS==eZPllkAFj@f8tPcp90Hq zUkFvs2B`PUgfn3iRJv!~V)+>b6>kiriwBL6rVe&PhGg(hcs=ZUtJTv3unX=*Q1!6` z9uN1xqv13tolpS}iFpKb7Jc-%0HcO%qu1EA`CI8=GYLglvs9tR)roDNk_3q03D z#eWfg7=8tw0KW%|;OkK1s0}LoU+@@MG~DXz6sUZj0hNzpD0$r5yGKH`$DL61J_)K_ z9)aptPeAp@y-?}?04lwoL&f_oR6qGAyat{(!s>4n9EW=xP>QhD(iH3A-oJ;1y6%_!y91+ zQdPn8(2d{QtlV9o%5f!Bey)S6@4-;@FdUu%Q&9c*3owLhpx*mMxCQ~o6!>eX_V^RjxcwVcI{$*o$0>JMeV+pr?;?06EcWaR74J5v^zVYjFbOsO z7C`l%M}7DjsCs!4svVo*$?z+%7yL0)eviGAu?GjhYWOr%_*tK_@(+c2?rzUI7~-A^ zRlfD`QusX7`(K6U!W`7NI^xro&PSo@|0<|*kMjOWsPylLG-)soD&KEH)x&$R0{#OI zhWC8N`rRg|di?`byrVvA?>Qcxjr&5VbgqT!55u9}n}Abc15Cm9pxW=AyR3d|;W@Y` zK-K?3sB|_#rMnL*ogc%qU>nr?gS)LC9Rn}Nc?#?eZ}9FqsQ$ScUJ1VjOW@z(HSn@~ z(EVTnUIphuJ^w}carj55bT3a>``_+)7gYNvz58CMe)~D7`k4v!z7&W5Uo9#G@_I;ehnGdvFtgCB!cQ1vnoUJsvu%J;8eA6P_VshoqL`e~i#M5zAq zd8qd+@cye|7u-*H_sj4)+^=}|ag~O1+zmBOYoXFx2bKO7sOMjVm%wHp{w7p?{SQ<={|O~;PO7!% zxHR+n z^?Vmt014=$+;rHON^;RE0gX$-*LzTA`s{ViP-NQ0Lz|<6s zh6;ZkDu2yT`S`B)|0&daeg&0YD^!2}1N!l7^gT;Rih0M%ZvLZ#ORL--Cy=OjDJ*hnlC3zR%>=g;4nz0+qjey}Jpzegc)hW~lVO z1=YT9dH+AbQ*a-Bzt#JhQ0>_Ts+=Y840t_M`9{Hyz!W?gPJ(K$+3;$(7^=O$35(%L z4_N-Mg?hdes@=vx<)a#&4JSa=a}!j4)7hAPKP@I3fKAO4Q_f6w!n2kp5t zq58oEQ2Ff!mCm(L<+=%~oFm|A@Y6m#4OMRwy?cu1EU0`fgnDj0)N|XQ7fxqQ2Dd`> z&(~ltc-jopF9$*O*9YK6xD8$kN6#ca=|2cB#{I%9E7z;=I^2JOYL6>tTRr!OYL8pt zh44;z37ibozt;Hh?NI&a08~Bx7^f;iqcE1}cy>akpI0+sF zAB5`PQ(=F&45}Soh1bKsK&mRZYA!ku+zl1~x_Q>#pM*;H4ygW-f+xWHVOKZ{x_$!n z+)JU}rVIDQkV zKK=$(A0J+5>0JPq;_l+PTmDC4-&$nzblIaO#}-57cNbLsy$t)n-@2~YIp|R=J_&Id0&Gn@2{cK$w58$ws-#lD!uo-`@dFNf_u{EU@W&s{yQ zgQ~|;sP~SCdj3Ao>7ENcS3%|TN$=hZ)ow3C)%UCL9M}f+{v%geKF@(?;Jyqh{hK^* zg-Y)>@4nZw(Q^uv{Fw_~c|AA!@E4)p^CfsO{F!(E6Dt1ct8E-!0`=Z2Jo`YEcMw#( za;R~Tgv#eU@4p`ExgDMdy#F`gdHBByRqpp-2#;p)D!ofQuYoGx5UBS1v}XlWJ&lKF z!|71ZJq}f#ZBXs}RjBvB0dIiqP~n$8Zt-t~a*u?{$5`**;QgmVJ-^y>ljrkL<=+qW z-dCW?{W?^>{s@)ciEAwV^PuAQhKk=0D&0|@pY{G#Q2Ckc-3xvAI`4iKsy_C6|L;T9 z<1f7bU*Sc#Pgra9as^brZidR&UGRK330?q~K#ivzQ2BWYs-C|CmF`cV#`n8Wa`IoE zC$F>eTm!}B_3$pd6dt$1?5sUu3GM{!4HrVi zdkL!iKZknnpP}CK5AQyHqvh*lDE|=ZJw2e(zX|HODj%MPD)$3W^|%CTe%s{v98~;f zD0%Q@&!0l2|68c|?|UBgq}A_<~9exzf zgI(Y=Q04zNRQZ1cHIDubkAY`xvT}aZvlr}v|3KIa)a2A{nm%xkRd0R~1y$N1}dkR#)*#%F8-}8I}cE|l&sC3WSYVYk1 zZ^xa2D({z|!hZ_&-nTse29>{}ZI=EiQ0;sfRC)S)-UUy>eIKOj1#`T+=XT5AO;F{V z2-R<=LiM+$-hT^JKimbCPBYYe^ew3O|H$(-sONv}`4{hh!gChyG^qMH*Sq_A4)z=l zRZr#KT?1AA2B>zL1AD^l@RRUOSPHLv-sI22P~%`5RJ(i!D!;EmrTeybAHBokoeov* zD`5z4fU38<;HmIF_%S#iYP>xMm7iCj()~5;58s7~fAvmF?`C)_?osg5@NsxG{2SEs zJzuc=4TgHI465D6!c*Yqq0(RI!`H(5aPNRBcgc(P{1mAApAS{9i=oonsvPe_mE-umKL7A++})w#4f5`h z@N(Q$Q1KVTYv9vR<$M!f2~TOZatwm%=l4S8vlc2pjZpPE4J!R5P~}_$74J(>{qXDF zea=4f522pF6zcs~L6!3ssB%N3HUfvy#5DG9~lajPC3+j6W%=+9*KK7RQy$NHeB!B-M?t{ekD8)|G`k{jDgBe z8mb)FWGy~fqGAO=*j~>ihC%W z2CJaP9D%HbdjG+fOrLuNo{9S%sPvBcit$9K{G1L|&WpYOO`bzM zM?m$%(cWDJmESZ}Ic9nPM?6S3;F%qvx|w{dk9We+BA2--74EA4BEiU04P`^i_NQPN?_Y<5>gs z+o%i1imA_}9>SqsByT1mNpEJI0EQX4A9aKHt49|iIcmbRY6@Qrzf7`D z<@_a7evf#?%6~4@d#;9(&(}kZ&pOX(@C4k8p`Kp{uY)f^$)SHkJ>TQo7Vj!3cR%Rb z1M2+=??2wN9)1}A$=*HPa~`}9|3{(v$quOUe;u9xf99Emde6I_??d&=;5$~2XF`>u z8+-s>2Nk~Chkpl3zP#z(!FSDl8dSXAP|x2CFNU9jm%=Z2Zh{}f{avVf|D)$g-?MtW z9G2jJH@pchfQt8BsCs!DD!rq>Z}00472Y4}eRp~HL*Bgxs=Qx;>d)VT%I6#KBk)g9 z{r;#QSbsPVo`U-mp0|2_7V5b)R6eGAJ`PX9y$hK--mkN8&K)|4l11^eq`?Bp!(Z+Q0d+P_1qAs z=gOhtS3>od@lfxb3nlk9`tUD9J^u~w{-NiqQ15*cD&0T9VtB-lE#1qY{Cj!c=y@wt zJKf>k4W3h>>T3af7Vdz`$2~u>`hF1p4fmt)8pZpm@ntv&_dlT8qyNv0pN3c9Zh&vV z4RADk=;v16pTmB*--o9iLH~Z$)-xktvvD~OUQPI3I0OD3Nigth zS@_4G`o)z{^*jjb`B9#qff`?9q1t&4>;o5h_bX7({m{FA<=F~de}XpzwC9`F4t=5e z@n_%zaK2~JuPy#esQ0XdN_QPpIktND0eBwnZ+UksRJs2IHU5fzW9@kkRC!Bb2vbn$ z%z^#kN~rW-hf41)C^>XYi}_#T*&7~(e_yEg41mi2oj!aH)bq=r>TL&9eeZ`VN7nPV zQ0e^9^Do~2@9;kSkIdRQeGp!Z`w^&qw+kx$R(KjbDrY!6mEjKBj|^% z){ib~xAX==l{e+x6X0dIS3%X&SK%=DOZW`z{+8wcXYhFWLzTbX`~Mpb!hOu!=0D7H z3{?C{Q0+erDxXW>F>rHP|xjv%EvBv7yJ^WI^7ws$|Jd#gTI%!pW*L^ z;%>ry9hd%k!_huW@6g{Cu9NV8$+@g;A0^IfT$kg&1OGeVgFgI4|NMt=AJ6?NI2;b) z`Y!SGH^s*>7j{G31GygHO5(YQOFB*o?m=8PaOrO#&)53AS_1^Cj z#QQGSoA~X-uNPe4pS=(M5Wm0UH-)PY*K*vO@E^qem$+}?eleH+9>p*Ed#K=E?ehrN zLHrg%jh_p6=H-Go8Y7!=pAHNEULf8ly~jH4mvdcAJdK5o1rhJZZy(q7TpH(ZT2{Aj6ZSdcT;~0SF8=;B!p`Tql0IN9`w-Vgu5aLOChiMd zKj!{PSni*F0=NEdBHoc)`um%~{i{7?{q2KO?bg}%zJ&Wut}D6p_i32)X`PQ-vV8@Y z_u`Isa}DSEj2!%>d1iyiwU}!@R}p@XbA6e6{mtRN9+r{NQ{11){g=5P0B7Mond=s= z&*DB8{}03Owuj>f%-OJ^q}7!zFI(>;Ixgume$$^BDY zeTnxL?iuhLu7~iu3+nHE&tDS$2kz%{{ha$ct{nH*L#D7`HPnbx{J(>Hv-1stFXF$2>u4W-3E}q;_7xw#!u#Kddo9-s zgfHj1+=urd{I|Go!To#g_i|0&n%I`*not?~mM9aepoM&%*D+ z6BWSUV8RySH=gTCu5WYc?;_9B;Yy#bg?#Y8vwgfz5al!6uY*VNe9EWQ3wIm$mk^fX zI>>cCS9ko2@t?`P{u*Hs+>PG=?mxx-P#<q)|I!u?&YCaz&zZ{fEO>hC$?+>U!D_h)l|5BF{GV{j>K!G8_c3An$Cdm8sU zxR);8n+r9;>Fcj@zaRfE;@-^t0Pb(*K7=3Q(%-MRPT~F{nBf}g{jVebd)!Zi`ujZh z--2I*OP!DX(-rr8ALoZ~F767hWn7oy{sCOib+vy+`sz5Y_XxWf{(`Wt`LOT!bh_bp z7xzoJZsh)XUEFVh|K`$P1N;hpS8(ZXIPM}J_C9`(aFyU+=lvAt1pUL`z5bbj+)v{E zMB;oH{()-@e*OG2{ki`sm;UAw)}8Asgnb+8?`K@sxI5|<|L*=d@8!-)9f{wm-v2rR zUM+|-kNYHPjmLi`_v?MSV{wo5@p^E-gli#T^Pv7F5w^j{?Tz~v+>gZX4=xnBgZl>D zulqE9hx=nb{I_~*Fa=Ho3WjOg#BKmMMpit9w;E%MJQ z_}ARu3!mot7uOW73w`)x?k^|)b5MWB6+E{R_Y?k|t)43hpTPAj?!H`i6Xy-M6u%kV z-|L@06TiQ5m2sVdfAqK5hs%98VbR}%_`iYUrOEcma(5zRAtY#68RVrJ?|M1%9izzQuJOaSH#|`1m*D|52_>2t3Hu!2NTC zJ;wDW?o)9`e|@;W8vj?l$EUgf7Jd)oevRu9+#lt72fwdF{as)PK8*WsT<75aB0RyT z`#A1hxYx=Jp8#q2Bix^XvvH4sqoDrI!oLgd=i@SyEHt&Cc zfM)K$LHvJnt>OAPejmXv!~OfwgD`>LZ{bbwTEg`A1%{HG{UrE3+>dkh^Kpvd zU-3WPySa6?lv{Pf-zSLs7cTuZ6XyZ$PxkI z8T=;x$HHg4-#E_)@xPDr8e?^lNVpZ6aEtRc-a2ycg9_vvSFXSp_T{g3ziD)FD;{wVL>>Yu-iIQqK^zQ(mn_dd># z;niH{;&-d}D_0!uPs8t%@K)Ua@?m%di?}}mzhk-o2G>8iCUQ;1FZ%l(H=o2^41Wp7 z@ytiK_V_qQV5U3FjJRIR?5}Ekq_t4IePtvalvy*;t*-gw^SB z<5IQb@UKc%j1R}A>%+#nO7fxjRjJx!Cdo~@Hj}PShSiDMagB*_&Zmyl2c+whsoGRS zI5<_E>_G{V89f&H4zskw;*n*el6C3&hAT#v`J^aR9Bp{1)E4ha&1LbeY!SvU!ozEuC+)5hYbiTQ}xM;1`^{N{N$KMia9V@QJ<`#;&fY`Y@jRy z(-Ui}(+L_hx(kOVYAI$tK8-bX_HRh@)bm_8s^4(=G*natT?bWC_;HDvWLQ_99!n)e zUiFE(s#HZ-m#7#|jksvFMB}!mv6|Yg3`dvs4+kbEq^L7|YsXcmGD>h#ikuBjOi0(K zls8o@|MFtJZg69Db%y*WYlE&s?&xjh9HQ436m&?kHd&vj4l~Jy23nTXha~H35_-gZ z!c0S=o-E*zZb*j%Q<+4j21h}j$vEXx&I)~T-rW0|tL(KmC#$KMp|x>!4y~=DGrL}B zxA_bVt>u-~)mBptsfOxgci+oG`Wp>ZQc|L{CM2p;u8G6OjO%7Yc`T)@-;$_HSYN!A z-X))5iAL%qbg9LziK&LFaLB;^6tQs(zO<@Knw}VzmJSFj(zOlANe!->yPWzi8gw0Q zea_n3J2J^cJ+(kL7%_MN|BMJn3@#12j;u@8xjBVEn1U+(OXIxca7s3-8deV99y5vR9QRF;dtMups}S5WY>ibO57HXv+Bj0?xs zr)xqRC1uIFgp+pay`_xn38^GiQdX7VrR1foib9XAFBn-=b6L7#eA0KZGWvrXF=gt^ zgiWp)ldk5;@%0m`YkEd!VwHNT^>Z*BR zP@Z;g7)@>0Nt`i?DKyD5Dub@0o!BfKiMJ)|Q)4HG2;by5hVx{a%c@>=z!SGoX4frU z7jW_>_G)nAE{c?J624P3_2hiW z?z`cqlJz`TTb&w%gsTp3E-x>=LjRP7^~ulCZ%0s=Ec(dqEdUoZT4Q7}nC? z;<<#@tHiIdc6==z#s>ubke!(Z+P=va} zn#g1(rt2#QxNgV;?p2nI=AxQ0G*F$76k1g?eTS3NjrHN}sZ=;RtfBr~CTkO8R0o=- z8>&bu%Ep)^UQ7c_SIT0l!M(kcLLni7#2{ZC)CupTF(`C^ZlQ}hi^REH4HUUPO-;pP zTbmeDl4(`A`6Zb%ulQ8 zEmWyXQ>AuKlX9j`9b4ISd6Rc2iB>e!Wh<#+#nUj<&o?$gq4O8?o)LCga0Q6&v}d6_2%O5xznKP9om7ImhP3-9EJou?*P!7cil}(}sbV+nIftZx1oq^R zIiZuntH_-*wJXrl!wF1Inw~X0ofg$t7xbHy$^`urnWXDw`MW;h2*!&SYKX-+ zN}rHQ>z}G<9FwYm)hOACdT#0)swxwcgZ}9>C9kVbNd=r77We4Y?NGm~<<}sYT0vVN z{k*F>Ikq9XuTPDu!a)Zc9~KYR6YiF*ceVC&>jLxjON9RQ>4_-(S}1UZ=o+*M9o5t^ zd3CzeaMAij~K@UXU?jb$xMuuva7gr>!1xa^QR4uF&8hOPpjXW$3MJbU+%t{DdyKkVf6)}&i z)x-OHK3Fusx@e_(!2P`*EE-8IrzFJ^jSW@l`V5WRoA;|qSSVO^sd9$*H&0|ERTAeo z)sTtBRaOO><`P~Qk~%AxbbU!tk))$$W|NYd zUdTgbvL;bKo*^|x7rNVs%{od zo0E(BA-Y^8*u;vKB9)9aEeg?C-iQn%Q<@XhzR{~Yl^#8kP7moZu5yePTCvVj@19kd9#RJ;wp)0k z|BtactWBaU50LC7PJH9UtQN+IJ}+c7Nk;KH`Z3-JG26s|Y7{5r=m17ez5N?$o9e#! zq$SXpJelm3D=7(T91Zo1dxx7X5L=9FXArU-9R zWCMEM_^>QluXTM`ToI+D^-Y3a<-+gISa_ynQVkk)K}xEw(t2im#!T2Lwa)QSa1@M@eg=_(QFWSvusLm5z9 zgR;V|Pa}Gb)l5zK+sH~n2Z)37c8UT&iExWy<10}keO}ErrqM}527oPz5)Fx>o5?7> zl|>KB73k%J7X`+GgWy{*Z-xpNpYtiMH4|qmF$(pD9Q=f!Fk>dzJfv5SY4^Y#tjuqwhB#O zu?&=fNKbVsDjHN@pRQ+q?WxZ3fsZq}_3VW@JtorU5A%Fcd7^$CmIO%%l;B2hLf41kpY|#hDJ)LaR#Z1uG8z+3k#f3_ z%ZcyIE-#o8WHF`y-n6P``$R6MDY}Jf)F#o`Mlz&oSrag-H83=u$JNVBz=Yup4IZIP zayARZ_6Pcg^BLHq4vR0QM_rv&b?1c)8c6-$#~Oqi8IPUqSo&FBIlmm`{BYiA+XIth z6OGjk(9J3a+4*E$uk=PKS6iPhSh9SU_%&(%!Ooj6#XbRF$o%yJPOz=Al=(5$5$*?V|y~H&eK}B9pM}{9Ku42u= zua!GI%i^n1+ZYAi4hf3JD*KySO}e!&2DT%NDd>=DNu)friy11zsOJ&s!Oo(hz=m{P z;Pq9;g;sFUIupeOZQrF(sI+OeQUB^&R5ZZF(A$-g_qUm%V0Dr}$xQJ=H3}MEC*@cc zO80ow9f>)vz-FH5%M%i58QCt)Ix`MSEWV9uH=DUMBMkKOWo%iDJ+yLdbYi1J%K0Zu zCgoWwKlEWe)R+u9zN3tZM;WY1OiI->Qf;=el_51| zd`&c%vJjxCj9Bg&w-u8s=#SkxB~vc*pEnjdi7X8a#f+7=)6|@?UGJ%2!*5t>Ou;%c zICR@;5PJsI!64h5%1>UhS;W5iX-j(wi8y-4y)2&w0xlLE!63I>keESfmsOZVofmCe zX#bHMs7uu)T{Ni`ten)o=%UHWM#eGiQ2- zB%$G`B@ne*Qsm9!l6P?qR7a8Y9Hq5TBtpZnmP#9f-ei)WJW<^I zJQO7onT}Ziu~;43g*TyLyQ-Cj9qsiEl3XT#e(~VCwC!d^A%4bBR8~s8;z4`BS)**x zQqsXw!mqv&!A*^I2ymKl-iu|mv>%@?{!*;$Us_F#BM%#GZk|_Vs+w3PFd*EB^QzN> z9~k$(RGnMY>AAek(3uHD_k9k=jd+~&RVGgjVHljGo0xBX-?ZE$H>FxWk-tRep*3xO8=%x(FwYTLX{ zPD)lZVi}Rqu~ci3@X$D_PTmtI%a$ApcQk?1JPnO(2SYw2i}i5j*OEB&J-VL!hyrq!(58gn}_@%EOs zm((jGQ+MX6 zt#a)VyBJ!*DBQ1$Fb`V4lBT3hmT`m;$9iZ3hjFxZNdK?2l}SlhPHQ@~R;E85kK(we z2b7N*7G55f_v;^Uq=YF|(8-dBdCQFjxK)h|dm7SA%6fg6Qtxm(9*2*_hLDRVu}Kl# z8=RsQ5B0;PL{rxi*&T3+gp_7ww_jh-_tk@u!91z+yu0l-l6yv7{)#bF0x+c#sSE(!nkv@uM742C)l#3L_9CVL;rD(0Cw zdS!m{31X{|Q>DVFr9+jcT2>h_^0fc4v%gJ6~#fw6)nVxsK(Tw*z9@ ziA&!ny!8GDFAaxUR%jdZD6;XZd8-PujXNjAs8=3jz1K31dXTHZD9#4R}3kCmN8aUV}pN&1T!2I z)5=ovf~l`25x97^eW`g+~#>g1BbCdROZ7?i4$U97@ zEHon^AckR=tgV;~-O0*BRES_0hYLb?++mpHwoX^Ne`Ng7o@8u%z}KBBruva3k;9)z z#3>V?Q)o-Vk&!(Vp^eel@4-c;0;d)9C54ID6SeT49LT0CT{##o9dfwaD~Tt9N;6@G z#jFAtaXRO#Lu`G+Ia9$lTD zXkHooYSa9qsiw5L5x49`J={?WXY17|mwNMM&yj(|5f^E3C~a=5S`ykVQ8IRrDSr~~ zkUe{3i*51B0;lr=siD4GbGky0$D_TYD=aRRTt@?#%$T#iN*xa3 zk-QQVj8Ly{D=O5X!mFYpf28 zlyHnS3a@Fn4J#LtH|pj6V%3|sFydUi*nD74bu#7C795@YBp2L6ndp?SRw4f2x(kq~ zj8oKF!A(-}$ugdHT3QfGhv}+ zo;4$-;mny7OLylc%CKIiinN&V%a$>$64++9ojA88LGR%(w_c#vOP`P~LM^*TImjTV z&k&=O1#^MO@LR97l~*81wVW>rN@Z)&KaH%blib<FlN>i2 z+^cst(`8)MU_Z@AuqLAd$y3K?bxzKmg(q#q1ph}8Zdj>PVJ@QW5ICE(42Tn$@l-Ua zCN6oWB-gto3LmSuLT9Qqx!KAo+B>op6eaTqhq+?4@%@9t20w^*MN*sF80rzNG?_`j3;}k^P$%kH z&EyZ(k77{M|C1beCA!}zARP0qPX1zfk521j4Dd}9hic^bt&GAzR*6ahSOw?xj!0}< zv1B>0)7GkMS5h}ojnY})uq!);3?1*7n5tx2#i3(Y5!GXzHtZ#j2{>DO@@o2l&eSH+ z<2V$fXX*8pB@X7$rQA92!xX}e6K&3znK|ukyL|ofL5Xy#g-!{$Osh4OS*5{@t2S{A zi!lgwb1r5@eZ7!kPnF1i_DbcKR$9&2!563Kj7lT(U2`_pAl`#fn8n;#%IMpNGK5y3 zfBe}8SH=2lM14e3(gK|%)d^zOgx$=`4;c4sevDa|I}XOuKPc1QSYXPEhw`nZiPTe0 z+Mr0dEjNo31)3(T!j!pFf1%W-SmH*W@#aHxBGB#iM-GWCx9vtNyLR4PVlP)%#)*pOUK5wg^qqWxqKPT5 z7KKiG&J;v3Nfp0X<&EEzkLFh{he^!a3`u*JII{C z`48=ep}%qzNl_$^|Hb#yw+d3%lx28U`zrNfE!A#lNF}m zdSp}_%VivVXzg*p$aczHN%Bg*mZ#q47hcly+DRl~>vfruY&pTGH?uxLeiqYUjM@P` zE><2na)lm!_;`qDcSx&a6uHP+HD#q;3)50N`uBB=q#E~$DT*dg;&wPVkGwE9hOxr^ zX?mdB$tD=}vMU49-?=K5Wy8Cj%=XJci|dvqa2*`CYyNs~LBeWNMTJ(^e2 zofCaV+Zj6OV~@!ot%7k9oy9_7)%ZiNeg-XA8QE4bQ~Y6{atBrtn0q(_5X;PHJ?ShF zZh2LS;-W8jd?I7>=6~!^Ch^>^6DpeNU_lv8z;~~1S?5mK)JgfF5eobZmV0G7iV*AD z`McG`#+BR69Y2=W*X*$Ze9tFM1)Cm>GK&k8A!j!;OsvmLjvVLa`D1F zRK;OL&f12H+^l+$cAGEiRx~{SO;dEr+sRC~hKTGbB`%w{#Tu}3 z=(IXiPu9*VL&vI#tdPh^E%aT4!scq1jzg(KBL;r~0yg((Jc-)@_!?y(PbqWM4tj_1tV}Ci(bbB%9vkyN4~_SjFO| zO{A&OeNno|lC%%p9gpd{jN4gV>&{HlKFNGf^4q*09Km<%&JlR!*wiGJ1dNb)+*ERw zfNyD7Xce&|Tf|0e(fwCF=z5u(k=&|hHwF%-ATROxmJS`pDzL`eD3zQtfn~upu$#y=jytHk zuG64cJG(}~xe~Ku#8Y*lQh!CP&qt|6&8645k3Scp&aO|O-xl-;pF<5wb|VgL zY%1w9-9mj#sxRs&0v`w4rlYFQKkasWG|<%N=w{?nMm$x6>#i2fTHCiCa<}p@hY>k8 z!BC|doQ8*04asvgvj!D{Vy}A6KkrU2+CfH(@ZTTqXukCwBi{}te$+v1y{B3+IiuQl zum3+v0V}_hJilS>4d3_s=V+0XVl` zm6dbC)=djd*qu%jTZFzUjXmbK{cJtkY4{Dqp(aVA<0fiZo@4D1QwQOX*pwoIRgK!SNqpzuvj>t(D}hRi^<4UIKHxm$S;9q4bOa|`AjZzmx9Y@+HB%~$R* zUk^?x%P+^6VL8T&_m2!QUDN9^*<4pmw_*sXQa>cx@FVG4kP2BSl zSiDj@6i}Oe`R@SA+4K%Zm-P!q+xcix@>7)tS%EJ{Sgbv+kkaq|(YJ%n0#`We#EN4y z^W+ouCk{0jib@mu?5QCAfc3wn8A*PnDB7cbJy$Kh?SX|)AY)#-_W-$V=1$;r*|lTeIEG#Qx-H5KR63! z;dbkQFfBU^76cou9(kF3h)0*m$2}f7{@Zx4)H_&4{G*+}W3rg;^qt*y`eMbkL~Oj_ z!}pG#gGYO%9i$ZjQGAVib9;`sdl_kqV~By5e_ZVzUL z7=42L!Z^6Cj3a2lZTgfnxXmd5tX=fFycBbr!5~XlC5|V9(NbUpP+o&__dAE=5$2Bmx-G22bVjq3#;J?pSsSs10 zek7*DCnb{cj5jvI*-&+pWV{}ZzMqZGEK=tv1yp;5>GI5VRh&8~)W|idFnHZBSD|$? ze`;~3j}=VWi?&VN53TUwsQDHgHzuI)S62LbLMKcQ{c#rmt1J@}Z1mr*UzMOQXqIGa zMQ3#+M8*-oqJbGtJ9ciEn8yUGOXqVZ8t|PgA|$l(pB0$WiUKH|$n{ofL>*2vxmip0 zIMbyMvqOiao>%ng+3Olq(g~U9wx}Pk(z?ZdsYX^Exgw2CizFRO>g%$lUWJcISu)_i zCaIIyjFhx^0T!$LR&#!EgvD$Qb<3ZKWE7GCr`RY4{og6(-6L&|0n2n`GoDY#Z|@=~ z`EE|N)iGPzl2Ep7yDC4!I!iM@B*6+kQOPM)D>iMDsGg`#7ZD2K5ZiaaOq5?p+HVs@ z#k2EymR5H@_@W1*z12>3QiP{C3gmz1CrXPqpk2xuf{ch(kZw628{k~x=|~3cUTV9qH_%&aF!FVp2t`jr)ZB=sktog`Iu}$(fuO zj;KJXN}gnxFj&;Y&Rj%O`k!+tDe9;1pDOJmN55afM{GJ!Zr{mLFpaB;)(2d|R5jJ? ztkIG;Zm94kxACk|sBbRqhduOJ6V?>BuVD#D1&_YAlAVPFYS5O$gg&cuQ7%Ob^V=2l zItE~Vy%C)=G9epaDu>(x@ivb<%F7cR&RVghc-04Jn)W*6xx|0Y_fhh=a#|V_7 z|FDA3*4-PmM%3bsuh?C8MD9gi_B3V%K27$9g~BzEx2vHFIc1)t4?jL_JN7W!uZ08E z>2fY9wAUyjPFAEcmc33YfY(RWbUtrLBe^;}-8Yb?7>uFmF>hA{jDEg@^-D-^fC}6X zNdyIAKuyZBTQ%zAlz1BfWptg4K`PNhPmCt?YJQ@P-*&U#4sgdR;^ZS=w@QtD-M5p0 zcD$s`1nzKgP?ctej6QwwQSD%#TP8?&1o}xGRF1$JMs*a_vL37t(hYSbd?LUIBOA_Q zM>d^S90$=zEFS6`>tq#TUeIaR=ygtCiQZ{dR2f8Wl?eSYa=O9?wqntJK*u1g=7Zi} z82n%J;3SkCJP6!ps{U8|0)Eq~(JeOvKF)AGM#4oRAufb7d365uAi9K3BC^wl>hAYI zI-E%?txxMS%^pK5dw6XH>nf8|QR&DYqu6hj{?cP0(x*?@yJzpKdi1=uNAHh^y?Qf4 zUf%QCo;{0(VHfM+7GhZE^;-~pFEXxr(&X@Ze{&PN*E-f+f;sp`qTA2a_9#y#YWjq+ zyH9vuRnhRF!w2QF(W|7V`}h~h&_l~IzT9QM;tFI-s`fhdN`82%;fB%WgL_=ds*x2DebRnsxTE{(LT9f+NAE(%RfUdg3mn<$J6iTGaFt)& zwO_ZO_4xy>>$YYW&dY7tn0>CPedX%-*N0trcK@T<+4Hlz7v^T|avr%|&Dl9?b4$19 z9$VMCYOC{WU$diq{UUt>;+!qb)5CuJ5V3jNQ%kqBPTN!5wLd@kP&&G}>i}p!N1WTd zF8kQB+_H_?gUg(M?ujY6H5+rQcJdDA(Ykd?%l=1Oc5lfoo#{MUb~oo%?`_+*rET*R z=aJnoKRfG1cborl;oOq#x%pe$wjOL>wzc3lhwV*mTeoNTFZqA{`^4E9)3aNayN_M+ zFKXR;uyym?!+Ki1x*!>Fs4w1H+;w2Lpl$m@ZQJZNJF_XfeUbCc&RW~PVuQPF**&*y z|Nes8)~)MW7tYA8*lf@5e>OX<@YWhCWTVf*=kLl+-%cGlSL?o|Z3hSbDk>hdNX-g|2V^- zxa&~zaNyDG^o`d0V`r3IJhEG7w>>eP3eT;Z^_wYEv-4-R?%gEktfe~jqBOLO%Pm`* zTQW<@gf06IQeu~Q?%}5FhE3U7Pq`A48vWPZ-a^H!G2dIs!~Xr`!QN5aby&B+l5ju! zMUTy`-IHCoEjx!a3A4z$)q7iZ?{zQrLqex%TXxT(`?=w!=x(e;BNPuC1*%~6z~PZc zZsF?Io$Fl(#gCuIrd7 z%TMdm%d$=MVZ3#)n4eR&A*I$(Kg7R%%Ix-a_Kucj+IOybv`?8v?IHzRc0Zck`r>b< zOv^qwE%*59mfiCu3b(F&XWzWGZM(7u_qQ(D=n`t%xwHMTweE@LwoNm5b#d2HDrV8T zwuKM3&R*df1dr^jT@s9Lm}O@zYg*~4ghnualxmxKMV6#C%bejba7r(F)S`Mzu zP2J`m^QR^(KiTJ|g~hq)>nV3P7wUe;xqaOW?T@W5?s_{l>Bj-L?W>TrpJK>iT{`3^ zo$1lJ=cWb4*`{p_1B%8kI>%E%)(Lw3s54RsxklHqd09TIpKfJ7cGEQfBXy|*EzJvZ z%QmTAovlDq^Xvmi+m^jET6Rxq+5L3e%%>SgEqnByPy@Sd*S73|hZ$JeUC$$Ma?>^| z%AC!-3%SJ*4Ezr}XP?+kg;_;YyUY-=z7m)g;$ao$9psN^WO!`}&6|Szx-DM}8Xe9#k^b@2694*6uOkKy8QFtq*0l&&bVwCfC%Io&F*% zsHn52OHBrMXl<=|sfLvHR9c#W5Mn9RLrxP{3jUGozG!QzshifkqfkBf(jLid5q;Rw z?PSzML+k8Wxz*1y741Yj3#^gxU^3|7cxT^AhN3A-odc-$Y}34!-LsLbx&z4Lpmp&Y zRFWXqv=D`=W#97b^jWA?_M^#%dh)Am*}Y5JA41)nPG@JHcv1@XrbkF|hre?xj4gJ^ zUwvfyMidNIE!hnZTaYQYZY2}sUTA9DG%NnSFvP zbV#Nk8hNzaTsTJJ{?9?Rm*F7{4nyp&FEoP`U7lEop8)rt7 zh-c7w_?~HHLRsR@InG=ArCZ){0PHG>y7^B0!}Buio5x zu$d*EpSX5!DY8U;q=gIGS5INYPyrOd^wa1S`YjfTVU|bRXZTUc3!mA>$XDMW^`>of z50;>92WMIYY{>fFGyVx=YL8WhrR%GA)>1O{>nW>E>HGB*&2fq|e;t)!iU9pI_t-{D zv2EK%d!;Hw3eZ%fqnoEppmuQNR!>n&IJxSkE@v(=H>ul-ZcBF6{-Drq!mlCQuO;Vp zPiOUkj1}TWvEC_VOFBnx@r&)NW_YpSmN7oz+>XudGv~0NWz;MT+NWy>nc5LBA+~MZ z;FU7dsaTwQ)hd2Z>;6Tp&8u^(=V#}vduQJ)t5z)ihyFTpXsN%mZ+1SJu+)EI7#60J z56^C0o?E&n|Fg&+5bM#W8FU@Oq@B3D=4L;YeQpV}gLl&)%n$90w`S)r$6 zKX1?9K@VYJE3xG)HbHIyOQsh+q{5m9cd@$N!HYV~yP4R%Lu;cM&(r;Fx;ASTa<8cE zz(eiNEZ0z+vae;|N_v`)@yM3k;seaZEqgaHIcA$*Y&*DyPTaD82BOCIzo&JQ>LP4Je!*@1)r&`C~~>p8oAJ=&_Y}HJw+aDn7h6K+7j=F& z0cU5q*$|<*TJy6PQqHc&Okxi(VH6d$FUBme%}rwbY_XrBIz4PV4B0IU{aD+u=Rc%T zgnUEhM=Pn7?$?VgUF&(K&T6)@8tjmJ1Om1W+DN+lyuCqo=To`84@q-#W*kfxDwhYT zwy#*|?PJbx=L5p{a};l?@%}b9=bUO37PENIHZ4Wta%px9vspR{lR$O~ZJvCaJZFbjYbY9xc(zF_4 zSvPNbcHuL=EjQ22?rkE9B~@taBG+#D8@2LE?`}J|3L_+sSUYHuC<70W4zblv)3p}lZMACWw)a;gJ2%`Y2p{@>Ev$-Wcwlu4OZSD!$SIlqO zzo0-r*6$V{>WO~b-n<+MPNT^L?ZKp%v-OU%)cE@Pv@H)l8|3ya#zqvhG|z2qo{ybS zcHyA)$*IiHCK*`X<>qX_I!=}P02Ys%<6qrmXxW&L%uO?`D6{weEviv+clU7HS9tv|Fo1 zEr_=_(dx{bd5y4B61iv>07z3M_uG3@pF!5_rFo;hqx{^_zHBpnCi2(+57WyI%XmDFHQpsl(C}IG zyMY^db+mf0Qp&wBHMf1I>c{Q(FtVtK)-_vH{$)E6pIJsw&&2q_qH@Y(bBvN zg#k_6HsR3F+t(fBSiCnv**R-iQ)&Izc5u0^I0_}5H6J~|7isH5 zny9l=SLe2@w^IAfG{2p6s2$ZyJ*TYrNH#fnYn&`L&dAKxV^`>J?zdfzpMmCsa*VT`{9zFxnUbc(; zA7k2rUOS9Vc|>8*V0Q0v|9>$11z^jPGLPYH@tRl#ROOh!x0+#C+H|4*P*{WhHEwR8 z`(jzv2)6wstJzK!Bf8i0v?vNV_+&mXgU&+X)ZtkC(l+&UH&bTRS-Ig_T%t_9pFGJ6(X`W}}I`lCv;c zDimls8j7R8B?7t`j=0%ao4_Bw*>#vLrwH46u= zTUOCz-c+E83mtjG!)yooine-T@CMm^ds#}iHm|Vujpv2@)RSAg4{Z+3M}}_q+OUjC#VN-6r24KDA)06V5Ai`q^MJ zRhg~Go5?-=62Z^RZZD=|NOS;}o{K6Lc&8gaPVc8K!j5qYb#0m5y!C59Lw)t-<%uz2 zcGgU$PM;nY`RuG`5Cl4^;bHCX4E3`XqFMS0Z28*kLt0z6T4-9>a$uDn zCMeQp2|_Dn*V_FYunY=rn(}6739i}nddo)3!L2+>Es#8ti&o^lMSkgUi0-fPf@UfM zz0Rj7YR4jLo89|ph_V>c}!TuRm4OrFMr>vznQXkZct(M(fHGQK;M`7XglGv=&A*`b!Twqu6 z#u3>Eoo{@Ofg>I|3J@VylDy96N0pzuy##h^ODnL9mp;%{8j?xJk6ib3cM^UKFgK@J zm@T>N@KA?<4qn!jK4~XX-K^-0^KQ~wGE3vs0dLaScx=M`yT*C zZ}#??|C_t)tLLKTqn#C(z%h9TT z-@cYT*zGo(mTops&B<)?xD6y$HVlf^=bE#R&yAZv<%zojzNTKFl->Uz+Soh$Ru#7A zA>k}Z-I*xf+Wy$%S$a~zd5>tbVUA@}(|S}O8`6qlzjlUr=~B&i!g`B#P&==h*h*4hFxOX}y}@epuJG zLSr2k(ZjZrHmLzyMvS}1bZ98HQR8l1vO12skVqUqQ4jt%L zEpDfm(Z+0SD#YP^8FZ4+^vwCRGIE2}0flu(1EXK!wfs{VN{lm9bX%DlU)9Mtw@l1Zfh{MVcL;RzqVkU+kEAIgV)UVTJGu$F;$2>56rx)4HgrDnzvR? z!|oO2w(oA)GriD&;VlGep&gr%?er<%7Ov5~-loaJ?MZHt=Sefw0p zgL-g$5-s=eL6XkRKA4+39YL90(CoJX*&B050NwEl*9Gj`f!t#cu^C3yAf?(@JjF}M zpv_#lNUaR~nym0dFalKj;{m;mer7eFoAoq$En7JHrM2GOlwkzPb8ZvW1+k&YX+)+R z2_M!U`JV^P702qe zwtXrw;Z7|&xzvjE+`foC_xN{q{f@uyczT|1RHS3LL!9l)*C3U>c()Zo&^GU(?5ds7 zD!^?E+PG?8vYJ((iIV7Ncl}zSAXd(;6%~s~dcj7{B_Lc{o11bk%;k8Ey1LUtv90Oc zHAB+2HlyR#I`e|RO`PJ7hWp z>#8|E72x}_Q&>!dD~l#aY)P(pXaCkPx8M;r#O#c^`DC|p9Op@Qpx0;G4fDgC+M#B( z+MBakE8r!wB!>#L*zo_U?Od9xI<6>u$NCjFo+VX@GtMBtB9))viQ`I!$QVmJESUg~ zEtGL zOfk5m!k8J~rlg{v@&Iieqn z=Px_1)C?;pGAubiSxyIOCkcs;?vY$g^kgTkO~0Z|g{YaTBoULm>65z$qGmNIjv#C)IWbc6)2_WfS)m%1naG;7yjvh)NSTZggr6q1X~0??1C z&0qdHfB)n4YThdIzVvJZuRPg8SwZ{*Ox#ij7kpb}FE{f3^)BgL286v*wJY!d0b)M4 z%i+$=c7^V#e7!is#rn!01QjvCh%S~Pma7vx=&KDl3JIf%5P-BRA=ylYyzuHnD;=CY z*d-O^mpWh)mL8h2R}%k|8Pdgk#akV0EvA7^lT+go-)~ckyLRV|)4=Nbd1uZkRCPCI zzVn>g73-iR@R&I;kGGtJ5MSy0s}>+Fr>oX!+-sbObFMF{=Smu7^VNd|#rChRi7@?1 zC&hy|yBi+~x9~?m?Y%Dk;_k)jM~{To0>*guIW*}Q#f=X!BI1!{C7H~UIKan-D5y2a z-^>0Mf$F{OsS57&+nngiV*Mweu~2}gJ0H&{e5@POAI~e0EKx=F+tHUx`A9R6L_leL z!PC757!mjsrllCd%&wdjZ%l9bl+*=V zIxCFZR37UEZxj*k1lif~^{a1R9n>1ZPsEqyWsbig+`ki$Gl_m)o_>FHa98f^ZMSA* zR#hPFWb66KOF6)po1dQkh_mVBt64#Nj-=Ig8_~X5uC2gz5}5JRI+8${-MJ(r-zX;8 z_TKT~>!qx&8A?SIny|TIp!JOvXdzO1hA5s6@0bDwoTHUG-n@xl%`5bxyGi2;C0zU) z!8%DAu>|8$(1VjrV2!I?&L;svHte@Ql!9@7&gp|E(T21diFd*uZd@UtqhO@U?iwT` zaLFIkjQHqp3SmkMb48`NeEX{C<$%YstgWfkY0DUsel?wqi<~_+L)a^juWc46j6hrD`B-7!yIX1=(4ADm9dn>;Ba_|-!rOi)Hw zNlYd-CRFOYDupF~Z7JO-mm&`}4K%(zVot-q+EHxv^B25OEDX(7mKF;QKvk30U@6pS zq8Hh%@>R|0;~6(|2n7KZ2q*iSjhdV^E@rdz9x+(dEdIZss%K(0j-9Rg{eacHNSH+!E7MCf0dw7qP=&ISKcL9j2gnW7~sRVNB=?mFgByGe+rnBeV%VG+t}z|x4Y%kCRozVFdzQG z1fMK?x)=V&9NI7{E0K)_=ymol#gzl6X<6LM@wB zP@aI67JT#M@@AG+Yv5yYevSKx7c3$KQwkdg96^c!bkmD7C>354IMLR#_o5Ty<7acf zU-nv>8RzY%H0F@wgoWSP<93-WBxAg}NsZf+NB`8v9==%odxJnAp4m2+#H{-@PcLDi z^DII9EEZY)diwj@)v~{cToL7D_+>w2VosWa87 z?a|iX0E-g+o$z99qiE-pB@pl*O`g*seeXz9X3{c>SpA^u^Hy|QLNoYrlR$kI*q_2E zE4p<#p{1~-9(m=A-=F&>0k!^58{9%R_vyN&rY^`$`bTd#*iUV$?c zPwfYZrBQHPSy)^BVyxK3mz%0y(LN#$*IGC*{c4b0sU08t0^WA-2{?bXQ%M+<2WXKm zxvaN$WS3Y4s^RWqqy`;I#J8Z@Q^|vAb*8%`G#qtNrHOOlm%vwAido0ex_Br|TKeK+!G&IKnx zMP}!446Ng>MIVC!ew$g&YBTjX1&f;x2SbD7|mijmS z&oVthRAG?KCoFoo>02940xft-5_bT&aupwKvCXGn5_M@13?&DgA^dc9`ssJd4Iok0 z-j=e5W{C;UPF`;*Oj&!%!aJv`T_VccNOWCfb_;6$;d1TSJmC|VO`3WEgE1m8QB<;6 z;5k8QsB@Qgs}#5KYHd&m8{NXjkaNb_bOI~q{0pEUB!+DUorCjbI3(uQYE-{1eT=NT zwKfs?l>h(WnF@BZrz&(&VcYi}#vFEnB9L|+84J~=KfE=4GOK|$ZT57x6l}pv50)q2 zWgSBgvAWiZt3umR6XR+3-6#&NpJU18B1$l;R+Cgm_3f?DVR3V-{Kz>TVxmdxG^}C} zvq#9TF;f$;D3RqzuH9(SfasI4`*@rsEp0vu>rR0Bw{oi-NL*g4lT>r&K9#F z&z&=whh~c*>1-ym1v!qGD&5L`lgoD*t_PIBdGuX_{@Y&=NA;Ch(3^HN=(iB0>k#?@;ZB= zv!=AYo_rn36z6R;PURopSWi~N6lw~Z8gZAZ4!!a#J8k61>hAgSuZbaLiqf9{G5dFa z`1>dS7|KDo3asA!J_*yCr~CI#VR|}uzLJeQz@6xkE5G(@QOdr?p0|UV`Vr3W=J?8X zz<4xv-B(-27knJdhp3aFQ#ll674Ed+uL=p&@A9L1U5TQ`IGsr2G9%WJN3k~OJQi*< zIK7_%F>p(OK`J$OqjIhz>LdhV*6<;f5|uOHu!~FqTb0`XPLQvT3oYs!OMBAp^U^y# zC!y*wbQe+#GjxsU}=!mNEBa+58S(#2d<;-C06wR8G>FzX(v9HX5kfC$;J`;cX z>_AiwPwE*QJ*Rm0uGmx@D1Y!Hm^BAQ4&Yeb-(P-I&m{9rQ!0S)(#hqgC{m`0Q_+gL z7I=F58TAYjO8PbXfz{4abtx2%TuuLcI?}R99)Gv`&a#D|gC52*{%U zN{6DYkv-r^YnUky`(L5sT^qLd9nR9ASAnn z?IS$O{Eb9fZDb6_U~gOl=c(x`(jpmYGa)J;&d6&TKzaG22{*>7U>c;;|!cB4|W@BErwt$vk9st~E1@5K(dEyLfK9 gdD`b3w(!+*DceLF--R6#> AppDir/apprun-hooks/linuxdeploy-plugin-gtk.sh -VERSION="${GITVERSION}" ./mkappimage.AppImage --appimage-extract-and-run "${GITHUB_WORKSPACE}"/AppDir - -mkdir -p "${GITHUB_WORKSPACE}"/artifacts/ -mv Cemu-"${GITVERSION}"-"${CPU_ARCH}".AppImage "${GITHUB_WORKSPACE}"/artifacts/ diff --git a/dist/linux/info.cemu.Cemu.desktop b/dist/linux/info.cemu.Cemu.desktop deleted file mode 100644 index 6eeb0120..00000000 --- a/dist/linux/info.cemu.Cemu.desktop +++ /dev/null @@ -1,27 +0,0 @@ -[Desktop Entry] -Name=Cemu -Type=Application -Terminal=false -Icon=info.cemu.Cemu -Exec=Cemu -GenericName=Wii U Emulator -GenericName[fi]=Wii U -emulaattori -GenericName[el]=Πρόγραμμα προσομοίωσης Wii U -GenericName[es]=Emulador de Wii U -GenericName[pt_BR]=Emulador de Wii U -GenericName[fr]=Émulateur de Wii U -GenericName[it]=Emulatore Wii U -Comment=Software to emulate Wii U games and applications on PC -Comment[fi]=Ohjelmisto Wii U -pelien ja sovellusten emulointiin PC:lla -Comment[de]=Software zum emulieren von Wii U Spielen und Anwendungen auf dem PC -Comment[ru]=Программа для эмуляции игр и приложений Wii U на PC -Comment[fr]=Application pour émuler des jeux et des applications Wii U sur PC -Comment[nl]=Applicatie om Wii U spellen en applicaties te emuleren op PC -Comment[el]=Πρόγραμμα προσομοίωσης παιχνιδιών και εφαρμογών Wii U στον υπολογιστή -Comment[es]=Software para emular juegos y aplicaciones de Wii U en PC -Comment[pt_BR]=Software para emular jogos e aplicativos de Wii U no PC -Comment[it]=Software per emulare giochi e applicazioni per Wii U su PC -Categories=Game;Emulator; -Keywords=Nintendo; -MimeType=application/x-wii-u-rom; -StartupWMClass=Cemu diff --git a/dist/linux/info.cemu.Cemu.metainfo.xml b/dist/linux/info.cemu.Cemu.metainfo.xml deleted file mode 100644 index ef59427f..00000000 --- a/dist/linux/info.cemu.Cemu.metainfo.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - info.cemu.Cemu - Cemu -