diff options
| author | 2023-11-19 09:50:49 -0500 | |
|---|---|---|
| committer | 2023-11-19 09:50:49 -0500 | |
| commit | c08da2d6add4f8e6770d408ad8c3361886e55469 (patch) | |
| tree | 3a6fa7b115e9b2cd3ade1075ae5023b43c5b833b /src | |
| parent | Merge pull request #12066 from ameerj/nvidia-nsanity (diff) | |
| parent | Fix out_icon_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir); (diff) | |
| download | yuzu-c08da2d6add4f8e6770d408ad8c3361886e55469.tar.gz yuzu-c08da2d6add4f8e6770d408ad8c3361886e55469.tar.xz yuzu-c08da2d6add4f8e6770d408ad8c3361886e55469.zip | |
Merge pull request #11792 from boludoz/new-shortcut
Improved shortcut: add games in applist for Windows, question for sta…
Diffstat (limited to '')
| -rw-r--r-- | src/yuzu/game_list.cpp | 6 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 425 | ||||
| -rw-r--r-- | src/yuzu/main.h | 21 | ||||
| -rw-r--r-- | src/yuzu/util/util.cpp | 12 | ||||
| -rw-r--r-- | src/yuzu/util/util.h | 3 |
5 files changed, 259 insertions, 208 deletions
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 7e7d8e252..f294dc23d 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp | |||
| @@ -567,9 +567,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 567 | QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); | 567 | QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); |
| 568 | QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); | 568 | QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); |
| 569 | QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); | 569 | QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); |
| 570 | // TODO: Implement shortcut creation for macOS | ||
| 571 | #if !defined(__APPLE__) | ||
| 570 | QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); | 572 | QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); |
| 571 | QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); | 573 | QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); |
| 572 | #ifndef WIN32 | ||
| 573 | QAction* create_applications_menu_shortcut = | 574 | QAction* create_applications_menu_shortcut = |
| 574 | shortcut_menu->addAction(tr("Add to Applications Menu")); | 575 | shortcut_menu->addAction(tr("Add to Applications Menu")); |
| 575 | #endif | 576 | #endif |
| @@ -647,10 +648,11 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 647 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { | 648 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { |
| 648 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); | 649 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); |
| 649 | }); | 650 | }); |
| 651 | // TODO: Implement shortcut creation for macOS | ||
| 652 | #if !defined(__APPLE__) | ||
| 650 | connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { | 653 | connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { |
| 651 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); | 654 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); |
| 652 | }); | 655 | }); |
| 653 | #ifndef WIN32 | ||
| 654 | connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { | 656 | connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { |
| 655 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); | 657 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); |
| 656 | }); | 658 | }); |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index f077d7f9c..f6b548fd3 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -10,6 +10,7 @@ | |||
| 10 | #include <thread> | 10 | #include <thread> |
| 11 | #include "core/loader/nca.h" | 11 | #include "core/loader/nca.h" |
| 12 | #include "core/tools/renderdoc.h" | 12 | #include "core/tools/renderdoc.h" |
| 13 | |||
| 13 | #ifdef __APPLE__ | 14 | #ifdef __APPLE__ |
| 14 | #include <unistd.h> // for chdir | 15 | #include <unistd.h> // for chdir |
| 15 | #endif | 16 | #endif |
| @@ -2847,170 +2848,259 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, | |||
| 2847 | QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); | 2848 | QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); |
| 2848 | } | 2849 | } |
| 2849 | 2850 | ||
| 2850 | void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, | 2851 | bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, |
| 2851 | GameListShortcutTarget target) { | 2852 | const std::string& comment, |
| 2852 | // Get path to yuzu executable | 2853 | const std::filesystem::path& icon_path, |
| 2853 | const QStringList args = QApplication::arguments(); | 2854 | const std::filesystem::path& command, |
| 2854 | std::filesystem::path yuzu_command = args[0].toStdString(); | 2855 | const std::string& arguments, const std::string& categories, |
| 2855 | 2856 | const std::string& keywords, const std::string& name) try { | |
| 2856 | // If relative path, make it an absolute path | 2857 | #if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD |
| 2857 | if (yuzu_command.c_str()[0] == '.') { | 2858 | std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop"); |
| 2858 | yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; | 2859 | std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); |
| 2860 | if (!shortcut_stream.is_open()) { | ||
| 2861 | LOG_ERROR(Frontend, "Failed to create shortcut"); | ||
| 2862 | return false; | ||
| 2859 | } | 2863 | } |
| 2860 | 2864 | // TODO: Migrate fmt::print to std::print in futures STD C++ 23. | |
| 2861 | #if defined(__linux__) | 2865 | fmt::print(shortcut_stream, "[Desktop Entry]\n"); |
| 2862 | // Warn once if we are making a shortcut to a volatile AppImage | 2866 | fmt::print(shortcut_stream, "Type=Application\n"); |
| 2863 | const std::string appimage_ending = | 2867 | fmt::print(shortcut_stream, "Version=1.0\n"); |
| 2864 | std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); | 2868 | fmt::print(shortcut_stream, "Name={}\n", name); |
| 2865 | if (yuzu_command.string().ends_with(appimage_ending) && | 2869 | if (!comment.empty()) { |
| 2866 | !UISettings::values.shortcut_already_warned) { | 2870 | fmt::print(shortcut_stream, "Comment={}\n", comment); |
| 2867 | if (QMessageBox::warning(this, tr("Create Shortcut"), | 2871 | } |
| 2868 | tr("This will create a shortcut to the current AppImage. This may " | 2872 | if (std::filesystem::is_regular_file(icon_path)) { |
| 2869 | "not work well if you update. Continue?"), | 2873 | fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); |
| 2870 | QMessageBox::StandardButton::Ok | | 2874 | } |
| 2871 | QMessageBox::StandardButton::Cancel) == | 2875 | fmt::print(shortcut_stream, "TryExec={}\n", command.string()); |
| 2872 | QMessageBox::StandardButton::Cancel) { | 2876 | fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments); |
| 2873 | return; | 2877 | if (!categories.empty()) { |
| 2878 | fmt::print(shortcut_stream, "Categories={}\n", categories); | ||
| 2879 | } | ||
| 2880 | if (!keywords.empty()) { | ||
| 2881 | fmt::print(shortcut_stream, "Keywords={}\n", keywords); | ||
| 2882 | } | ||
| 2883 | return true; | ||
| 2884 | #elif defined(_WIN32) // Windows | ||
| 2885 | HRESULT hr = CoInitialize(nullptr); | ||
| 2886 | if (FAILED(hr)) { | ||
| 2887 | LOG_ERROR(Frontend, "CoInitialize failed"); | ||
| 2888 | return false; | ||
| 2889 | } | ||
| 2890 | SCOPE_EXIT({ CoUninitialize(); }); | ||
| 2891 | IShellLinkW* ps1 = nullptr; | ||
| 2892 | IPersistFile* persist_file = nullptr; | ||
| 2893 | SCOPE_EXIT({ | ||
| 2894 | if (persist_file != nullptr) { | ||
| 2895 | persist_file->Release(); | ||
| 2874 | } | 2896 | } |
| 2875 | UISettings::values.shortcut_already_warned = true; | 2897 | if (ps1 != nullptr) { |
| 2898 | ps1->Release(); | ||
| 2899 | } | ||
| 2900 | }); | ||
| 2901 | HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, | ||
| 2902 | reinterpret_cast<void**>(&ps1)); | ||
| 2903 | if (FAILED(hres)) { | ||
| 2904 | LOG_ERROR(Frontend, "Failed to create IShellLinkW instance"); | ||
| 2905 | return false; | ||
| 2876 | } | 2906 | } |
| 2877 | #endif // __linux__ | 2907 | hres = ps1->SetPath(command.c_str()); |
| 2878 | 2908 | if (FAILED(hres)) { | |
| 2879 | std::filesystem::path target_directory{}; | 2909 | LOG_ERROR(Frontend, "Failed to set path"); |
| 2880 | 2910 | return false; | |
| 2881 | switch (target) { | ||
| 2882 | case GameListShortcutTarget::Desktop: { | ||
| 2883 | const QString desktop_path = | ||
| 2884 | QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); | ||
| 2885 | target_directory = desktop_path.toUtf8().toStdString(); | ||
| 2886 | break; | ||
| 2887 | } | 2911 | } |
| 2888 | case GameListShortcutTarget::Applications: { | 2912 | if (!arguments.empty()) { |
| 2889 | const QString applications_path = | 2913 | hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data()); |
| 2890 | QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); | 2914 | if (FAILED(hres)) { |
| 2891 | if (applications_path.isEmpty()) { | 2915 | LOG_ERROR(Frontend, "Failed to set arguments"); |
| 2892 | const char* home = std::getenv("HOME"); | 2916 | return false; |
| 2893 | if (home != nullptr) { | ||
| 2894 | target_directory = std::filesystem::path(home) / ".local/share/applications"; | ||
| 2895 | } | ||
| 2896 | } else { | ||
| 2897 | target_directory = applications_path.toUtf8().toStdString(); | ||
| 2898 | } | 2917 | } |
| 2899 | break; | ||
| 2900 | } | 2918 | } |
| 2901 | default: | 2919 | if (!comment.empty()) { |
| 2902 | return; | 2920 | hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data()); |
| 2921 | if (FAILED(hres)) { | ||
| 2922 | LOG_ERROR(Frontend, "Failed to set description"); | ||
| 2923 | return false; | ||
| 2924 | } | ||
| 2903 | } | 2925 | } |
| 2904 | 2926 | if (std::filesystem::is_regular_file(icon_path)) { | |
| 2905 | const QDir dir(QString::fromStdString(target_directory.generic_string())); | 2927 | hres = ps1->SetIconLocation(icon_path.c_str(), 0); |
| 2906 | if (!dir.exists()) { | 2928 | if (FAILED(hres)) { |
| 2907 | QMessageBox::critical(this, tr("Create Shortcut"), | 2929 | LOG_ERROR(Frontend, "Failed to set icon location"); |
| 2908 | tr("Cannot create shortcut. Path \"%1\" does not exist.") | 2930 | return false; |
| 2909 | .arg(QString::fromStdString(target_directory.generic_string())), | 2931 | } |
| 2910 | QMessageBox::StandardButton::Ok); | ||
| 2911 | return; | ||
| 2912 | } | 2932 | } |
| 2933 | hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&persist_file)); | ||
| 2934 | if (FAILED(hres)) { | ||
| 2935 | LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); | ||
| 2936 | return false; | ||
| 2937 | } | ||
| 2938 | hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); | ||
| 2939 | if (FAILED(hres)) { | ||
| 2940 | LOG_ERROR(Frontend, "Failed to save shortcut"); | ||
| 2941 | return false; | ||
| 2942 | } | ||
| 2943 | return true; | ||
| 2944 | #else // Unsupported platform | ||
| 2945 | return false; | ||
| 2946 | #endif | ||
| 2947 | } catch (const std::exception& e) { | ||
| 2948 | LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what()); | ||
| 2949 | return false; | ||
| 2950 | } | ||
| 2951 | // Messages in pre-defined message boxes for less code spaghetti | ||
| 2952 | bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title) { | ||
| 2953 | int result = 0; | ||
| 2954 | QMessageBox::StandardButtons buttons; | ||
| 2955 | switch (imsg) { | ||
| 2956 | case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES: | ||
| 2957 | buttons = QMessageBox::Yes | QMessageBox::No; | ||
| 2958 | result = | ||
| 2959 | QMessageBox::information(parent, tr("Create Shortcut"), | ||
| 2960 | tr("Do you want to launch the game in fullscreen?"), buttons); | ||
| 2961 | return result == QMessageBox::Yes; | ||
| 2962 | case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: | ||
| 2963 | QMessageBox::information(parent, tr("Create Shortcut"), | ||
| 2964 | tr("Successfully created a shortcut to %1").arg(game_title)); | ||
| 2965 | return false; | ||
| 2966 | case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING: | ||
| 2967 | buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel; | ||
| 2968 | result = | ||
| 2969 | QMessageBox::warning(this, tr("Create Shortcut"), | ||
| 2970 | tr("This will create a shortcut to the current AppImage. This may " | ||
| 2971 | "not work well if you update. Continue?"), | ||
| 2972 | buttons); | ||
| 2973 | return result == QMessageBox::Ok; | ||
| 2974 | default: | ||
| 2975 | buttons = QMessageBox::Ok; | ||
| 2976 | QMessageBox::critical(parent, tr("Create Shortcut"), | ||
| 2977 | tr("Failed to create a shortcut to %1").arg(game_title), buttons); | ||
| 2978 | return false; | ||
| 2979 | } | ||
| 2980 | } | ||
| 2913 | 2981 | ||
| 2914 | const std::string game_file_name = std::filesystem::path(game_path).filename().string(); | 2982 | bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, |
| 2915 | // Determine full paths for icon and shortcut | 2983 | std::filesystem::path& out_icon_path) { |
| 2916 | #if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) | 2984 | // Get path to Yuzu icons directory & icon extension |
| 2917 | const char* home = std::getenv("HOME"); | 2985 | std::string ico_extension = "png"; |
| 2918 | const std::filesystem::path home_path = (home == nullptr ? "~" : home); | 2986 | #if defined(_WIN32) |
| 2919 | const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); | 2987 | out_icon_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir); |
| 2920 | 2988 | ico_extension = "ico"; | |
| 2921 | std::filesystem::path system_icons_path = | 2989 | #elif defined(__linux__) || defined(__FreeBSD__) |
| 2922 | (xdg_data_home == nullptr ? home_path / ".local/share/" | 2990 | out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256"; |
| 2923 | : std::filesystem::path(xdg_data_home)) / | 2991 | #endif |
| 2924 | "icons/hicolor/256x256"; | 2992 | // Create icons directory if it doesn't exist |
| 2925 | if (!Common::FS::CreateDirs(system_icons_path)) { | 2993 | if (!Common::FS::CreateDirs(out_icon_path)) { |
| 2926 | QMessageBox::critical( | 2994 | QMessageBox::critical( |
| 2927 | this, tr("Create Icon"), | 2995 | this, tr("Create Icon"), |
| 2928 | tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") | 2996 | tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") |
| 2929 | .arg(QString::fromStdString(system_icons_path)), | 2997 | .arg(QString::fromStdString(out_icon_path.string())), |
| 2930 | QMessageBox::StandardButton::Ok); | 2998 | QMessageBox::StandardButton::Ok); |
| 2931 | return; | 2999 | out_icon_path.clear(); |
| 3000 | return false; | ||
| 2932 | } | 3001 | } |
| 2933 | std::filesystem::path icon_path = | ||
| 2934 | system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name) | ||
| 2935 | : fmt::format("yuzu-{:016X}.png", program_id)); | ||
| 2936 | const std::filesystem::path shortcut_path = | ||
| 2937 | target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) | ||
| 2938 | : fmt::format("yuzu-{:016X}.desktop", program_id)); | ||
| 2939 | #elif defined(WIN32) | ||
| 2940 | std::filesystem::path icons_path = | ||
| 2941 | Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); | ||
| 2942 | std::filesystem::path icon_path = | ||
| 2943 | icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) | ||
| 2944 | : fmt::format("yuzu-{:016X}.ico", program_id))); | ||
| 2945 | #else | ||
| 2946 | std::string icon_extension; | ||
| 2947 | #endif | ||
| 2948 | |||
| 2949 | // Get title from game file | ||
| 2950 | const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), | ||
| 2951 | system->GetContentProvider()}; | ||
| 2952 | const auto control = pm.GetControlMetadata(); | ||
| 2953 | const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); | ||
| 2954 | 3002 | ||
| 2955 | std::string title{fmt::format("{:016X}", program_id)}; | 3003 | // Create icon file path |
| 2956 | 3004 | out_icon_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension) | |
| 2957 | if (control.first != nullptr) { | 3005 | : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension)); |
| 2958 | title = control.first->GetApplicationName(); | 3006 | return true; |
| 2959 | } else { | 3007 | } |
| 2960 | loader->ReadTitle(title); | ||
| 2961 | } | ||
| 2962 | 3008 | ||
| 2963 | // Get icon from game file | 3009 | void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, |
| 2964 | std::vector<u8> icon_image_file{}; | 3010 | GameListShortcutTarget target) { |
| 2965 | if (control.second != nullptr) { | 3011 | std::string game_title; |
| 2966 | icon_image_file = control.second->ReadAllBytes(); | 3012 | QString qt_game_title; |
| 2967 | } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { | 3013 | std::filesystem::path out_icon_path; |
| 2968 | LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); | 3014 | // Get path to yuzu executable |
| 3015 | const QStringList args = QApplication::arguments(); | ||
| 3016 | std::filesystem::path yuzu_command = args[0].toStdString(); | ||
| 3017 | // If relative path, make it an absolute path | ||
| 3018 | if (yuzu_command.c_str()[0] == '.') { | ||
| 3019 | yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; | ||
| 2969 | } | 3020 | } |
| 2970 | 3021 | // Shortcut path | |
| 2971 | QImage icon_data = | 3022 | std::filesystem::path shortcut_path{}; |
| 2972 | QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); | 3023 | if (target == GameListShortcutTarget::Desktop) { |
| 2973 | #if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) | 3024 | shortcut_path = |
| 2974 | // Convert and write the icon as a PNG | 3025 | QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); |
| 2975 | if (!icon_data.save(QString::fromStdString(icon_path.string()))) { | 3026 | } else if (target == GameListShortcutTarget::Applications) { |
| 2976 | LOG_ERROR(Frontend, "Could not write icon as PNG to file"); | 3027 | shortcut_path = |
| 3028 | QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation).toStdString(); | ||
| 3029 | } | ||
| 3030 | // Icon path and title | ||
| 3031 | if (std::filesystem::exists(shortcut_path)) { | ||
| 3032 | // Get title from game file | ||
| 3033 | const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), | ||
| 3034 | system->GetContentProvider()}; | ||
| 3035 | const auto control = pm.GetControlMetadata(); | ||
| 3036 | const auto loader = | ||
| 3037 | Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); | ||
| 3038 | game_title = fmt::format("{:016X}", program_id); | ||
| 3039 | if (control.first != nullptr) { | ||
| 3040 | game_title = control.first->GetApplicationName(); | ||
| 3041 | } else { | ||
| 3042 | loader->ReadTitle(game_title); | ||
| 3043 | } | ||
| 3044 | // Delete illegal characters from title | ||
| 3045 | const std::string illegal_chars = "<>:\"/\\|?*."; | ||
| 3046 | for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) { | ||
| 3047 | if (illegal_chars.find(*it) != std::string::npos) { | ||
| 3048 | game_title.erase(it.base() - 1); | ||
| 3049 | } | ||
| 3050 | } | ||
| 3051 | qt_game_title = QString::fromStdString(game_title); | ||
| 3052 | // Get icon from game file | ||
| 3053 | std::vector<u8> icon_image_file{}; | ||
| 3054 | if (control.second != nullptr) { | ||
| 3055 | icon_image_file = control.second->ReadAllBytes(); | ||
| 3056 | } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { | ||
| 3057 | LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); | ||
| 3058 | } | ||
| 3059 | QImage icon_data = | ||
| 3060 | QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); | ||
| 3061 | if (GMainWindow::MakeShortcutIcoPath(program_id, game_title, out_icon_path)) { | ||
| 3062 | if (!SaveIconToFile(out_icon_path, icon_data)) { | ||
| 3063 | LOG_ERROR(Frontend, "Could not write icon to file"); | ||
| 3064 | } | ||
| 3065 | } | ||
| 2977 | } else { | 3066 | } else { |
| 2978 | LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); | 3067 | GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, |
| 2979 | } | 3068 | qt_game_title); |
| 2980 | #elif defined(WIN32) | 3069 | LOG_ERROR(Frontend, "Invalid shortcut target"); |
| 2981 | if (!SaveIconToFile(icon_path.string(), icon_data)) { | ||
| 2982 | LOG_ERROR(Frontend, "Could not write icon to file"); | ||
| 2983 | return; | 3070 | return; |
| 2984 | } | 3071 | } |
| 3072 | #if defined(__linux__) | ||
| 3073 | // Special case for AppImages | ||
| 3074 | // Warn once if we are making a shortcut to a volatile AppImage | ||
| 3075 | const std::string appimage_ending = | ||
| 3076 | std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); | ||
| 3077 | if (yuzu_command.string().ends_with(appimage_ending) && | ||
| 3078 | !UISettings::values.shortcut_already_warned) { | ||
| 3079 | if (GMainWindow::CreateShortcutMessagesGUI( | ||
| 3080 | this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, qt_game_title)) { | ||
| 3081 | return; | ||
| 3082 | } | ||
| 3083 | UISettings::values.shortcut_already_warned = true; | ||
| 3084 | } | ||
| 2985 | #endif // __linux__ | 3085 | #endif // __linux__ |
| 2986 | 3086 | // Create shortcut | |
| 2987 | #ifdef _WIN32 | 3087 | std::string arguments = fmt::format("-g \"{:s}\"", game_path); |
| 2988 | // Replace characters that are illegal in Windows filenames by a dash | 3088 | if (GMainWindow::CreateShortcutMessagesGUI( |
| 2989 | const std::string illegal_chars = "<>:\"/\\|?*"; | 3089 | this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, qt_game_title)) { |
| 2990 | for (char c : illegal_chars) { | 3090 | arguments = "-f " + arguments; |
| 2991 | std::replace(title.begin(), title.end(), c, '_'); | ||
| 2992 | } | 3091 | } |
| 2993 | const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); | 3092 | const std::string comment = fmt::format("Start {:s} with the yuzu Emulator", game_title); |
| 2994 | #endif | ||
| 2995 | |||
| 2996 | const std::string comment = | ||
| 2997 | tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); | ||
| 2998 | const std::string arguments = fmt::format("-g \"{:s}\"", game_path); | ||
| 2999 | const std::string categories = "Game;Emulator;Qt;"; | 3093 | const std::string categories = "Game;Emulator;Qt;"; |
| 3000 | const std::string keywords = "Switch;Nintendo;"; | 3094 | const std::string keywords = "Switch;Nintendo;"; |
| 3001 | 3095 | ||
| 3002 | if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), | 3096 | if (GMainWindow::CreateShortcutLink(shortcut_path, comment, out_icon_path, yuzu_command, |
| 3003 | yuzu_command.string(), arguments, categories, keywords)) { | 3097 | arguments, categories, keywords, game_title)) { |
| 3004 | QMessageBox::critical(this, tr("Create Shortcut"), | 3098 | GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS, |
| 3005 | tr("Failed to create a shortcut at %1") | 3099 | qt_game_title); |
| 3006 | .arg(QString::fromStdString(shortcut_path.string()))); | ||
| 3007 | return; | 3100 | return; |
| 3008 | } | 3101 | } |
| 3009 | 3102 | GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, | |
| 3010 | LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); | 3103 | qt_game_title); |
| 3011 | QMessageBox::information( | ||
| 3012 | this, tr("Create Shortcut"), | ||
| 3013 | tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); | ||
| 3014 | } | 3104 | } |
| 3015 | 3105 | ||
| 3016 | void GMainWindow::OnGameListOpenDirectory(const QString& directory) { | 3106 | void GMainWindow::OnGameListOpenDirectory(const QString& directory) { |
| @@ -4005,66 +4095,6 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file | |||
| 4005 | } | 4095 | } |
| 4006 | } | 4096 | } |
| 4007 | 4097 | ||
| 4008 | bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title, | ||
| 4009 | const std::string& comment, const std::string& icon_path, | ||
| 4010 | const std::string& command, const std::string& arguments, | ||
| 4011 | const std::string& categories, const std::string& keywords) { | ||
| 4012 | #if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) | ||
| 4013 | // This desktop file template was writing referencing | ||
| 4014 | // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html | ||
| 4015 | std::string shortcut_contents{}; | ||
| 4016 | shortcut_contents.append("[Desktop Entry]\n"); | ||
| 4017 | shortcut_contents.append("Type=Application\n"); | ||
| 4018 | shortcut_contents.append("Version=1.0\n"); | ||
| 4019 | shortcut_contents.append(fmt::format("Name={:s}\n", title)); | ||
| 4020 | shortcut_contents.append(fmt::format("Comment={:s}\n", comment)); | ||
| 4021 | shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path)); | ||
| 4022 | shortcut_contents.append(fmt::format("TryExec={:s}\n", command)); | ||
| 4023 | shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments)); | ||
| 4024 | shortcut_contents.append(fmt::format("Categories={:s}\n", categories)); | ||
| 4025 | shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords)); | ||
| 4026 | |||
| 4027 | std::ofstream shortcut_stream(shortcut_path); | ||
| 4028 | if (!shortcut_stream.is_open()) { | ||
| 4029 | LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path); | ||
| 4030 | return false; | ||
| 4031 | } | ||
| 4032 | shortcut_stream << shortcut_contents; | ||
| 4033 | shortcut_stream.close(); | ||
| 4034 | |||
| 4035 | return true; | ||
| 4036 | #elif defined(WIN32) | ||
| 4037 | IShellLinkW* shell_link; | ||
| 4038 | auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, | ||
| 4039 | (void**)&shell_link); | ||
| 4040 | if (FAILED(hres)) { | ||
| 4041 | return false; | ||
| 4042 | } | ||
| 4043 | shell_link->SetPath( | ||
| 4044 | Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to | ||
| 4045 | shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); | ||
| 4046 | shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); | ||
| 4047 | shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); | ||
| 4048 | |||
| 4049 | IPersistFile* persist_file; | ||
| 4050 | hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); | ||
| 4051 | if (FAILED(hres)) { | ||
| 4052 | return false; | ||
| 4053 | } | ||
| 4054 | |||
| 4055 | hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); | ||
| 4056 | if (FAILED(hres)) { | ||
| 4057 | return false; | ||
| 4058 | } | ||
| 4059 | |||
| 4060 | persist_file->Release(); | ||
| 4061 | shell_link->Release(); | ||
| 4062 | |||
| 4063 | return true; | ||
| 4064 | #endif | ||
| 4065 | return false; | ||
| 4066 | } | ||
| 4067 | |||
| 4068 | void GMainWindow::OnLoadAmiibo() { | 4098 | void GMainWindow::OnLoadAmiibo() { |
| 4069 | if (emu_thread == nullptr || !emu_thread->IsRunning()) { | 4099 | if (emu_thread == nullptr || !emu_thread->IsRunning()) { |
| 4070 | return; | 4100 | return; |
| @@ -4103,7 +4133,6 @@ void GMainWindow::OnLoadAmiibo() { | |||
| 4103 | bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text, | 4133 | bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text, |
| 4104 | QMessageBox::StandardButtons buttons, | 4134 | QMessageBox::StandardButtons buttons, |
| 4105 | QMessageBox::StandardButton defaultButton) { | 4135 | QMessageBox::StandardButton defaultButton) { |
| 4106 | |||
| 4107 | QMessageBox* box_dialog = new QMessageBox(parent); | 4136 | QMessageBox* box_dialog = new QMessageBox(parent); |
| 4108 | box_dialog->setWindowTitle(title); | 4137 | box_dialog->setWindowTitle(title); |
| 4109 | box_dialog->setText(text); | 4138 | box_dialog->setText(text); |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index f9c6efe4f..f67c4cfda 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | #include <memory> | 6 | #include <memory> |
| 7 | #include <optional> | 7 | #include <optional> |
| 8 | 8 | ||
| 9 | #include <filesystem> | ||
| 9 | #include <QMainWindow> | 10 | #include <QMainWindow> |
| 10 | #include <QMessageBox> | 11 | #include <QMessageBox> |
| 11 | #include <QPushButton> | 12 | #include <QPushButton> |
| @@ -174,6 +175,13 @@ class GMainWindow : public QMainWindow { | |||
| 174 | UI_EMU_STOPPING, | 175 | UI_EMU_STOPPING, |
| 175 | }; | 176 | }; |
| 176 | 177 | ||
| 178 | enum { | ||
| 179 | CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, | ||
| 180 | CREATE_SHORTCUT_MSGBOX_SUCCESS, | ||
| 181 | CREATE_SHORTCUT_MSGBOX_ERROR, | ||
| 182 | CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, | ||
| 183 | }; | ||
| 184 | |||
| 177 | public: | 185 | public: |
| 178 | void filterBarSetChecked(bool state); | 186 | void filterBarSetChecked(bool state); |
| 179 | void UpdateUITheme(); | 187 | void UpdateUITheme(); |
| @@ -456,11 +464,14 @@ private: | |||
| 456 | bool ConfirmShutdownGame(); | 464 | bool ConfirmShutdownGame(); |
| 457 | 465 | ||
| 458 | QString GetTasStateDescription() const; | 466 | QString GetTasStateDescription() const; |
| 459 | bool CreateShortcut(const std::string& shortcut_path, const std::string& title, | 467 | bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title); |
| 460 | const std::string& comment, const std::string& icon_path, | 468 | bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, |
| 461 | const std::string& command, const std::string& arguments, | 469 | std::filesystem::path& out_icon_path); |
| 462 | const std::string& categories, const std::string& keywords); | 470 | bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, |
| 463 | 471 | const std::filesystem::path& icon_path, | |
| 472 | const std::filesystem::path& command, const std::string& arguments, | ||
| 473 | const std::string& categories, const std::string& keywords, | ||
| 474 | const std::string& name); | ||
| 464 | /** | 475 | /** |
| 465 | * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog | 476 | * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog |
| 466 | * The only difference is that it returns a boolean. | 477 | * The only difference is that it returns a boolean. |
diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index f2854c8ec..7b2a47496 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp | |||
| @@ -42,7 +42,7 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { | |||
| 42 | return circle_pixmap; | 42 | return circle_pixmap; |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | bool SaveIconToFile(const std::string_view path, const QImage& image) { | 45 | bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) { |
| 46 | #if defined(WIN32) | 46 | #if defined(WIN32) |
| 47 | #pragma pack(push, 2) | 47 | #pragma pack(push, 2) |
| 48 | struct IconDir { | 48 | struct IconDir { |
| @@ -73,7 +73,7 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { | |||
| 73 | .id_count = static_cast<WORD>(scale_sizes.size()), | 73 | .id_count = static_cast<WORD>(scale_sizes.size()), |
| 74 | }; | 74 | }; |
| 75 | 75 | ||
| 76 | Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, | 76 | Common::FS::IOFile icon_file(icon_path.string(), Common::FS::FileAccessMode::Write, |
| 77 | Common::FS::FileType::BinaryFile); | 77 | Common::FS::FileType::BinaryFile); |
| 78 | if (!icon_file.IsOpen()) { | 78 | if (!icon_file.IsOpen()) { |
| 79 | return false; | 79 | return false; |
| @@ -135,6 +135,14 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { | |||
| 135 | icon_file.Close(); | 135 | icon_file.Close(); |
| 136 | 136 | ||
| 137 | return true; | 137 | return true; |
| 138 | #elif defined(__linux__) || defined(__FreeBSD__) | ||
| 139 | // Convert and write the icon as a PNG | ||
| 140 | if (!image.save(QString::fromStdString(icon_path.string()))) { | ||
| 141 | LOG_ERROR(Frontend, "Could not write icon as PNG to file"); | ||
| 142 | } else { | ||
| 143 | LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); | ||
| 144 | } | ||
| 145 | return true; | ||
| 138 | #else | 146 | #else |
| 139 | return false; | 147 | return false; |
| 140 | #endif | 148 | #endif |
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 09c14ce3f..4094cf6c2 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | #pragma once | 4 | #pragma once |
| 5 | 5 | ||
| 6 | #include <filesystem> | ||
| 6 | #include <QFont> | 7 | #include <QFont> |
| 7 | #include <QString> | 8 | #include <QString> |
| 8 | 9 | ||
| @@ -25,4 +26,4 @@ | |||
| 25 | * @param image The image to save | 26 | * @param image The image to save |
| 26 | * @return bool If the operation succeeded | 27 | * @return bool If the operation succeeded |
| 27 | */ | 28 | */ |
| 28 | [[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); | 29 | [[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); |