Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8244301d | |||
| a956d0b0eb | |||
| d8e59a28ea | |||
| 67bdd8ed58 | |||
| 0a4f0b6a5d | |||
| e77d2b2103 | |||
| 6177cbdbe1 | |||
| a1d48b5f52 | |||
| fe83ee102b | |||
| a139fdf4ac | |||
| 30448641f2 | |||
| c49c3e9f27 | |||
| 053da44ecd | |||
| 13891fd62d | |||
| 5aaafa6a56 | |||
| dfec9c9a43 | |||
| 7a8f484020 | |||
| 2d8eba5baf | |||
| 7fc5af3622 | |||
| cd81194fc0 | |||
| ef98828d40 | |||
| d6969fa7d4 | |||
| ee35f7adf7 | |||
| 5130b8a6a9 | |||
| 1559477740 | |||
| a83eb90a78 | |||
| 9cf52d027d | |||
| 03276e7490 | |||
| 6c449793b8 | |||
| 84815fa879 | |||
| ec95c73a12 | |||
| 1449ed9dbf | |||
| 58783b8a46 | |||
| b1ca56bed2 | |||
| 22fd208e8d | |||
| ba661c8d9a | |||
| 5adbe66ae8 | |||
| 46a962eb5d |
@@ -0,0 +1,31 @@
|
||||
The icons in this folder and its subfolders have the following licenses:
|
||||
|
||||
Icon Name | License | Origin/Author
|
||||
--- | --- | ---
|
||||
qt_themes/default/icons/16x16/checked.png | Free for non-commercial use
|
||||
qt_themes/default/icons/16x16/failed.png | Free for non-commercial use
|
||||
qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
|
||||
qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/16x16/checked.png | Free for non-commercial use
|
||||
qt_themes/qdarkstyle/icons/16x16/failed.png | Free for non-commercial use
|
||||
qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
|
||||
qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
|
||||
<!-- TODO: Add the license of the yuzu icon -->
|
||||
|
After Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 582 B |
|
After Width: | Height: | Size: 460 B |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 680 B |
@@ -0,0 +1,14 @@
|
||||
[Icon Theme]
|
||||
Name=colorful
|
||||
Comment=Colorful theme
|
||||
Inherits=default
|
||||
Directories=16x16,48x48,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
@@ -0,0 +1,15 @@
|
||||
<RCC>
|
||||
<qresource prefix="icons/colorful">
|
||||
<file alias="index.theme">icons/index.theme</file>
|
||||
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
|
||||
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
|
||||
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
|
||||
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
|
||||
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
|
||||
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="colorful">
|
||||
<file>style.qss</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
This file is intentionally left blank.
|
||||
We do not want to apply any stylesheet for colorful, only icons.
|
||||
*/
|
||||
|
After Width: | Height: | Size: 401 B |
@@ -0,0 +1,8 @@
|
||||
[Icon Theme]
|
||||
Name=colorful_dark
|
||||
Comment=Colorful theme (Dark style)
|
||||
Inherits=default
|
||||
Directories=16x16
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
@@ -0,0 +1,57 @@
|
||||
<RCC>
|
||||
<qresource prefix="icons/colorful_dark">
|
||||
<file alias="index.theme">icons/index.theme</file>
|
||||
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||
<file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
|
||||
<file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
|
||||
<file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
|
||||
<file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file>
|
||||
<file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>
|
||||
<file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
|
||||
<qresource prefix="qss_icons">
|
||||
<file alias="rc/up_arrow_disabled.png">../qdarkstyle/rc/up_arrow_disabled.png</file>
|
||||
<file alias="rc/Hmovetoolbar.png">../qdarkstyle/rc/Hmovetoolbar.png</file>
|
||||
<file alias="rc/stylesheet-branch-end.png">../qdarkstyle/rc/stylesheet-branch-end.png</file>
|
||||
<file alias="rc/branch_closed-on.png">../qdarkstyle/rc/branch_closed-on.png</file>
|
||||
<file alias="rc/stylesheet-vline.png">../qdarkstyle/rc/stylesheet-vline.png</file>
|
||||
<file alias="rc/branch_closed.png">../qdarkstyle/rc/branch_closed.png</file>
|
||||
<file alias="rc/branch_open-on.png">../qdarkstyle/rc/branch_open-on.png</file>
|
||||
<file alias="rc/transparent.png">../qdarkstyle/rc/transparent.png</file>
|
||||
<file alias="rc/right_arrow_disabled.png">../qdarkstyle/rc/right_arrow_disabled.png</file>
|
||||
<file alias="rc/sizegrip.png">../qdarkstyle/rc/sizegrip.png</file>
|
||||
<file alias="rc/close.png">../qdarkstyle/rc/close.png</file>
|
||||
<file alias="rc/close-hover.png">../qdarkstyle/rc/close-hover.png</file>
|
||||
<file alias="rc/close-pressed.png">../qdarkstyle/rc/close-pressed.png</file>
|
||||
<file alias="rc/down_arrow.png">../qdarkstyle/rc/down_arrow.png</file>
|
||||
<file alias="rc/Vmovetoolbar.png">../qdarkstyle/rc/Vmovetoolbar.png</file>
|
||||
<file alias="rc/left_arrow.png">../qdarkstyle/rc/left_arrow.png</file>
|
||||
<file alias="rc/stylesheet-branch-more.png">../qdarkstyle/rc/stylesheet-branch-more.png</file>
|
||||
<file alias="rc/up_arrow.png">../qdarkstyle/rc/up_arrow.png</file>
|
||||
<file alias="rc/right_arrow.png">../qdarkstyle/rc/right_arrow.png</file>
|
||||
<file alias="rc/left_arrow_disabled.png">../qdarkstyle/rc/left_arrow_disabled.png</file>
|
||||
<file alias="rc/Hsepartoolbar.png">../qdarkstyle/rc/Hsepartoolbar.png</file>
|
||||
<file alias="rc/branch_open.png">../qdarkstyle/rc/branch_open.png</file>
|
||||
<file alias="rc/Vsepartoolbar.png">../qdarkstyle/rc/Vsepartoolbar.png</file>
|
||||
<file alias="rc/down_arrow_disabled.png">../qdarkstyle/rc/down_arrow_disabled.png</file>
|
||||
<file alias="rc/undock.png">../qdarkstyle/rc/undock.png</file>
|
||||
<file alias="rc/checkbox_checked_disabled.png">../qdarkstyle/rc/checkbox_checked_disabled.png</file>
|
||||
<file alias="rc/checkbox_checked_focus.png">../qdarkstyle/rc/checkbox_checked_focus.png</file>
|
||||
<file alias="rc/checkbox_checked.png">../qdarkstyle/rc/checkbox_checked.png</file>
|
||||
<file alias="rc/checkbox_indeterminate.png">../qdarkstyle/rc/checkbox_indeterminate.png</file>
|
||||
<file alias="rc/checkbox_indeterminate_focus.png">../qdarkstyle/rc/checkbox_indeterminate_focus.png</file>
|
||||
<file alias="rc/checkbox_unchecked_disabled.png">../qdarkstyle/rc/checkbox_unchecked_disabled.png</file>
|
||||
<file alias="rc/checkbox_unchecked_focus.png">../qdarkstyle/rc/checkbox_unchecked_focus.png</file>
|
||||
<file alias="rc/checkbox_unchecked.png">../qdarkstyle/rc/checkbox_unchecked.png</file>
|
||||
<file alias="rc/radio_checked_disabled.png">../qdarkstyle/rc/radio_checked_disabled.png</file>
|
||||
<file alias="rc/radio_checked_focus.png">../qdarkstyle/rc/radio_checked_focus.png</file>
|
||||
<file alias="rc/radio_checked.png">../qdarkstyle/rc/radio_checked.png</file>
|
||||
<file alias="rc/radio_unchecked_disabled.png">../qdarkstyle/rc/radio_unchecked_disabled.png</file>
|
||||
<file alias="rc/radio_unchecked_focus.png">../qdarkstyle/rc/radio_unchecked_focus.png</file>
|
||||
<file alias="rc/radio_unchecked.png">../qdarkstyle/rc/radio_unchecked.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="colorful_dark">
|
||||
<file alias="style.qss">../qdarkstyle/style.qss</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -5,7 +5,21 @@
|
||||
<file alias="16x16/checked.png">icons/16x16/checked.png</file>
|
||||
|
||||
<file alias="16x16/failed.png">icons/16x16/failed.png</file>
|
||||
|
||||
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||
|
||||
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
|
||||
|
||||
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
|
||||
|
||||
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
|
||||
|
||||
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
|
||||
|
||||
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
|
||||
|
||||
<file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file>
|
||||
|
||||
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 614 B |
@@ -1,10 +1,13 @@
|
||||
[Icon Theme]
|
||||
Name=default
|
||||
Comment=default theme
|
||||
Directories=16x16,256x256
|
||||
Directories=16x16,48x48,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 339 B |
|
After Width: | Height: | Size: 676 B |
@@ -2,10 +2,13 @@
|
||||
Name=qdarkstyle
|
||||
Comment=dark theme
|
||||
Inherits=default
|
||||
Directories=16x16,256x256
|
||||
Directories=16x16,48x48,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
@@ -1,6 +1,13 @@
|
||||
<RCC>
|
||||
<qresource prefix="icons/qdarkstyle">
|
||||
<file alias="index.theme">icons/index.theme</file>
|
||||
<file alias="16x16/lock.png">icons/16x16/lock.png</file>
|
||||
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
|
||||
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
|
||||
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
|
||||
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
|
||||
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
|
||||
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="qss_icons">
|
||||
<file>rc/up_arrow_disabled.png</file>
|
||||
|
||||
@@ -337,3 +337,19 @@ proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
|
||||
The icons used in this project have the following licenses:
|
||||
|
||||
Icon Name | License | Origin/Author
|
||||
--- | --- | ---
|
||||
checked.png | Free for non-commercial use
|
||||
failed.png | Free for non-commercial use
|
||||
lock.png | CC BY-ND 3.0 | https://icons8.com
|
||||
plus_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
bad_folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
chip.png | CC BY-ND 3.0 | https://icons8.com
|
||||
folder.png | CC BY-ND 3.0 | https://icons8.com
|
||||
plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 from the Citra team
|
||||
plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
|
||||
sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
|
||||
@@ -1057,6 +1057,7 @@ IApplicationFunctions::IApplicationFunctions() : ServiceFramework("IApplicationF
|
||||
{120, nullptr, "ExecuteProgram"},
|
||||
{121, nullptr, "ClearUserChannel"},
|
||||
{122, nullptr, "UnpopToUserChannel"},
|
||||
{130, &IApplicationFunctions::GetGpuErrorDetectedSystemEvent, "GetGpuErrorDetectedSystemEvent"},
|
||||
{500, nullptr, "StartContinuousRecordingFlushForDebug"},
|
||||
{1000, nullptr, "CreateMovieMaker"},
|
||||
{1001, nullptr, "PrepareForJit"},
|
||||
@@ -1064,6 +1065,10 @@ IApplicationFunctions::IApplicationFunctions() : ServiceFramework("IApplicationF
|
||||
// clang-format on
|
||||
|
||||
RegisterHandlers(functions);
|
||||
|
||||
auto& kernel = Core::System::GetInstance().Kernel();
|
||||
gpu_error_detected_event = Kernel::WritableEvent::CreateEventPair(
|
||||
kernel, Kernel::ResetType::Manual, "IApplicationFunctions:GpuErrorDetectedSystemEvent");
|
||||
}
|
||||
|
||||
IApplicationFunctions::~IApplicationFunctions() = default;
|
||||
@@ -1285,6 +1290,14 @@ void IApplicationFunctions::GetSaveDataSize(Kernel::HLERequestContext& ctx) {
|
||||
rb.Push(size.journal);
|
||||
}
|
||||
|
||||
void IApplicationFunctions::GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.PushCopyObjects(gpu_error_detected_event.readable);
|
||||
}
|
||||
|
||||
void InstallInterfaces(SM::ServiceManager& service_manager,
|
||||
std::shared_ptr<NVFlinger::NVFlinger> nvflinger, Core::System& system) {
|
||||
auto message_queue = std::make_shared<AppletMessageQueue>();
|
||||
|
||||
@@ -242,6 +242,9 @@ private:
|
||||
void BeginBlockingHomeButton(Kernel::HLERequestContext& ctx);
|
||||
void EndBlockingHomeButton(Kernel::HLERequestContext& ctx);
|
||||
void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx);
|
||||
void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx);
|
||||
|
||||
Kernel::EventPair gpu_error_detected_event;
|
||||
};
|
||||
|
||||
class IHomeMenuFunctions final : public ServiceFramework<IHomeMenuFunctions> {
|
||||
|
||||
@@ -165,15 +165,15 @@ public:
|
||||
static const FunctionInfo functions[] = {
|
||||
{0, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceName"},
|
||||
{1, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolume"},
|
||||
{2, nullptr, "GetAudioDeviceOutputVolume"},
|
||||
{2, &IAudioDevice::GetAudioDeviceOutputVolume, "GetAudioDeviceOutputVolume"},
|
||||
{3, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceName"},
|
||||
{4, &IAudioDevice::QueryAudioDeviceSystemEvent, "QueryAudioDeviceSystemEvent"},
|
||||
{5, &IAudioDevice::GetActiveChannelCount, "GetActiveChannelCount"},
|
||||
{6, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceNameAuto"},
|
||||
{7, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolumeAuto"},
|
||||
{8, nullptr, "GetAudioDeviceOutputVolumeAuto"},
|
||||
{8, &IAudioDevice::GetAudioDeviceOutputVolume, "GetAudioDeviceOutputVolumeAuto"},
|
||||
{10, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceNameAuto"},
|
||||
{11, nullptr, "QueryAudioDeviceInputEvent"},
|
||||
{11, &IAudioDevice::QueryAudioDeviceInputEvent, "QueryAudioDeviceInputEvent"},
|
||||
{12, &IAudioDevice::QueryAudioDeviceOutputEvent, "QueryAudioDeviceOutputEvent"},
|
||||
{13, nullptr, "GetAudioSystemMasterVolumeSetting"},
|
||||
};
|
||||
@@ -183,6 +183,10 @@ public:
|
||||
buffer_event = Kernel::WritableEvent::CreateEventPair(kernel, Kernel::ResetType::Automatic,
|
||||
"IAudioOutBufferReleasedEvent");
|
||||
|
||||
// Should be similar to audio_output_device_switch_event
|
||||
audio_input_device_switch_event = Kernel::WritableEvent::CreateEventPair(
|
||||
kernel, Kernel::ResetType::Automatic, "IAudioDevice:AudioInputDeviceSwitchedEvent");
|
||||
|
||||
// Should only be signalled when an audio output device has been changed, example: speaker
|
||||
// to headset
|
||||
audio_output_device_switch_event = Kernel::WritableEvent::CreateEventPair(
|
||||
@@ -246,6 +250,19 @@ private:
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
void GetAudioDeviceOutputVolume(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp{ctx};
|
||||
|
||||
const auto device_name_buffer = ctx.ReadBuffer();
|
||||
const std::string name = Common::StringFromBuffer(device_name_buffer);
|
||||
|
||||
LOG_WARNING(Service_Audio, "(STUBBED) called. name={}", name);
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 3};
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.Push(1.0f);
|
||||
}
|
||||
|
||||
void GetActiveAudioDeviceName(Kernel::HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||
|
||||
@@ -279,6 +296,15 @@ private:
|
||||
rb.Push<u32>(1);
|
||||
}
|
||||
|
||||
// Should be similar to QueryAudioDeviceOutputEvent
|
||||
void QueryAudioDeviceInputEvent(Kernel::HLERequestContext& ctx) {
|
||||
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.PushCopyObjects(audio_input_device_switch_event.readable);
|
||||
}
|
||||
|
||||
void QueryAudioDeviceOutputEvent(Kernel::HLERequestContext& ctx) {
|
||||
LOG_DEBUG(Service_Audio, "called");
|
||||
|
||||
@@ -289,6 +315,7 @@ private:
|
||||
|
||||
u32_le revision = 0;
|
||||
Kernel::EventPair buffer_event;
|
||||
Kernel::EventPair audio_input_device_switch_event;
|
||||
Kernel::EventPair audio_output_device_switch_event;
|
||||
|
||||
}; // namespace Audio
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <fmt/time.h>
|
||||
#include <fmt/chrono.h>
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scm_rev.h"
|
||||
|
||||
@@ -636,10 +636,15 @@ Controller_NPad::LedPattern Controller_NPad::GetLedPattern(u32 npad_id) {
|
||||
return LedPattern{0, 0, 0, 0};
|
||||
};
|
||||
}
|
||||
|
||||
void Controller_NPad::SetVibrationEnabled(bool can_vibrate) {
|
||||
can_controllers_vibrate = can_vibrate;
|
||||
}
|
||||
|
||||
bool Controller_NPad::IsVibrationEnabled() const {
|
||||
return can_controllers_vibrate;
|
||||
}
|
||||
|
||||
void Controller_NPad::ClearAllConnectedControllers() {
|
||||
for (auto& controller : connected_controllers) {
|
||||
if (controller.is_connected && controller.type != NPadControllerType::None) {
|
||||
@@ -648,6 +653,7 @@ void Controller_NPad::ClearAllConnectedControllers() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Controller_NPad::DisconnectAllConnectedControllers() {
|
||||
std::for_each(connected_controllers.begin(), connected_controllers.end(),
|
||||
[](ControllerHolder& controller) { controller.is_connected = false; });
|
||||
|
||||
@@ -119,6 +119,7 @@ public:
|
||||
void DisconnectNPad(u32 npad_id);
|
||||
LedPattern GetLedPattern(u32 npad_id);
|
||||
void SetVibrationEnabled(bool can_vibrate);
|
||||
bool IsVibrationEnabled() const;
|
||||
void ClearAllConnectedControllers();
|
||||
void DisconnectAllConnectedControllers();
|
||||
void ConnectAllDisconnectedControllers();
|
||||
|
||||
@@ -216,8 +216,8 @@ Hid::Hid() : ServiceFramework("hid") {
|
||||
{201, &Hid::SendVibrationValue, "SendVibrationValue"},
|
||||
{202, &Hid::GetActualVibrationValue, "GetActualVibrationValue"},
|
||||
{203, &Hid::CreateActiveVibrationDeviceList, "CreateActiveVibrationDeviceList"},
|
||||
{204, nullptr, "PermitVibration"},
|
||||
{205, nullptr, "IsVibrationPermitted"},
|
||||
{204, &Hid::PermitVibration, "PermitVibration"},
|
||||
{205, &Hid::IsVibrationPermitted, "IsVibrationPermitted"},
|
||||
{206, &Hid::SendVibrationValues, "SendVibrationValues"},
|
||||
{207, nullptr, "SendVibrationGcErmCommand"},
|
||||
{208, nullptr, "GetActualVibrationGcErmCommand"},
|
||||
@@ -679,6 +679,27 @@ void Hid::CreateActiveVibrationDeviceList(Kernel::HLERequestContext& ctx) {
|
||||
rb.PushIpcInterface<IActiveVibrationDeviceList>();
|
||||
}
|
||||
|
||||
void Hid::PermitVibration(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp{ctx};
|
||||
const auto can_vibrate{rp.Pop<bool>()};
|
||||
applet_resource->GetController<Controller_NPad>(HidController::NPad)
|
||||
.SetVibrationEnabled(can_vibrate);
|
||||
|
||||
LOG_DEBUG(Service_HID, "called, can_vibrate={}", can_vibrate);
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 2};
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
void Hid::IsVibrationPermitted(Kernel::HLERequestContext& ctx) {
|
||||
LOG_DEBUG(Service_HID, "called");
|
||||
|
||||
IPC::ResponseBuilder rb{ctx, 3};
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.Push(
|
||||
applet_resource->GetController<Controller_NPad>(HidController::NPad).IsVibrationEnabled());
|
||||
}
|
||||
|
||||
void Hid::ActivateConsoleSixAxisSensor(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp{ctx};
|
||||
const auto applet_resource_user_id{rp.Pop<u64>()};
|
||||
|
||||
@@ -114,6 +114,8 @@ private:
|
||||
void SetNpadHandheldActivationMode(Kernel::HLERequestContext& ctx);
|
||||
void GetVibrationDeviceInfo(Kernel::HLERequestContext& ctx);
|
||||
void CreateActiveVibrationDeviceList(Kernel::HLERequestContext& ctx);
|
||||
void PermitVibration(Kernel::HLERequestContext& ctx);
|
||||
void IsVibrationPermitted(Kernel::HLERequestContext& ctx);
|
||||
void ActivateConsoleSixAxisSensor(Kernel::HLERequestContext& ctx);
|
||||
void StartConsoleSixAxisSensor(Kernel::HLERequestContext& ctx);
|
||||
void StopSixAxisSensor(Kernel::HLERequestContext& ctx);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/time.h>
|
||||
#include <json.hpp>
|
||||
|
||||
#include "common/file_util.h"
|
||||
|
||||
@@ -14,12 +14,22 @@
|
||||
namespace OpenGL {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
T GetInteger(GLenum pname) {
|
||||
GLint temporary;
|
||||
glGetIntegerv(pname, &temporary);
|
||||
return static_cast<T>(temporary);
|
||||
}
|
||||
|
||||
bool TestProgram(const GLchar* glsl) {
|
||||
const GLuint shader{glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &glsl)};
|
||||
GLint link_status;
|
||||
glGetProgramiv(shader, GL_LINK_STATUS, &link_status);
|
||||
glDeleteProgram(shader);
|
||||
return link_status == GL_TRUE;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
Device::Device() {
|
||||
@@ -32,6 +42,11 @@ Device::Device() {
|
||||
has_vertex_viewport_layer = GLAD_GL_ARB_shader_viewport_layer_array;
|
||||
has_variable_aoffi = TestVariableAoffi();
|
||||
has_component_indexing_bug = TestComponentIndexingBug();
|
||||
has_precise_bug = TestPreciseBug();
|
||||
|
||||
LOG_INFO(Render_OpenGL, "Renderer_VariableAOFFI: {}", has_variable_aoffi);
|
||||
LOG_INFO(Render_OpenGL, "Renderer_ComponentIndexingBug: {}", has_component_indexing_bug);
|
||||
LOG_INFO(Render_OpenGL, "Renderer_PreciseBug: {}", has_precise_bug);
|
||||
}
|
||||
|
||||
Device::Device(std::nullptr_t) {
|
||||
@@ -42,30 +57,21 @@ Device::Device(std::nullptr_t) {
|
||||
has_vertex_viewport_layer = true;
|
||||
has_variable_aoffi = true;
|
||||
has_component_indexing_bug = false;
|
||||
has_precise_bug = false;
|
||||
}
|
||||
|
||||
bool Device::TestVariableAoffi() {
|
||||
const GLchar* AOFFI_TEST = R"(#version 430 core
|
||||
return TestProgram(R"(#version 430 core
|
||||
// This is a unit test, please ignore me on apitrace bug reports.
|
||||
uniform sampler2D tex;
|
||||
uniform ivec2 variable_offset;
|
||||
out vec4 output_attribute;
|
||||
void main() {
|
||||
output_attribute = textureOffset(tex, vec2(0), variable_offset);
|
||||
}
|
||||
)";
|
||||
const GLuint shader{glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &AOFFI_TEST)};
|
||||
GLint link_status{};
|
||||
glGetProgramiv(shader, GL_LINK_STATUS, &link_status);
|
||||
glDeleteProgram(shader);
|
||||
|
||||
const bool supported{link_status == GL_TRUE};
|
||||
LOG_INFO(Render_OpenGL, "Renderer_VariableAOFFI: {}", supported);
|
||||
return supported;
|
||||
})");
|
||||
}
|
||||
|
||||
bool Device::TestComponentIndexingBug() {
|
||||
constexpr char log_message[] = "Renderer_ComponentIndexingBug: {}";
|
||||
const GLchar* COMPONENT_TEST = R"(#version 430 core
|
||||
layout (std430, binding = 0) buffer OutputBuffer {
|
||||
uint output_value;
|
||||
@@ -105,12 +111,21 @@ void main() {
|
||||
GLuint result;
|
||||
glGetNamedBufferSubData(ssbo.handle, 0, sizeof(result), &result);
|
||||
if (result != values.at(index)) {
|
||||
LOG_INFO(Render_OpenGL, log_message, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
LOG_INFO(Render_OpenGL, log_message, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Device::TestPreciseBug() {
|
||||
return !TestProgram(R"(#version 430 core
|
||||
in vec3 coords;
|
||||
out float out_value;
|
||||
uniform sampler2DShadow tex;
|
||||
void main() {
|
||||
precise float tmp_value = vec4(texture(tex, coords)).x;
|
||||
out_value = tmp_value;
|
||||
})");
|
||||
}
|
||||
|
||||
} // namespace OpenGL
|
||||
|
||||
@@ -46,9 +46,14 @@ public:
|
||||
return has_component_indexing_bug;
|
||||
}
|
||||
|
||||
bool HasPreciseBug() const {
|
||||
return has_precise_bug;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool TestVariableAoffi();
|
||||
static bool TestComponentIndexingBug();
|
||||
static bool TestPreciseBug();
|
||||
|
||||
std::size_t uniform_buffer_alignment{};
|
||||
std::size_t shader_storage_alignment{};
|
||||
@@ -58,6 +63,7 @@ private:
|
||||
bool has_vertex_viewport_layer{};
|
||||
bool has_variable_aoffi{};
|
||||
bool has_component_indexing_bug{};
|
||||
bool has_precise_bug{};
|
||||
};
|
||||
|
||||
} // namespace OpenGL
|
||||
|
||||
@@ -517,10 +517,37 @@ void Config::ReadPathValues() {
|
||||
UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString();
|
||||
UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString();
|
||||
UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString();
|
||||
UISettings::values.game_directory_path =
|
||||
UISettings::values.game_dir_deprecated =
|
||||
ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString();
|
||||
UISettings::values.game_directory_deepscan =
|
||||
UISettings::values.game_dir_deprecated_deepscan =
|
||||
ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool();
|
||||
const int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs"));
|
||||
for (int i = 0; i < gamedirs_size; ++i) {
|
||||
qt_config->setArrayIndex(i);
|
||||
UISettings::GameDir game_dir;
|
||||
game_dir.path = ReadSetting(QStringLiteral("path")).toString();
|
||||
game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool();
|
||||
game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool();
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
}
|
||||
qt_config->endArray();
|
||||
// create NAND and SD card directories if empty, these are not removable through the UI,
|
||||
// also carries over old game list settings if present
|
||||
if (UISettings::values.game_dirs.isEmpty()) {
|
||||
UISettings::GameDir game_dir;
|
||||
game_dir.path = QStringLiteral("SDMC");
|
||||
game_dir.expanded = true;
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
game_dir.path = QStringLiteral("UserNAND");
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
game_dir.path = QStringLiteral("SysNAND");
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) {
|
||||
game_dir.path = UISettings::values.game_dir_deprecated;
|
||||
game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan;
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
}
|
||||
}
|
||||
UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList();
|
||||
|
||||
qt_config->endGroup();
|
||||
@@ -899,10 +926,15 @@ void Config::SavePathValues() {
|
||||
WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path);
|
||||
WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path);
|
||||
WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path);
|
||||
WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path,
|
||||
QStringLiteral("."));
|
||||
WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan,
|
||||
false);
|
||||
qt_config->beginWriteArray(QStringLiteral("gamedirs"));
|
||||
for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
|
||||
qt_config->setArrayIndex(i);
|
||||
const auto& game_dir = UISettings::values.game_dirs[i];
|
||||
WriteSetting(QStringLiteral("path"), game_dir.path);
|
||||
WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false);
|
||||
WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true);
|
||||
}
|
||||
qt_config->endArray();
|
||||
WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files);
|
||||
|
||||
qt_config->endGroup();
|
||||
|
||||
@@ -20,25 +20,29 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
|
||||
|
||||
SetConfiguration();
|
||||
|
||||
connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this,
|
||||
[] { UISettings::values.is_game_list_reload_pending.exchange(true); });
|
||||
connect(ui->toggle_frame_limit, &QCheckBox::toggled, ui->frame_limit, &QSpinBox::setEnabled);
|
||||
}
|
||||
|
||||
ConfigureGeneral::~ConfigureGeneral() = default;
|
||||
|
||||
void ConfigureGeneral::SetConfiguration() {
|
||||
ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan);
|
||||
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
|
||||
ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot);
|
||||
ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme));
|
||||
|
||||
ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit);
|
||||
ui->frame_limit->setEnabled(ui->toggle_frame_limit->isChecked());
|
||||
ui->frame_limit->setValue(Settings::values.frame_limit);
|
||||
}
|
||||
|
||||
void ConfigureGeneral::ApplyConfiguration() {
|
||||
UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked();
|
||||
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
|
||||
UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked();
|
||||
UISettings::values.theme =
|
||||
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
|
||||
|
||||
Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked();
|
||||
Settings::values.frame_limit = ui->frame_limit->value();
|
||||
}
|
||||
|
||||
void ConfigureGeneral::changeEvent(QEvent* event) {
|
||||
|
||||
@@ -25,11 +25,31 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="GeneralVerticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_deepscan">
|
||||
<property name="text">
|
||||
<string>Search sub-directories for games</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_frame_limit">
|
||||
<property name="text">
|
||||
<string>Limit Speed Percent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="frame_limit">
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_check_exit">
|
||||
|
||||
@@ -55,7 +55,6 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent)
|
||||
|
||||
SetConfiguration();
|
||||
|
||||
connect(ui->toggle_frame_limit, &QCheckBox::toggled, ui->frame_limit, &QSpinBox::setEnabled);
|
||||
connect(ui->bg_button, &QPushButton::clicked, this, [this] {
|
||||
const QColor new_bg_color = QColorDialog::getColor(bg_color);
|
||||
if (!new_bg_color.isValid()) {
|
||||
@@ -72,9 +71,6 @@ void ConfigureGraphics::SetConfiguration() {
|
||||
|
||||
ui->resolution_factor_combobox->setCurrentIndex(
|
||||
static_cast<int>(FromResolutionFactor(Settings::values.resolution_factor)));
|
||||
ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit);
|
||||
ui->frame_limit->setEnabled(ui->toggle_frame_limit->isChecked());
|
||||
ui->frame_limit->setValue(Settings::values.frame_limit);
|
||||
ui->use_disk_shader_cache->setEnabled(runtime_lock);
|
||||
ui->use_disk_shader_cache->setChecked(Settings::values.use_disk_shader_cache);
|
||||
ui->use_accurate_gpu_emulation->setChecked(Settings::values.use_accurate_gpu_emulation);
|
||||
@@ -89,8 +85,6 @@ void ConfigureGraphics::SetConfiguration() {
|
||||
void ConfigureGraphics::ApplyConfiguration() {
|
||||
Settings::values.resolution_factor =
|
||||
ToResolutionFactor(static_cast<Resolution>(ui->resolution_factor_combobox->currentIndex()));
|
||||
Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked();
|
||||
Settings::values.frame_limit = ui->frame_limit->value();
|
||||
Settings::values.use_disk_shader_cache = ui->use_disk_shader_cache->isChecked();
|
||||
Settings::values.use_accurate_gpu_emulation = ui->use_accurate_gpu_emulation->isChecked();
|
||||
Settings::values.use_asynchronous_gpu_emulation =
|
||||
|
||||
@@ -22,33 +22,6 @@
|
||||
<string>Graphics</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="toggle_frame_limit">
|
||||
<property name="text">
|
||||
<string>Limit Speed Percent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="frame_limit">
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="use_disk_shader_cache">
|
||||
<property name="text">
|
||||
|
||||
@@ -301,13 +301,16 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
|
||||
});
|
||||
}
|
||||
connect(analog_map_stick[analog_id], &QPushButton::clicked, [=] {
|
||||
QMessageBox::information(this, tr("Information"),
|
||||
tr("After pressing OK, first move your joystick horizontally, "
|
||||
"and then vertically."));
|
||||
HandleClick(
|
||||
analog_map_stick[analog_id],
|
||||
[=](const Common::ParamPackage& params) { analogs_param[analog_id] = params; },
|
||||
InputCommon::Polling::DeviceType::Analog);
|
||||
if (QMessageBox::information(
|
||||
this, tr("Information"),
|
||||
tr("After pressing OK, first move your joystick horizontally, "
|
||||
"and then vertically."),
|
||||
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) {
|
||||
HandleClick(
|
||||
analog_map_stick[analog_id],
|
||||
[=](const Common::ParamPackage& params) { analogs_param[analog_id] = params; },
|
||||
InputCommon::Polling::DeviceType::Analog);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
|
||||
return QObject::eventFilter(obj, event);
|
||||
|
||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
||||
int rowCount = gamelist->tree_view->model()->rowCount();
|
||||
QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
|
||||
|
||||
// If the searchfield's text hasn't changed special function keys get checked
|
||||
@@ -56,19 +55,9 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
|
||||
// If there is only one result launch this game
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter: {
|
||||
QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view);
|
||||
QModelIndex root_index = item_model->invisibleRootItem()->index();
|
||||
QStandardItem* child_file;
|
||||
QString file_path;
|
||||
int resultCount = 0;
|
||||
for (int i = 0; i < rowCount; ++i) {
|
||||
if (!gamelist->tree_view->isRowHidden(i, root_index)) {
|
||||
++resultCount;
|
||||
child_file = gamelist->item_model->item(i, 0);
|
||||
file_path = child_file->data(GameListItemPath::FullPathRole).toString();
|
||||
}
|
||||
}
|
||||
if (resultCount == 1) {
|
||||
if (gamelist->search_field->visible == 1) {
|
||||
QString file_path = gamelist->getLastFilterResultItem();
|
||||
|
||||
// To avoid loading error dialog loops while confirming them using enter
|
||||
// Also users usually want to run a different game after closing one
|
||||
gamelist->search_field->edit_filter->clear();
|
||||
@@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve
|
||||
}
|
||||
|
||||
void GameListSearchField::setFilterResult(int visible, int total) {
|
||||
this->visible = visible;
|
||||
this->total = total;
|
||||
|
||||
label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
|
||||
}
|
||||
|
||||
QString GameList::getLastFilterResultItem() const {
|
||||
QStandardItem* folder;
|
||||
QStandardItem* child;
|
||||
QString file_path;
|
||||
const int folder_count = item_model->rowCount();
|
||||
for (int i = 0; i < folder_count; ++i) {
|
||||
folder = item_model->item(i, 0);
|
||||
const QModelIndex folder_index = folder->index();
|
||||
const int children_count = folder->rowCount();
|
||||
for (int j = 0; j < children_count; ++j) {
|
||||
if (!tree_view->isRowHidden(j, folder_index)) {
|
||||
child = folder->child(j, 0);
|
||||
file_path = child->data(GameListItemPath::FullPathRole).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return file_path;
|
||||
}
|
||||
|
||||
void GameListSearchField::clear() {
|
||||
edit_filter->clear();
|
||||
}
|
||||
@@ -147,45 +158,120 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput)
|
||||
[&haystack](const QString& s) { return haystack.contains(s); });
|
||||
}
|
||||
|
||||
// Syncs the expanded state of Game Directories with settings to persist across sessions
|
||||
void GameList::onItemExpanded(const QModelIndex& item) {
|
||||
const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>();
|
||||
if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir ||
|
||||
type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir)
|
||||
item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded =
|
||||
tree_view->isExpanded(item);
|
||||
}
|
||||
|
||||
// Event in order to filter the gamelist after editing the searchfield
|
||||
void GameList::onTextChanged(const QString& new_text) {
|
||||
const int row_count = tree_view->model()->rowCount();
|
||||
const QString edit_filter_text = new_text.toLower();
|
||||
const QModelIndex root_index = item_model->invisibleRootItem()->index();
|
||||
const int folder_count = tree_view->model()->rowCount();
|
||||
QString edit_filter_text = new_text.toLower();
|
||||
QStandardItem* folder;
|
||||
QStandardItem* child;
|
||||
int children_total = 0;
|
||||
QModelIndex root_index = item_model->invisibleRootItem()->index();
|
||||
|
||||
// If the searchfield is empty every item is visible
|
||||
// Otherwise the filter gets applied
|
||||
if (edit_filter_text.isEmpty()) {
|
||||
for (int i = 0; i < row_count; ++i) {
|
||||
tree_view->setRowHidden(i, root_index, false);
|
||||
for (int i = 0; i < folder_count; ++i) {
|
||||
folder = item_model->item(i, 0);
|
||||
const QModelIndex folder_index = folder->index();
|
||||
const int children_count = folder->rowCount();
|
||||
for (int j = 0; j < children_count; ++j) {
|
||||
++children_total;
|
||||
tree_view->setRowHidden(j, folder_index, false);
|
||||
}
|
||||
}
|
||||
search_field->setFilterResult(row_count, row_count);
|
||||
search_field->setFilterResult(children_total, children_total);
|
||||
} else {
|
||||
int result_count = 0;
|
||||
for (int i = 0; i < row_count; ++i) {
|
||||
const QStandardItem* child_file = item_model->item(i, 0);
|
||||
const QString file_path =
|
||||
child_file->data(GameListItemPath::FullPathRole).toString().toLower();
|
||||
const QString file_title =
|
||||
child_file->data(GameListItemPath::TitleRole).toString().toLower();
|
||||
const QString file_program_id =
|
||||
child_file->data(GameListItemPath::ProgramIdRole).toString().toLower();
|
||||
for (int i = 0; i < folder_count; ++i) {
|
||||
folder = item_model->item(i, 0);
|
||||
const QModelIndex folder_index = folder->index();
|
||||
const int children_count = folder->rowCount();
|
||||
for (int j = 0; j < children_count; ++j) {
|
||||
++children_total;
|
||||
const QStandardItem* child = folder->child(j, 0);
|
||||
const QString file_path =
|
||||
child->data(GameListItemPath::FullPathRole).toString().toLower();
|
||||
const QString file_title =
|
||||
child->data(GameListItemPath::TitleRole).toString().toLower();
|
||||
const QString file_program_id =
|
||||
child->data(GameListItemPath::ProgramIdRole).toString().toLower();
|
||||
|
||||
// Only items which filename in combination with its title contains all words
|
||||
// that are in the searchfield will be visible in the gamelist
|
||||
// The search is case insensitive because of toLower()
|
||||
// I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
|
||||
// multiple conversions of edit_filter_text for each game in the gamelist
|
||||
const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) +
|
||||
QLatin1Char{' '} + file_title;
|
||||
if (ContainsAllWords(file_name, edit_filter_text) ||
|
||||
(file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
|
||||
tree_view->setRowHidden(i, root_index, false);
|
||||
++result_count;
|
||||
} else {
|
||||
tree_view->setRowHidden(i, root_index, true);
|
||||
// Only items which filename in combination with its title contains all words
|
||||
// that are in the searchfield will be visible in the gamelist
|
||||
// The search is case insensitive because of toLower()
|
||||
// I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
|
||||
// multiple conversions of edit_filter_text for each game in the gamelist
|
||||
const QString file_name =
|
||||
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} +
|
||||
file_title;
|
||||
if (ContainsAllWords(file_name, edit_filter_text) ||
|
||||
(file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) {
|
||||
tree_view->setRowHidden(j, folder_index, false);
|
||||
++result_count;
|
||||
} else {
|
||||
tree_view->setRowHidden(j, folder_index, true);
|
||||
}
|
||||
search_field->setFilterResult(result_count, children_total);
|
||||
}
|
||||
search_field->setFilterResult(result_count, row_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameList::onUpdateThemedIcons() {
|
||||
for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
|
||||
QStandardItem* child = item_model->invisibleRootItem()->child(i);
|
||||
|
||||
const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
|
||||
switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||
case GameListItemType::SdmcDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("sd_card"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::UserNandDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::SysNandDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
case GameListItemType::CustomDir: {
|
||||
const UISettings::GameDir* game_dir =
|
||||
child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
|
||||
const QString icon_name = QFileInfo::exists(game_dir->path)
|
||||
? QStringLiteral("folder")
|
||||
: QStringLiteral("bad_folder");
|
||||
child->setData(
|
||||
QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
|
||||
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
}
|
||||
case GameListItemType::AddDir:
|
||||
child->setData(
|
||||
QIcon::fromTheme(QStringLiteral("plus"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
|
||||
tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
|
||||
tree_view->setSortingEnabled(true);
|
||||
tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
|
||||
tree_view->setUniformRowHeights(true);
|
||||
tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }"));
|
||||
|
||||
@@ -230,12 +315,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide
|
||||
item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type"));
|
||||
item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size"));
|
||||
}
|
||||
item_model->setSortRole(GameListItemPath::TitleRole);
|
||||
|
||||
connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);
|
||||
connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
|
||||
connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
|
||||
connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
|
||||
connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
|
||||
|
||||
// We must register all custom types with the Qt Automoc system so that we are able to use it
|
||||
// with signals/slots. In this case, QList falls under the umbrells of custom types.
|
||||
// We must register all custom types with the Qt Automoc system so that we are able to use
|
||||
// it with signals/slots. In this case, QList falls under the umbrells of custom types.
|
||||
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
|
||||
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -263,38 +352,68 @@ void GameList::clearFilter() {
|
||||
search_field->clear();
|
||||
}
|
||||
|
||||
void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
|
||||
void GameList::AddDirEntry(GameListDir* entry_items) {
|
||||
item_model->invisibleRootItem()->appendRow(entry_items);
|
||||
tree_view->setExpanded(
|
||||
entry_items->index(),
|
||||
entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
|
||||
}
|
||||
|
||||
void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
|
||||
parent->appendRow(entry_items);
|
||||
}
|
||||
|
||||
void GameList::ValidateEntry(const QModelIndex& item) {
|
||||
// We don't care about the individual QStandardItem that was selected, but its row.
|
||||
const int row = item_model->itemFromIndex(item)->row();
|
||||
const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
|
||||
const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString();
|
||||
const auto selected = item.sibling(item.row(), 0);
|
||||
|
||||
if (file_path.isEmpty())
|
||||
return;
|
||||
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||
case GameListItemType::Game: {
|
||||
const QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
|
||||
if (file_path.isEmpty())
|
||||
return;
|
||||
const QFileInfo file_info(file_path);
|
||||
if (!file_info.exists())
|
||||
return;
|
||||
|
||||
if (!QFileInfo::exists(file_path))
|
||||
return;
|
||||
|
||||
const QFileInfo file_info{file_path};
|
||||
if (file_info.isDir()) {
|
||||
const QDir dir{file_path};
|
||||
const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
|
||||
if (matching_main.size() == 1) {
|
||||
emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
|
||||
if (file_info.isDir()) {
|
||||
const QDir dir{file_path};
|
||||
const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files);
|
||||
if (matching_main.size() == 1) {
|
||||
emit GameChosen(dir.path() + QDir::separator() + matching_main[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Users usually want to run a diffrent game after closing one
|
||||
search_field->clear();
|
||||
emit GameChosen(file_path);
|
||||
// Users usually want to run a different game after closing one
|
||||
search_field->clear();
|
||||
emit GameChosen(file_path);
|
||||
break;
|
||||
}
|
||||
case GameListItemType::AddDir:
|
||||
emit AddDirectory();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool GameList::isEmpty() const {
|
||||
for (int i = 0; i < item_model->rowCount(); i++) {
|
||||
const QStandardItem* child = item_model->invisibleRootItem()->child(i);
|
||||
const auto type = static_cast<GameListItemType>(child->type());
|
||||
if (!child->hasChildren() &&
|
||||
(type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir ||
|
||||
type == GameListItemType::SysNandDir)) {
|
||||
item_model->invisibleRootItem()->removeRow(child->row());
|
||||
i--;
|
||||
};
|
||||
}
|
||||
return !item_model->invisibleRootItem()->hasChildren();
|
||||
}
|
||||
|
||||
void GameList::DonePopulating(QStringList watch_list) {
|
||||
emit ShowList(!isEmpty());
|
||||
|
||||
item_model->invisibleRootItem()->appendRow(new GameListAddDir());
|
||||
|
||||
// Clear out the old directories to watch for changes and add the new ones
|
||||
auto watch_dirs = watcher->directories();
|
||||
if (!watch_dirs.isEmpty()) {
|
||||
@@ -311,9 +430,13 @@ void GameList::DonePopulating(QStringList watch_list) {
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
tree_view->setEnabled(true);
|
||||
int rowCount = tree_view->model()->rowCount();
|
||||
search_field->setFilterResult(rowCount, rowCount);
|
||||
if (rowCount > 0) {
|
||||
const int folder_count = tree_view->model()->rowCount();
|
||||
int children_total = 0;
|
||||
for (int i = 0; i < folder_count; ++i) {
|
||||
children_total += item_model->item(i, 0)->rowCount();
|
||||
}
|
||||
search_field->setFilterResult(children_total, children_total);
|
||||
if (children_total > 0) {
|
||||
search_field->setFocus();
|
||||
}
|
||||
}
|
||||
@@ -323,12 +446,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||
if (!item.isValid())
|
||||
return;
|
||||
|
||||
int row = item_model->itemFromIndex(item)->row();
|
||||
QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
|
||||
u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong();
|
||||
std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString();
|
||||
|
||||
const auto selected = item.sibling(item.row(), 0);
|
||||
QMenu context_menu;
|
||||
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||
case GameListItemType::Game:
|
||||
AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
|
||||
selected.data(GameListItemPath::FullPathRole).toString().toStdString());
|
||||
break;
|
||||
case GameListItemType::CustomDir:
|
||||
AddPermDirPopup(context_menu, selected);
|
||||
AddCustomDirPopup(context_menu, selected);
|
||||
break;
|
||||
case GameListItemType::SdmcDir:
|
||||
case GameListItemType::UserNandDir:
|
||||
case GameListItemType::SysNandDir:
|
||||
AddPermDirPopup(context_menu, selected);
|
||||
break;
|
||||
}
|
||||
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
|
||||
}
|
||||
|
||||
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) {
|
||||
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
|
||||
QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location"));
|
||||
QAction* open_transferable_shader_cache =
|
||||
@@ -344,19 +482,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
|
||||
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
|
||||
|
||||
connect(open_save_location, &QAction::triggered,
|
||||
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); });
|
||||
connect(open_lfs_location, &QAction::triggered,
|
||||
[&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); });
|
||||
connect(open_save_location, &QAction::triggered, [this, program_id]() {
|
||||
emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData);
|
||||
});
|
||||
connect(open_lfs_location, &QAction::triggered, [this, program_id]() {
|
||||
emit OpenFolderRequested(program_id, GameListOpenTarget::ModData);
|
||||
});
|
||||
connect(open_transferable_shader_cache, &QAction::triggered,
|
||||
[&]() { emit OpenTransferableShaderCacheRequested(program_id); });
|
||||
connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); });
|
||||
connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); });
|
||||
connect(navigate_to_gamedb_entry, &QAction::triggered,
|
||||
[&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); });
|
||||
connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); });
|
||||
[this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); });
|
||||
connect(dump_romfs, &QAction::triggered,
|
||||
[this, program_id, path]() { emit DumpRomFSRequested(program_id, path); });
|
||||
connect(copy_tid, &QAction::triggered,
|
||||
[this, program_id]() { emit CopyTIDRequested(program_id); });
|
||||
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
|
||||
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
|
||||
});
|
||||
connect(properties, &QAction::triggered,
|
||||
[this, path]() { emit OpenPerGameGeneralRequested(path); });
|
||||
};
|
||||
|
||||
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
|
||||
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
|
||||
UISettings::GameDir& game_dir =
|
||||
*selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
|
||||
|
||||
QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
|
||||
QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
|
||||
|
||||
deep_scan->setCheckable(true);
|
||||
deep_scan->setChecked(game_dir.deep_scan);
|
||||
|
||||
connect(deep_scan, &QAction::triggered, [this, &game_dir] {
|
||||
game_dir.deep_scan = !game_dir.deep_scan;
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
});
|
||||
connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] {
|
||||
UISettings::values.game_dirs.removeOne(game_dir);
|
||||
item_model->invisibleRootItem()->removeRow(selected.row());
|
||||
});
|
||||
}
|
||||
|
||||
void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
|
||||
UISettings::GameDir& game_dir =
|
||||
*selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
|
||||
|
||||
QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up"));
|
||||
QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down "));
|
||||
QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location"));
|
||||
|
||||
const int row = selected.row();
|
||||
|
||||
move_up->setEnabled(row > 0);
|
||||
move_down->setEnabled(row < item_model->rowCount() - 2);
|
||||
|
||||
connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] {
|
||||
// find the indices of the items in settings and swap them
|
||||
std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
|
||||
UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
|
||||
*selected.sibling(row - 1, 0)
|
||||
.data(GameListDir::GameDirRole)
|
||||
.value<UISettings::GameDir*>())]);
|
||||
// move the treeview items
|
||||
QList<QStandardItem*> item = item_model->takeRow(row);
|
||||
item_model->invisibleRootItem()->insertRow(row - 1, item);
|
||||
tree_view->setExpanded(selected, game_dir.expanded);
|
||||
});
|
||||
|
||||
connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] {
|
||||
// find the indices of the items in settings and swap them
|
||||
std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)],
|
||||
UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(
|
||||
*selected.sibling(row + 1, 0)
|
||||
.data(GameListDir::GameDirRole)
|
||||
.value<UISettings::GameDir*>())]);
|
||||
// move the treeview items
|
||||
const QList<QStandardItem*> item = item_model->takeRow(row);
|
||||
item_model->invisibleRootItem()->insertRow(row + 1, item);
|
||||
tree_view->setExpanded(selected, game_dir.expanded);
|
||||
});
|
||||
|
||||
connect(open_directory_location, &QAction::triggered,
|
||||
[this, game_dir] { emit OpenDirectory(game_dir.path); });
|
||||
}
|
||||
|
||||
void GameList::LoadCompatibilityList() {
|
||||
@@ -403,14 +608,7 @@ void GameList::LoadCompatibilityList() {
|
||||
}
|
||||
}
|
||||
|
||||
void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
|
||||
const QFileInfo dir_info{dir_path};
|
||||
if (!dir_info.exists() || !dir_info.isDir()) {
|
||||
LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString());
|
||||
search_field->setFilterResult(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
tree_view->setEnabled(false);
|
||||
|
||||
// Update the columns in case UISettings has changed
|
||||
@@ -433,17 +631,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
|
||||
|
||||
// Delete any rows that might already exist if we're repopulating
|
||||
item_model->removeRows(0, item_model->rowCount());
|
||||
search_field->clear();
|
||||
|
||||
emit ShouldCancelWorker();
|
||||
|
||||
GameListWorker* worker =
|
||||
new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list);
|
||||
GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list);
|
||||
|
||||
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
|
||||
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
|
||||
Qt::QueuedConnection);
|
||||
connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
|
||||
Qt::QueuedConnection);
|
||||
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel
|
||||
// without delay.
|
||||
// Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
|
||||
// cancel without delay.
|
||||
connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
|
||||
Qt::DirectConnection);
|
||||
|
||||
@@ -471,10 +671,40 @@ const QStringList GameList::supported_file_extensions = {
|
||||
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
|
||||
void GameList::RefreshGameDirectory() {
|
||||
if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) {
|
||||
if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||
search_field->clear();
|
||||
PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
|
||||
connect(parent, &GMainWindow::UpdateThemedIcons, this,
|
||||
&GameListPlaceholder::onUpdateThemedIcons);
|
||||
|
||||
layout = new QVBoxLayout;
|
||||
image = new QLabel;
|
||||
text = new QLabel;
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
|
||||
|
||||
text->setText(tr("Double-click to add a new folder to the game list"));
|
||||
QFont font = text->font();
|
||||
font.setPointSize(20);
|
||||
text->setFont(font);
|
||||
text->setAlignment(Qt::AlignHCenter);
|
||||
image->setAlignment(Qt::AlignHCenter);
|
||||
|
||||
layout->addWidget(image);
|
||||
layout->addWidget(text);
|
||||
setLayout(layout);
|
||||
}
|
||||
|
||||
GameListPlaceholder::~GameListPlaceholder() = default;
|
||||
|
||||
void GameListPlaceholder::onUpdateThemedIcons() {
|
||||
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
|
||||
}
|
||||
|
||||
void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
|
||||
emit GameListPlaceholder::AddDirectory();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QModelIndex>
|
||||
#include <QSettings>
|
||||
#include <QStandardItem>
|
||||
@@ -16,13 +17,16 @@
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "uisettings.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
|
||||
class GameListWorker;
|
||||
class GameListSearchField;
|
||||
class GameListDir;
|
||||
class GMainWindow;
|
||||
|
||||
namespace FileSys {
|
||||
@@ -52,12 +56,14 @@ public:
|
||||
FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr);
|
||||
~GameList() override;
|
||||
|
||||
QString getLastFilterResultItem() const;
|
||||
void clearFilter();
|
||||
void setFilterFocus();
|
||||
void setFilterVisible(bool visibility);
|
||||
bool isEmpty() const;
|
||||
|
||||
void LoadCompatibilityList();
|
||||
void PopulateAsync(const QString& dir_path, bool deep_scan);
|
||||
void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
|
||||
|
||||
void SaveInterfaceLayout();
|
||||
void LoadInterfaceLayout();
|
||||
@@ -74,19 +80,29 @@ signals:
|
||||
void NavigateToGamedbEntryRequested(u64 program_id,
|
||||
const CompatibilityList& compatibility_list);
|
||||
void OpenPerGameGeneralRequested(const std::string& file);
|
||||
void OpenDirectory(const QString& directory);
|
||||
void AddDirectory();
|
||||
void ShowList(bool show);
|
||||
|
||||
private slots:
|
||||
void onItemExpanded(const QModelIndex& item);
|
||||
void onTextChanged(const QString& new_text);
|
||||
void onFilterCloseClicked();
|
||||
void onUpdateThemedIcons();
|
||||
|
||||
private:
|
||||
void AddEntry(const QList<QStandardItem*>& entry_items);
|
||||
void AddDirEntry(GameListDir* entry_items);
|
||||
void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
|
||||
void ValidateEntry(const QModelIndex& item);
|
||||
void DonePopulating(QStringList watch_list);
|
||||
|
||||
void PopupContextMenu(const QPoint& menu_location);
|
||||
void RefreshGameDirectory();
|
||||
|
||||
void PopupContextMenu(const QPoint& menu_location);
|
||||
void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path);
|
||||
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
GameListSearchField* search_field;
|
||||
@@ -102,3 +118,24 @@ private:
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(GameListOpenTarget);
|
||||
|
||||
class GameListPlaceholder : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GameListPlaceholder(GMainWindow* parent = nullptr);
|
||||
~GameListPlaceholder();
|
||||
|
||||
signals:
|
||||
void AddDirectory();
|
||||
|
||||
private slots:
|
||||
void onUpdateThemedIcons();
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
QVBoxLayout* layout = nullptr;
|
||||
QLabel* image = nullptr;
|
||||
QLabel* text = nullptr;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
#include <QStandardItem>
|
||||
@@ -22,6 +23,17 @@
|
||||
#include "yuzu/uisettings.h"
|
||||
#include "yuzu/util/util.h"
|
||||
|
||||
enum class GameListItemType {
|
||||
Game = QStandardItem::UserType + 1,
|
||||
CustomDir = QStandardItem::UserType + 2,
|
||||
SdmcDir = QStandardItem::UserType + 3,
|
||||
UserNandDir = QStandardItem::UserType + 4,
|
||||
SysNandDir = QStandardItem::UserType + 5,
|
||||
AddDir = QStandardItem::UserType + 6
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(GameListItemType);
|
||||
|
||||
/**
|
||||
* Gets the default icon (for games without valid title metadata)
|
||||
* @param size The desired width and height of the default icon.
|
||||
@@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) {
|
||||
class GameListItem : public QStandardItem {
|
||||
|
||||
public:
|
||||
// used to access type from item index
|
||||
static const int TypeRole = Qt::UserRole + 1;
|
||||
static const int SortRole = Qt::UserRole + 2;
|
||||
GameListItem() = default;
|
||||
explicit GameListItem(const QString& string) : QStandardItem(string) {}
|
||||
GameListItem(const QString& string) : QStandardItem(string) {
|
||||
setData(string, SortRole);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -48,14 +65,15 @@ public:
|
||||
*/
|
||||
class GameListItemPath : public GameListItem {
|
||||
public:
|
||||
static const int FullPathRole = Qt::UserRole + 1;
|
||||
static const int TitleRole = Qt::UserRole + 2;
|
||||
static const int ProgramIdRole = Qt::UserRole + 3;
|
||||
static const int FileTypeRole = Qt::UserRole + 4;
|
||||
static const int TitleRole = SortRole;
|
||||
static const int FullPathRole = SortRole + 1;
|
||||
static const int ProgramIdRole = SortRole + 2;
|
||||
static const int FileTypeRole = SortRole + 3;
|
||||
|
||||
GameListItemPath() = default;
|
||||
GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
|
||||
const QString& game_name, const QString& game_type, u64 program_id) {
|
||||
setData(type(), TypeRole);
|
||||
setData(game_path, FullPathRole);
|
||||
setData(game_name, TitleRole);
|
||||
setData(qulonglong(program_id), ProgramIdRole);
|
||||
@@ -72,6 +90,10 @@ public:
|
||||
setData(picture, Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
QVariant data(int role) const override {
|
||||
if (role == Qt::DisplayRole) {
|
||||
std::string filename;
|
||||
@@ -103,9 +125,11 @@ public:
|
||||
class GameListItemCompat : public GameListItem {
|
||||
Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
|
||||
public:
|
||||
static const int CompatNumberRole = Qt::UserRole + 1;
|
||||
static const int CompatNumberRole = SortRole;
|
||||
GameListItemCompat() = default;
|
||||
explicit GameListItemCompat(const QString& compatibility) {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
struct CompatStatus {
|
||||
QString color;
|
||||
const char* text;
|
||||
@@ -135,6 +159,10 @@ public:
|
||||
setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
bool operator<(const QStandardItem& other) const override {
|
||||
return data(CompatNumberRole) < other.data(CompatNumberRole);
|
||||
}
|
||||
@@ -146,12 +174,12 @@ public:
|
||||
* human-readable string representation will be displayed to the user.
|
||||
*/
|
||||
class GameListItemSize : public GameListItem {
|
||||
|
||||
public:
|
||||
static const int SizeRole = Qt::UserRole + 1;
|
||||
static const int SizeRole = SortRole;
|
||||
|
||||
GameListItemSize() = default;
|
||||
explicit GameListItemSize(const qulonglong size_bytes) {
|
||||
setData(type(), TypeRole);
|
||||
setData(size_bytes, SizeRole);
|
||||
}
|
||||
|
||||
@@ -167,6 +195,10 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::Game);
|
||||
}
|
||||
|
||||
/**
|
||||
* This operator is, in practice, only used by the TreeView sorting systems.
|
||||
* Override it so that it will correctly sort by numerical value instead of by string
|
||||
@@ -177,6 +209,82 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class GameListDir : public GameListItem {
|
||||
public:
|
||||
static const int GameDirRole = Qt::UserRole + 2;
|
||||
|
||||
explicit GameListDir(UISettings::GameDir& directory,
|
||||
GameListItemType dir_type = GameListItemType::CustomDir)
|
||||
: dir_type{dir_type} {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
UISettings::GameDir* game_dir = &directory;
|
||||
setData(QVariant::fromValue(game_dir), GameDirRole);
|
||||
|
||||
const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
|
||||
switch (dir_type) {
|
||||
case GameListItemType::SdmcDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("sd_card"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::UserNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::SysNandDir:
|
||||
setData(
|
||||
QIcon::fromTheme(QStringLiteral("chip"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("System Titles"), Qt::DisplayRole);
|
||||
break;
|
||||
case GameListItemType::CustomDir:
|
||||
const QString icon_name = QFileInfo::exists(game_dir->path)
|
||||
? QStringLiteral("folder")
|
||||
: QStringLiteral("bad_folder");
|
||||
setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
|
||||
icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(game_dir->path, Qt::DisplayRole);
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(dir_type);
|
||||
}
|
||||
|
||||
private:
|
||||
GameListItemType dir_type;
|
||||
};
|
||||
|
||||
class GameListAddDir : public GameListItem {
|
||||
public:
|
||||
explicit GameListAddDir() {
|
||||
setData(type(), TypeRole);
|
||||
|
||||
const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
|
||||
setData(QIcon::fromTheme(QStringLiteral("plus"))
|
||||
.pixmap(icon_size)
|
||||
.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
|
||||
Qt::DecorationRole);
|
||||
setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
|
||||
}
|
||||
|
||||
int type() const override {
|
||||
return static_cast<int>(GameListItemType::AddDir);
|
||||
}
|
||||
};
|
||||
|
||||
class GameList;
|
||||
class QHBoxLayout;
|
||||
class QTreeView;
|
||||
@@ -208,6 +316,9 @@ private:
|
||||
// EventFilter in order to process systemkeys while editing the searchfield
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
};
|
||||
int visible;
|
||||
int total;
|
||||
|
||||
QHBoxLayout* layout_filter = nullptr;
|
||||
QTreeView* tree_view = nullptr;
|
||||
QLabel* label_filter = nullptr;
|
||||
|
||||
@@ -223,21 +223,37 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri
|
||||
} // Anonymous namespace
|
||||
|
||||
GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
|
||||
FileSys::ManualContentProvider* provider, QString dir_path,
|
||||
bool deep_scan, const CompatibilityList& compatibility_list)
|
||||
: vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan),
|
||||
FileSys::ManualContentProvider* provider,
|
||||
QVector<UISettings::GameDir>& game_dirs,
|
||||
const CompatibilityList& compatibility_list)
|
||||
: vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),
|
||||
compatibility_list(compatibility_list) {}
|
||||
|
||||
GameListWorker::~GameListWorker() = default;
|
||||
|
||||
void GameListWorker::AddTitlesToGameList() {
|
||||
const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>(
|
||||
Core::System::GetInstance().GetContentProvider());
|
||||
const auto installed_games = cache.ListEntriesFilterOrigin(
|
||||
std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program);
|
||||
void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
|
||||
using namespace FileSys;
|
||||
|
||||
const auto& cache =
|
||||
dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider());
|
||||
|
||||
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games;
|
||||
installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
|
||||
ContentRecordType::Program);
|
||||
|
||||
if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
|
||||
} else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
|
||||
installed_games = cache.ListEntriesFilterOrigin(
|
||||
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
|
||||
}
|
||||
|
||||
for (const auto& [slot, game] : installed_games) {
|
||||
if (slot == FileSys::ContentProviderUnionSlot::FrontendManual)
|
||||
if (slot == ContentProviderUnionSlot::FrontendManual)
|
||||
continue;
|
||||
|
||||
const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
|
||||
@@ -250,21 +266,22 @@ void GameListWorker::AddTitlesToGameList() {
|
||||
u64 program_id = 0;
|
||||
loader->ReadProgramId(program_id);
|
||||
|
||||
const FileSys::PatchManager patch{program_id};
|
||||
const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control);
|
||||
const PatchManager patch{program_id};
|
||||
const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
|
||||
if (control != nullptr)
|
||||
GetMetadataFromControlNCA(patch, *control, icon, name);
|
||||
|
||||
emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
|
||||
compatibility_list, patch));
|
||||
compatibility_list, patch),
|
||||
parent_dir);
|
||||
}
|
||||
}
|
||||
|
||||
void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
|
||||
unsigned int recursion) {
|
||||
const auto callback = [this, target, recursion](u64* num_entries_out,
|
||||
const std::string& directory,
|
||||
const std::string& virtual_name) -> bool {
|
||||
unsigned int recursion, GameListDir* parent_dir) {
|
||||
const auto callback = [this, target, recursion,
|
||||
parent_dir](u64* num_entries_out, const std::string& directory,
|
||||
const std::string& virtual_name) -> bool {
|
||||
if (stop_processing) {
|
||||
// Breaks the callback loop.
|
||||
return false;
|
||||
@@ -317,11 +334,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
const FileSys::PatchManager patch{program_id};
|
||||
|
||||
emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
|
||||
compatibility_list, patch));
|
||||
compatibility_list, patch),
|
||||
parent_dir);
|
||||
}
|
||||
} else if (is_dir && recursion > 0) {
|
||||
watch_list.append(QString::fromStdString(physical_name));
|
||||
ScanFileSystem(target, physical_name, recursion - 1);
|
||||
ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -332,12 +350,32 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||
|
||||
void GameListWorker::run() {
|
||||
stop_processing = false;
|
||||
watch_list.append(dir_path);
|
||||
provider->ClearAllEntries();
|
||||
ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(),
|
||||
deep_scan ? 256 : 0);
|
||||
AddTitlesToGameList();
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0);
|
||||
|
||||
for (UISettings::GameDir& game_dir : game_dirs) {
|
||||
if (game_dir.path == QStringLiteral("SDMC")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
|
||||
emit DirEntryReady({game_list_dir});
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == QStringLiteral("UserNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
|
||||
emit DirEntryReady({game_list_dir});
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else if (game_dir.path == QStringLiteral("SysNAND")) {
|
||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
|
||||
emit DirEntryReady({game_list_dir});
|
||||
AddTitlesToGameList(game_list_dir);
|
||||
} else {
|
||||
watch_list.append(game_dir.path);
|
||||
auto* const game_list_dir = new GameListDir(game_dir);
|
||||
emit DirEntryReady({game_list_dir});
|
||||
provider->ClearAllEntries();
|
||||
ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2,
|
||||
game_list_dir);
|
||||
ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
|
||||
game_dir.deep_scan ? 256 : 0, game_list_dir);
|
||||
}
|
||||
};
|
||||
|
||||
emit Finished(watch_list);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
@@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
|
||||
FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan,
|
||||
const CompatibilityList& compatibility_list);
|
||||
explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs,
|
||||
FileSys::ManualContentProvider* provider,
|
||||
QVector<UISettings::GameDir>& game_dirs,
|
||||
const CompatibilityList& compatibility_list);
|
||||
~GameListWorker() override;
|
||||
|
||||
/// Starts the processing of directory tree information.
|
||||
@@ -48,31 +50,33 @@ signals:
|
||||
/**
|
||||
* The `EntryReady` signal is emitted once an entry has been prepared and is ready
|
||||
* to be added to the game list.
|
||||
* @param entry_items a list with `QStandardItem`s that make up the columns of the new entry.
|
||||
* @param entry_items a list with `QStandardItem`s that make up the columns of the new
|
||||
* entry.
|
||||
*/
|
||||
void EntryReady(QList<QStandardItem*> entry_items);
|
||||
void DirEntryReady(GameListDir* entry_items);
|
||||
void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
|
||||
|
||||
/**
|
||||
* After the worker has traversed the game directory looking for entries, this signal is emitted
|
||||
* with a list of folders that should be watched for changes as well.
|
||||
* After the worker has traversed the game directory looking for entries, this signal is
|
||||
* emitted with a list of folders that should be watched for changes as well.
|
||||
*/
|
||||
void Finished(QStringList watch_list);
|
||||
|
||||
private:
|
||||
void AddTitlesToGameList();
|
||||
void AddTitlesToGameList(GameListDir* parent_dir);
|
||||
|
||||
enum class ScanTarget {
|
||||
FillManualContentProvider,
|
||||
PopulateGameList,
|
||||
};
|
||||
|
||||
void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0);
|
||||
void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion,
|
||||
GameListDir* parent_dir);
|
||||
|
||||
std::shared_ptr<FileSys::VfsFilesystem> vfs;
|
||||
FileSys::ManualContentProvider* provider;
|
||||
QStringList watch_list;
|
||||
QString dir_path;
|
||||
bool deep_scan;
|
||||
const CompatibilityList& compatibility_list;
|
||||
QVector<UISettings::GameDir>& game_dirs;
|
||||
std::atomic_bool stop_processing;
|
||||
};
|
||||
|
||||
@@ -216,8 +216,7 @@ GMainWindow::GMainWindow()
|
||||
OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
|
||||
|
||||
game_list->LoadCompatibilityList();
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
|
||||
// Show one-time "callout" messages to the user
|
||||
ShowTelemetryCallout();
|
||||
@@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() {
|
||||
game_list = new GameList(vfs, provider.get(), this);
|
||||
ui.horizontalLayout->addWidget(game_list);
|
||||
|
||||
game_list_placeholder = new GameListPlaceholder(this);
|
||||
ui.horizontalLayout->addWidget(game_list_placeholder);
|
||||
game_list_placeholder->setVisible(false);
|
||||
|
||||
loading_screen = new LoadingScreen(this);
|
||||
loading_screen->hide();
|
||||
ui.horizontalLayout->addWidget(loading_screen);
|
||||
@@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() {
|
||||
|
||||
void GMainWindow::ConnectWidgetEvents() {
|
||||
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
|
||||
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
|
||||
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
|
||||
connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this,
|
||||
&GMainWindow::OnTransferableShaderCacheOpenFile);
|
||||
@@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() {
|
||||
connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);
|
||||
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
|
||||
&GMainWindow::OnGameListNavigateToGamedbEntry);
|
||||
connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
|
||||
connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
|
||||
&GMainWindow::OnGameListAddDirectory);
|
||||
connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
|
||||
|
||||
connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
|
||||
&GMainWindow::OnGameListOpenPerGameProperties);
|
||||
|
||||
@@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() {
|
||||
connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder);
|
||||
connect(ui.action_Install_File_NAND, &QAction::triggered, this,
|
||||
&GMainWindow::OnMenuInstallToNAND);
|
||||
connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
|
||||
&GMainWindow::OnMenuSelectGameListRoot);
|
||||
connect(ui.action_Select_NAND_Directory, &QAction::triggered, this,
|
||||
[this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); });
|
||||
connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this,
|
||||
@@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) {
|
||||
// Update the GUI
|
||||
if (ui.action_Single_Window_Mode->isChecked()) {
|
||||
game_list->hide();
|
||||
game_list_placeholder->hide();
|
||||
}
|
||||
status_bar_update_timer.start(2000);
|
||||
|
||||
@@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() {
|
||||
render_window->hide();
|
||||
loading_screen->hide();
|
||||
loading_screen->Clear();
|
||||
game_list->show();
|
||||
if (game_list->isEmpty())
|
||||
game_list_placeholder->show();
|
||||
else
|
||||
game_list->show();
|
||||
game_list->setFilterFocus();
|
||||
|
||||
UpdateWindowTitle();
|
||||
@@ -1298,6 +1309,47 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
|
||||
QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
|
||||
}
|
||||
|
||||
void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
|
||||
QString path;
|
||||
if (directory == QStringLiteral("SDMC")) {
|
||||
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
|
||||
"Nintendo/Contents/registered");
|
||||
} else if (directory == QStringLiteral("UserNAND")) {
|
||||
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
||||
"user/Contents/registered");
|
||||
} else if (directory == QStringLiteral("SysNAND")) {
|
||||
path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
|
||||
"system/Contents/registered");
|
||||
} else {
|
||||
path = directory;
|
||||
}
|
||||
if (!QFileInfo::exists(path)) {
|
||||
QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
|
||||
return;
|
||||
}
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
||||
}
|
||||
|
||||
void GMainWindow::OnGameListAddDirectory() {
|
||||
const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
|
||||
if (dir_path.isEmpty())
|
||||
return;
|
||||
UISettings::GameDir game_dir{dir_path, false, true};
|
||||
if (!UISettings::values.game_dirs.contains(game_dir)) {
|
||||
UISettings::values.game_dirs.append(game_dir);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
} else {
|
||||
LOG_WARNING(Frontend, "Selected directory is already in the game list");
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::OnGameListShowList(bool show) {
|
||||
if (emulation_running && ui.action_Single_Window_Mode->isChecked())
|
||||
return;
|
||||
game_list->setVisible(show);
|
||||
game_list_placeholder->setVisible(!show);
|
||||
};
|
||||
|
||||
void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
|
||||
u64 title_id{};
|
||||
const auto v_file = Core::GetGameFileFromPath(vfs, file);
|
||||
@@ -1316,8 +1368,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) {
|
||||
|
||||
const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
|
||||
if (reload) {
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
|
||||
config->Save();
|
||||
@@ -1407,8 +1458,7 @@ void GMainWindow::OnMenuInstallToNAND() {
|
||||
const auto success = [this]() {
|
||||
QMessageBox::information(this, tr("Successfully Installed"),
|
||||
tr("The file was successfully installed."));
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) +
|
||||
DIR_SEP + "game_list");
|
||||
};
|
||||
@@ -1533,14 +1583,6 @@ void GMainWindow::OnMenuInstallToNAND() {
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::OnMenuSelectGameListRoot() {
|
||||
QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
|
||||
if (!dir_path.isEmpty()) {
|
||||
UISettings::values.game_directory_path = dir_path;
|
||||
game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan);
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) {
|
||||
const auto res = QMessageBox::information(
|
||||
this, tr("Changing Emulated Directory"),
|
||||
@@ -1559,8 +1601,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target)
|
||||
: FileUtil::UserPath::NANDDir,
|
||||
dir_path.toStdString());
|
||||
Service::FileSystem::CreateFactories(*vfs);
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1724,11 +1765,11 @@ void GMainWindow::OnConfigure() {
|
||||
if (UISettings::values.enable_discord_presence != old_discord_presence) {
|
||||
SetDiscordEnabled(UISettings::values.enable_discord_presence);
|
||||
}
|
||||
emit UpdateThemedIcons();
|
||||
|
||||
const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
|
||||
if (reload) {
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
|
||||
config->Save();
|
||||
@@ -1992,8 +2033,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
||||
Service::FileSystem::CreateFactories(*vfs);
|
||||
|
||||
if (behavior == ReinitializeKeyBehavior::Warning) {
|
||||
game_list->PopulateAsync(UISettings::values.game_directory_path,
|
||||
UISettings::values.game_directory_deepscan);
|
||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2158,7 +2198,6 @@ void GMainWindow::UpdateUITheme() {
|
||||
}
|
||||
|
||||
QIcon::setThemeSearchPaths(theme_paths);
|
||||
emit UpdateThemedIcons();
|
||||
}
|
||||
|
||||
void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
|
||||
|
||||
@@ -30,6 +30,7 @@ class ProfilerWidget;
|
||||
class QLabel;
|
||||
class WaitTreeWidget;
|
||||
enum class GameListOpenTarget;
|
||||
class GameListPlaceholder;
|
||||
|
||||
namespace Core::Frontend {
|
||||
struct SoftwareKeyboardParameters;
|
||||
@@ -186,12 +187,13 @@ private slots:
|
||||
void OnGameListCopyTID(u64 program_id);
|
||||
void OnGameListNavigateToGamedbEntry(u64 program_id,
|
||||
const CompatibilityList& compatibility_list);
|
||||
void OnGameListOpenDirectory(const QString& directory);
|
||||
void OnGameListAddDirectory();
|
||||
void OnGameListShowList(bool show);
|
||||
void OnGameListOpenPerGameProperties(const std::string& file);
|
||||
void OnMenuLoadFile();
|
||||
void OnMenuLoadFolder();
|
||||
void OnMenuInstallToNAND();
|
||||
/// Called whenever a user selects the "File->Select Game List Root" menu item
|
||||
void OnMenuSelectGameListRoot();
|
||||
/// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card
|
||||
void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target);
|
||||
void OnMenuRecentFile();
|
||||
@@ -223,6 +225,8 @@ private:
|
||||
GameList* game_list;
|
||||
LoadingScreen* loading_screen;
|
||||
|
||||
GameListPlaceholder* game_list_placeholder;
|
||||
|
||||
// Status bar elements
|
||||
QLabel* message_label = nullptr;
|
||||
QLabel* emu_speed_label = nullptr;
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
<addaction name="action_Load_File"/>
|
||||
<addaction name="action_Load_Folder"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Select_Game_List_Root"/>
|
||||
<addaction name="menu_recent_files"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_Select_NAND_Directory"/>
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <QByteArray>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace UISettings {
|
||||
@@ -25,6 +27,18 @@ struct Shortcut {
|
||||
using Themes = std::array<std::pair<const char*, const char*>, 2>;
|
||||
extern const Themes themes;
|
||||
|
||||
struct GameDir {
|
||||
QString path;
|
||||
bool deep_scan;
|
||||
bool expanded;
|
||||
bool operator==(const GameDir& rhs) const {
|
||||
return path == rhs.path;
|
||||
};
|
||||
bool operator!=(const GameDir& rhs) const {
|
||||
return !operator==(rhs);
|
||||
};
|
||||
};
|
||||
|
||||
struct Values {
|
||||
QByteArray geometry;
|
||||
QByteArray state;
|
||||
@@ -55,8 +69,9 @@ struct Values {
|
||||
QString roms_path;
|
||||
QString symbols_path;
|
||||
QString screenshot_path;
|
||||
QString game_directory_path;
|
||||
bool game_directory_deepscan;
|
||||
QString game_dir_deprecated;
|
||||
bool game_dir_deprecated_deepscan;
|
||||
QVector<UISettings::GameDir> game_dirs;
|
||||
QStringList recent_files;
|
||||
|
||||
QString theme;
|
||||
@@ -84,3 +99,5 @@ struct Values {
|
||||
|
||||
extern Values values;
|
||||
} // namespace UISettings
|
||||
|
||||
Q_DECLARE_METATYPE(UISettings::GameDir*);
|
||||
|
||||