diff options
26 files changed, 256 insertions, 31 deletions
diff --git a/dist/license.md b/dist/license.md index e9bc87656..7bdebfec1 100644 --- a/dist/license.md +++ b/dist/license.md @@ -12,6 +12,7 @@ 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/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com @@ -20,6 +21,7 @@ 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/qdarkstyle/icons/48x48/star.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/16x16/view-refresh.png | Apache 2.0 | https://material.io qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com @@ -28,5 +30,6 @@ 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 +qt_themes/colorful/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com <!-- TODO: Add the license of the yuzu icon -->
\ No newline at end of file diff --git a/dist/qt_themes/colorful/icons/48x48/star.png b/dist/qt_themes/colorful/icons/48x48/star.png Binary files differnew file mode 100644 index 000000000..43b5d52ed --- /dev/null +++ b/dist/qt_themes/colorful/icons/48x48/star.png diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc index 36735519a..18b10869e 100644 --- a/dist/qt_themes/colorful/style.qrc +++ b/dist/qt_themes/colorful/style.qrc @@ -7,6 +7,7 @@ <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="48x48/star.png">icons/48x48/star.png</file> <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> </qresource> <qresource prefix="colorful"> diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index 2182f33f3..b195747a3 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -10,6 +10,7 @@ <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="48x48/star.png">icons/48x48/star.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> diff --git a/dist/qt_themes/default/icons/48x48/star.png b/dist/qt_themes/default/icons/48x48/star.png Binary files differnew file mode 100644 index 000000000..740f7f3e7 --- /dev/null +++ b/dist/qt_themes/default/icons/48x48/star.png diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/star.png b/dist/qt_themes/qdarkstyle/icons/48x48/star.png Binary files differnew file mode 100644 index 000000000..90d423a1d --- /dev/null +++ b/dist/qt_themes/qdarkstyle/icons/48x48/star.png diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc index 2b91204f3..34e872d25 100644 --- a/dist/qt_themes/qdarkstyle/style.qrc +++ b/dist/qt_themes/qdarkstyle/style.qrc @@ -8,6 +8,7 @@ <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="48x48/star.png">icons/48x48/star.png</file> <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> </qresource> <qresource prefix="qss_icons"> diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png b/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png Binary files differnew file mode 100644 index 000000000..90d423a1d --- /dev/null +++ b/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc index 579e73ece..142dd3288 100644 --- a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc +++ b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc @@ -8,6 +8,7 @@ <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="48x48/star.png">icons/48x48/star.png</file> <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> </qresource> <qresource prefix="qss_icons"> diff --git a/license.txt b/license.txt index 86e7b3c1b..495f3e676 100644 --- a/license.txt +++ b/license.txt @@ -358,6 +358,7 @@ chip.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com folder.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com sd_card.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com +star.png | CC BY-ND 3.0 | https://icons8.com Note: Some icons are different in different themes, and they are separately listed diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 532e418b0..04cf3f5b9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -377,6 +377,8 @@ add_library(core STATIC hle/service/glue/arp.h hle/service/glue/bgtc.cpp hle/service/glue/bgtc.h + hle/service/glue/ectx.cpp + hle/service/glue/ectx.h hle/service/glue/errors.h hle/service/glue/glue.cpp hle/service/glue/glue.h diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 4c8216b47..58c7f2930 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -687,7 +687,7 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, {501, nullptr, "SuppressDisablingSleepTemporarily"}, {502, nullptr, "IsSleepEnabled"}, {503, nullptr, "IsDisablingSleepSuppressed"}, - {900, nullptr, "SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled"}, + {900, &ICommonStateGetter::SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled, "SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled"}, }; // clang-format on @@ -817,6 +817,14 @@ void ICommonStateGetter::SetCpuBoostMode(Kernel::HLERequestContext& ctx) { apm_sys->SetCpuBoostMode(ctx); } +void ICommonStateGetter::SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled( + Kernel::HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); +} + IStorageImpl::~IStorageImpl() = default; class StorageDataImpl final : public IStorageImpl { diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 756434716..5d302e155 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -196,6 +196,7 @@ private: void EndVrModeEx(Kernel::HLERequestContext& ctx); void GetDefaultDisplayResolution(Kernel::HLERequestContext& ctx); void SetCpuBoostMode(Kernel::HLERequestContext& ctx); + void SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled(Kernel::HLERequestContext& ctx); std::shared_ptr<AppletMessageQueue> msg_queue; bool vr_mode_state{}; diff --git a/src/core/hle/service/glue/ectx.cpp b/src/core/hle/service/glue/ectx.cpp new file mode 100644 index 000000000..249c6f003 --- /dev/null +++ b/src/core/hle/service/glue/ectx.cpp @@ -0,0 +1,22 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/hle/service/glue/ectx.h" + +namespace Service::Glue { + +ECTX_AW::ECTX_AW(Core::System& system_) : ServiceFramework{system_, "ectx:aw"} { + // clang-format off + static const FunctionInfo functions[] = { + {0, nullptr, "CreateContextRegistrar"}, + {1, nullptr, "CommitContext"}, + }; + // clang-format on + + RegisterHandlers(functions); +} + +ECTX_AW::~ECTX_AW() = default; + +} // namespace Service::Glue diff --git a/src/core/hle/service/glue/ectx.h b/src/core/hle/service/glue/ectx.h new file mode 100644 index 000000000..b275e808a --- /dev/null +++ b/src/core/hle/service/glue/ectx.h @@ -0,0 +1,21 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/hle/service/service.h" + +namespace Core { +class System; +} + +namespace Service::Glue { + +class ECTX_AW final : public ServiceFramework<ECTX_AW> { +public: + explicit ECTX_AW(Core::System& system_); + ~ECTX_AW() override; +}; + +} // namespace Service::Glue diff --git a/src/core/hle/service/glue/glue.cpp b/src/core/hle/service/glue/glue.cpp index 4eafbe5fa..a08dc9758 100644 --- a/src/core/hle/service/glue/glue.cpp +++ b/src/core/hle/service/glue/glue.cpp @@ -6,6 +6,7 @@ #include "core/core.h" #include "core/hle/service/glue/arp.h" #include "core/hle/service/glue/bgtc.h" +#include "core/hle/service/glue/ectx.h" #include "core/hle/service/glue/glue.h" namespace Service::Glue { @@ -20,6 +21,9 @@ void InstallInterfaces(Core::System& system) { // BackGround Task Controller std::make_shared<BGTC_T>(system)->InstallAsService(system.ServiceManager()); std::make_shared<BGTC_SC>(system)->InstallAsService(system.ServiceManager()); + + // Error Context + std::make_shared<ECTX_AW>(system)->InstallAsService(system.ServiceManager()); } } // namespace Service::Glue diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp index 4e58b9b80..e2f671d8e 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp @@ -31,9 +31,8 @@ NvResult nvhost_nvdec::Ioctl1(DeviceFD fd, Ioctl command, const std::vector<u8>& return SetSubmitTimeout(input, output); case 0x9: return MapBuffer(input, output); - case 0xa: { + case 0xa: return UnmapBuffer(input, output); - } default: break; } @@ -67,7 +66,8 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, const std::vector<u8>& void nvhost_nvdec::OnOpen(DeviceFD fd) {} void nvhost_nvdec::OnClose(DeviceFD fd) { - system.GPU().ClearCommandBuffer(); + LOG_INFO(Service_NVDRV, "NVDEC video stream ended"); + system.GPU().ClearCdmaInstance(); } } // namespace Service::Nvidia::Devices diff --git a/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp b/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp index 0421fb956..301efe8a1 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp @@ -29,13 +29,8 @@ NvResult nvhost_vic::Ioctl1(DeviceFD fd, Ioctl command, const std::vector<u8>& i return GetWaitbase(input, output); case 0x9: return MapBuffer(input, output); - case 0xa: { - if (command.length == 0x1c) { - Tegra::ChCommandHeaderList cmdlist{{0xDEADB33F}}; - system.GPU().PushCommandBuffer(cmdlist); - } + case 0xa: return UnmapBuffer(input, output); - } default: break; } @@ -69,6 +64,9 @@ NvResult nvhost_vic::Ioctl3(DeviceFD fd, Ioctl command, const std::vector<u8>& i } void nvhost_vic::OnOpen(DeviceFD fd) {} -void nvhost_vic::OnClose(DeviceFD fd) {} + +void nvhost_vic::OnClose(DeviceFD fd) { + system.GPU().ClearCdmaInstance(); +} } // namespace Service::Nvidia::Devices diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 7c42f1177..a38024242 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -492,10 +492,8 @@ void GPU::PushCommandBuffer(Tegra::ChCommandHeaderList& entries) { cdma_pusher->ProcessEntries(std::move(entries)); } -void GPU::ClearCommandBuffer() { - // This condition fires when a video stream ends, clear all intermediary data +void GPU::ClearCdmaInstance() { cdma_pusher.reset(); - LOG_INFO(Service_NVDRV, "NVDEC video stream ended"); } void GPU::SwapBuffers(const Tegra::FramebufferConfig* framebuffer) { diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index b1960ea86..8669e9940 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -324,8 +324,8 @@ public: /// Push GPU command buffer entries to be processed void PushCommandBuffer(Tegra::ChCommandHeaderList& entries); - /// Frees the CDMAPusher to free up resources - void ClearCommandBuffer(); + /// Frees the CDMAPusher instance to free up resources + void ClearCdmaInstance(); /// Swap buffers (render frame) void SwapBuffers(const Tegra::FramebufferConfig* framebuffer); diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index bc2a53841..017348e05 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -417,7 +417,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { }; } -[[nodiscard]] constexpr SwizzleSource ConvertGreenRed(SwizzleSource value) { +[[nodiscard]] SwizzleSource ConvertGreenRed(SwizzleSource value) { switch (value) { case SwizzleSource::G: return SwizzleSource::R; @@ -426,6 +426,17 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { } } +[[nodiscard]] SwizzleSource SwapBlueRed(SwizzleSource value) { + switch (value) { + case SwizzleSource::R: + return SwizzleSource::B; + case SwizzleSource::B: + return SwizzleSource::R; + default: + return value; + } +} + void CopyBufferToImage(vk::CommandBuffer cmdbuf, VkBuffer src_buffer, VkImage image, VkImageAspectFlags aspect_mask, bool is_initialized, std::span<const VkBufferImageCopy> copies) { @@ -543,6 +554,15 @@ void CopyBufferToImage(vk::CommandBuffer cmdbuf, VkBuffer src_buffer, VkImage im }; } +[[nodiscard]] bool IsFormatFlipped(PixelFormat format) { + switch (format) { + case PixelFormat::A1B5G5R5_UNORM: + return true; + default: + return false; + } +} + struct RangedBarrierRange { u32 min_mip = std::numeric_limits<u32>::max(); u32 max_mip = std::numeric_limits<u32>::min(); @@ -948,6 +968,9 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI }; if (!info.IsRenderTarget()) { swizzle = info.Swizzle(); + if (IsFormatFlipped(format)) { + std::ranges::transform(swizzle, swizzle.begin(), SwapBlueRed); + } if ((aspect_mask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) { std::ranges::transform(swizzle, swizzle.begin(), ConvertGreenRed); } diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 851246233..d1b8c4fc9 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -29,9 +29,10 @@ Config::~Config() { } const std::array<int, Settings::NativeButton::NumButtons> Config::default_buttons = { - Qt::Key_A, Qt::Key_S, Qt::Key_Z, Qt::Key_X, Qt::Key_3, Qt::Key_4, Qt::Key_Q, - Qt::Key_W, Qt::Key_1, Qt::Key_2, Qt::Key_N, Qt::Key_M, Qt::Key_F, Qt::Key_T, - Qt::Key_H, Qt::Key_G, Qt::Key_D, Qt::Key_C, Qt::Key_B, Qt::Key_V, + Qt::Key_C, Qt::Key_X, Qt::Key_V, Qt::Key_Z, Qt::Key_F, + Qt::Key_G, Qt::Key_Q, Qt::Key_E, Qt::Key_R, Qt::Key_T, + Qt::Key_M, Qt::Key_N, Qt::Key_Left, Qt::Key_Up, Qt::Key_Right, + Qt::Key_Down, Qt::Key_Q, Qt::Key_E, 0, 0, }; const std::array<int, Settings::NativeMotion::NumMotions> Config::default_motions = { @@ -41,10 +42,10 @@ const std::array<int, Settings::NativeMotion::NumMotions> Config::default_motion const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> Config::default_analogs{{ { - Qt::Key_Up, - Qt::Key_Down, - Qt::Key_Left, - Qt::Key_Right, + Qt::Key_W, + Qt::Key_S, + Qt::Key_A, + Qt::Key_D, }, { Qt::Key_I, @@ -55,8 +56,8 @@ const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> Config: }}; const std::array<int, 2> Config::default_stick_mod = { - Qt::Key_E, - Qt::Key_R, + Qt::Key_Shift, + 0, }; const std::array<int, Settings::NativeMouseButton::NumMouseButtons> Config::default_mouse_buttons = @@ -936,6 +937,13 @@ void Config::ReadUIGamelistValues() { UISettings::values.row_2_text_id = ReadSetting(QStringLiteral("row_2_text_id"), 2).toUInt(); UISettings::values.cache_game_list = ReadSetting(QStringLiteral("cache_game_list"), true).toBool(); + const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites")); + for (int i = 0; i < favorites_size; i++) { + qt_config->setArrayIndex(i); + UISettings::values.favorited_ids.append( + ReadSetting(QStringLiteral("program_id")).toULongLong()); + } + qt_config->endArray(); qt_config->endGroup(); } @@ -1479,6 +1487,13 @@ void Config::SaveUIGamelistValues() { WriteSetting(QStringLiteral("row_1_text_id"), UISettings::values.row_1_text_id, 3); WriteSetting(QStringLiteral("row_2_text_id"), UISettings::values.row_2_text_id, 2); WriteSetting(QStringLiteral("cache_game_list"), UISettings::values.cache_game_list, true); + qt_config->beginWriteArray(QStringLiteral("favorites")); + for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) { + qt_config->setArrayIndex(i); + WriteSetting(QStringLiteral("program_id"), + QVariant::fromValue(UISettings::values.favorited_ids[i])); + } + qt_config->endArray(); qt_config->endGroup(); } diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 9afd5b45f..48b78d65f 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -11,6 +11,7 @@ #include <QJsonDocument> #include <QJsonObject> #include <QKeyEvent> +#include <QList> #include <QMenu> #include <QThreadPool> #include <fmt/format.h> @@ -84,6 +85,10 @@ void GameListSearchField::setFilterResult(int visible, int total) { label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); } +bool GameListSearchField::isEmpty() const { + return edit_filter->text().isEmpty(); +} + QString GameList::GetLastFilterResultItem() const { QString file_path; const int folder_count = item_model->rowCount(); @@ -187,7 +192,9 @@ void GameList::OnTextChanged(const QString& new_text) { // If the searchfield is empty every item is visible // Otherwise the filter gets applied if (edit_filter_text.isEmpty()) { - for (int i = 0; i < folder_count; ++i) { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + UISettings::values.favorited_ids.size() == 0); + for (int i = 1; i < folder_count; ++i) { folder = item_model->item(i, 0); const QModelIndex folder_index = folder->index(); const int children_count = folder->rowCount(); @@ -198,8 +205,9 @@ void GameList::OnTextChanged(const QString& new_text) { } search_field->setFilterResult(children_total, children_total); } else { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); int result_count = 0; - for (int i = 0; i < folder_count; ++i) { + for (int i = 1; i < folder_count; ++i) { folder = item_model->item(i, 0); const QModelIndex folder_index = folder->index(); const int children_count = folder->rowCount(); @@ -280,6 +288,13 @@ void GameList::OnUpdateThemedIcons() { .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), Qt::DecorationRole); break; + case GameListItemType::Favorites: + child->setData( + QIcon::fromTheme(QStringLiteral("star")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; default: break; } @@ -427,6 +442,13 @@ void GameList::DonePopulating(const QStringList& watch_list) { emit ShowList(!IsEmpty()); item_model->invisibleRootItem()->appendRow(new GameListAddDir()); + item_model->invisibleRootItem()->insertRow(0, new GameListFavorites()); + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + UISettings::values.favorited_ids.size() == 0); + tree_view->expand(item_model->invisibleRootItem()->child(0)->index()); + for (const auto id : UISettings::values.favorited_ids) { + AddFavorite(id); + } // Clear out the old directories to watch for changes and add the new ones auto watch_dirs = watcher->directories(); @@ -446,7 +468,7 @@ void GameList::DonePopulating(const QStringList& watch_list) { tree_view->setEnabled(true); const int folder_count = tree_view->model()->rowCount(); int children_total = 0; - for (int i = 0; i < folder_count; ++i) { + for (int i = 1; i < folder_count; ++i) { children_total += item_model->item(i, 0)->rowCount(); } search_field->setFilterResult(children_total, children_total); @@ -478,6 +500,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { case GameListItemType::SysNandDir: AddPermDirPopup(context_menu, selected); break; + case GameListItemType::Favorites: + AddFavoritesPopup(context_menu); + break; default: break; } @@ -485,6 +510,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { } void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) { + QAction* favorite = context_menu.addAction(tr("Favorite")); + context_menu.addSeparator(); QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location")); QAction* open_transferable_shader_cache = @@ -503,6 +530,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri context_menu.addSeparator(); QAction* properties = context_menu.addAction(tr("Properties")); + favorite->setVisible(program_id != 0); + favorite->setCheckable(true); + favorite->setChecked(UISettings::values.favorited_ids.contains(program_id)); open_save_location->setVisible(program_id != 0); open_mod_location->setVisible(program_id != 0); open_transferable_shader_cache->setVisible(program_id != 0); @@ -513,6 +543,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); + connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); }); connect(open_save_location, &QAction::triggered, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path); }); @@ -576,7 +607,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { const int row = selected.row(); - move_up->setEnabled(row > 0); + move_up->setEnabled(row > 1); move_down->setEnabled(row < item_model->rowCount() - 2); connect(move_up, &QAction::triggered, [this, selected, row, game_dir_index] { @@ -614,6 +645,18 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { }); } +void GameList::AddFavoritesPopup(QMenu& context_menu) { + QAction* clear_all = context_menu.addAction(tr("Clear")); + + connect(clear_all, &QAction::triggered, [this] { + for (const auto id : UISettings::values.favorited_ids) { + RemoveFavorite(id); + } + UISettings::values.favorited_ids.clear(); + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); + }); +} + void GameList::LoadCompatibilityList() { QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")}; @@ -728,6 +771,58 @@ void GameList::RefreshGameDirectory() { } } +void GameList::ToggleFavorite(u64 program_id) { + if (!UISettings::values.favorited_ids.contains(program_id)) { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), + !search_field->isEmpty()); + UISettings::values.favorited_ids.append(program_id); + AddFavorite(program_id); + item_model->sort(tree_view->header()->sortIndicatorSection(), + tree_view->header()->sortIndicatorOrder()); + } else { + UISettings::values.favorited_ids.removeOne(program_id); + RemoveFavorite(program_id); + if (UISettings::values.favorited_ids.size() == 0) { + tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); + } + } +} + +void GameList::AddFavorite(u64 program_id) { + auto* favorites_row = item_model->item(0); + + for (int i = 1; i < item_model->rowCount() - 1; i++) { + const auto* folder = item_model->item(i); + for (int j = 0; j < folder->rowCount(); j++) { + if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() == + program_id) { + QList<QStandardItem*> list; + for (int k = 0; k < item_model->columnCount(); k++) { + list.append(folder->child(j, k)->clone()); + } + list[0]->setData(folder->child(j)->data(GameListItem::SortRole), + GameListItem::SortRole); + list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString()); + + favorites_row->appendRow(list); + return; + } + } + } +} + +void GameList::RemoveFavorite(u64 program_id) { + auto* favorites_row = item_model->item(0); + + for (int i = 0; i < favorites_row->rowCount(); i++) { + const auto* game = favorites_row->child(i); + if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { + favorites_row->removeRow(i); + return; + } + } +} + GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { connect(parent, &GMainWindow::UpdateThemedIcons, this, &GameListPlaceholder::onUpdateThemedIcons); diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 58059a3c4..9c0a1a482 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -112,10 +112,15 @@ private: void RefreshGameDirectory(); + void ToggleFavorite(u64 program_id); + void AddFavorite(u64 program_id); + void RemoveFavorite(u64 program_id); + void PopupContextMenu(const QPoint& menu_location); void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path); void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); + void AddFavoritesPopup(QMenu& context_menu); std::shared_ptr<FileSys::VfsFilesystem> vfs; FileSys::ManualContentProvider* provider; diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index f25445f18..7ca8ece23 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -29,7 +29,8 @@ enum class GameListItemType { SdmcDir = QStandardItem::UserType + 3, UserNandDir = QStandardItem::UserType + 4, SysNandDir = QStandardItem::UserType + 5, - AddDir = QStandardItem::UserType + 6 + AddDir = QStandardItem::UserType + 6, + Favorites = QStandardItem::UserType + 7, }; Q_DECLARE_METATYPE(GameListItemType); @@ -310,6 +311,28 @@ public: } }; +class GameListFavorites : public GameListItem { +public: + explicit GameListFavorites() { + setData(type(), TypeRole); + + const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); + setData(QIcon::fromTheme(QStringLiteral("star")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(QObject::tr("Favorites"), Qt::DisplayRole); + } + + int type() const override { + return static_cast<int>(GameListItemType::Favorites); + } + + bool operator<(const QStandardItem& other) const override { + return false; + } +}; + class GameList; class QHBoxLayout; class QTreeView; @@ -324,6 +347,7 @@ public: explicit GameListSearchField(GameList* parent = nullptr); void setFilterResult(int visible, int total); + bool isEmpty() const; void clear(); void setFocus(); diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index ce3945485..5ba00b8c8 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -74,6 +74,7 @@ struct Values { QString game_dir_deprecated; bool game_dir_deprecated_deepscan; QVector<UISettings::GameDir> game_dirs; + QVector<u64> favorited_ids; QStringList recent_files; QString language; |