summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar boludoz2023-10-15 02:02:22 -0300
committerGravatar boludoz2023-10-15 02:02:22 -0300
commit3062a35eb1297067446156c43e9d0df2f684edff (patch)
treef2ca58e0b8a6c413f3c6783a1501204729e0394c /src
parentMerge pull request #11780 from Darkness4/master (diff)
downloadyuzu-3062a35eb1297067446156c43e9d0df2f684edff.tar.gz
yuzu-3062a35eb1297067446156c43e9d0df2f684edff.tar.xz
yuzu-3062a35eb1297067446156c43e9d0df2f684edff.zip
Improved shortcut: add games in applist for Windows, question for start game at fullscreen & better unicode support for some Windows path funcs.
Diffstat (limited to 'src')
-rw-r--r--src/common/fs/fs_util.cpp59
-rw-r--r--src/common/fs/fs_util.h22
-rw-r--r--src/common/fs/path_util.cpp87
-rw-r--r--src/common/fs/path_util.h15
-rw-r--r--src/yuzu/game_list.cpp4
-rw-r--r--src/yuzu/main.cpp490
-rw-r--r--src/yuzu/main.h22
-rw-r--r--src/yuzu/util/util.cpp17
-rw-r--r--src/yuzu/util/util.h14
9 files changed, 515 insertions, 215 deletions
diff --git a/src/common/fs/fs_util.cpp b/src/common/fs/fs_util.cpp
index 813a713c3..442f63728 100644
--- a/src/common/fs/fs_util.cpp
+++ b/src/common/fs/fs_util.cpp
@@ -36,4 +36,63 @@ std::string PathToUTF8String(const std::filesystem::path& path) {
36 return ToUTF8String(path.u8string()); 36 return ToUTF8String(path.u8string());
37} 37}
38 38
39std::u8string U8FilenameSantizer(const std::u8string_view u8filename) {
40 std::u8string u8path_santized{u8filename.begin(), u8filename.end()};
41 size_t eSizeSanitized = u8path_santized.size();
42
43 // Special case for ":", for example: 'Pepe: La secuela' --> 'Pepe - La
44 // secuela' or 'Pepe : La secuela' --> 'Pepe - La secuela'
45 for (size_t i = 0; i < eSizeSanitized; i++) {
46 switch (u8path_santized[i]) {
47 case u8':':
48 if (i == 0 || i == eSizeSanitized - 1) {
49 u8path_santized.replace(i, 1, u8"_");
50 } else if (u8path_santized[i - 1] == u8' ') {
51 u8path_santized.replace(i, 1, u8"-");
52 } else {
53 u8path_santized.replace(i, 1, u8" -");
54 eSizeSanitized++;
55 }
56 break;
57 case u8'\\':
58 case u8'/':
59 case u8'*':
60 case u8'?':
61 case u8'\"':
62 case u8'<':
63 case u8'>':
64 case u8'|':
65 case u8'\0':
66 u8path_santized.replace(i, 1, u8"_");
67 break;
68 default:
69 break;
70 }
71 }
72
73 // Delete duplicated spaces || Delete duplicated dots (MacOS i think)
74 for (size_t i = 0; i < eSizeSanitized - 1; i++) {
75 if ((u8path_santized[i] == u8' ' && u8path_santized[i + 1] == u8' ') ||
76 (u8path_santized[i] == u8'.' && u8path_santized[i + 1] == u8'.')) {
77 u8path_santized.erase(i, 1);
78 i--;
79 }
80 }
81
82 // Delete all spaces and dots at the end (Windows almost)
83 while (u8path_santized.back() == u8' ' || u8path_santized.back() == u8'.') {
84 u8path_santized.pop_back();
85 }
86
87 if (u8path_santized.empty()) {
88 return u8"";
89 }
90
91 return u8path_santized;
92}
93
94std::string UTF8FilenameSantizer(const std::string_view filename) {
95 return ToUTF8String(U8FilenameSantizer(ToU8String(filename)));
96}
97
39} // namespace Common::FS 98} // namespace Common::FS
diff --git a/src/common/fs/fs_util.h b/src/common/fs/fs_util.h
index 2492a9f94..dbb4f5a9a 100644
--- a/src/common/fs/fs_util.h
+++ b/src/common/fs/fs_util.h
@@ -82,4 +82,24 @@ concept IsChar = std::same_as<T, char>;
82 */ 82 */
83[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path); 83[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path);
84 84
85} // namespace Common::FS 85/**
86 * Fix filename (remove invalid characters)
87 *
88 * @param u8_string dirty encoded filename string
89 *
90 * @returns utf8_string santized filename string
91 *
92 */
93[[nodiscard]] std::u8string U8FilenameSantizer(const std::u8string_view u8filename);
94
95/**
96 * Fix filename (remove invalid characters)
97 *
98 * @param utf8_string dirty encoded filename string
99 *
100 * @returns utf8_string santized filename string
101 *
102 */
103[[nodiscard]] std::string UTF8FilenameSantizer(const std::string_view filename);
104
105} // namespace Common::FS \ No newline at end of file
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index 0abd81a45..a461161ed 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -6,6 +6,7 @@
6#include <unordered_map> 6#include <unordered_map>
7 7
8#include "common/fs/fs.h" 8#include "common/fs/fs.h"
9#include "common/string_util.h"
9#ifdef ANDROID 10#ifdef ANDROID
10#include "common/fs/fs_android.h" 11#include "common/fs/fs_android.h"
11#endif 12#endif
@@ -14,7 +15,7 @@
14#include "common/logging/log.h" 15#include "common/logging/log.h"
15 16
16#ifdef _WIN32 17#ifdef _WIN32
17#include <shlobj.h> // Used in GetExeDirectory() 18#include <shlobj.h> // Used in GetExeDirectory() and GetWindowsDesktop()
18#else 19#else
19#include <cstdlib> // Used in Get(Home/Data)Directory() 20#include <cstdlib> // Used in Get(Home/Data)Directory()
20#include <pwd.h> // Used in GetHomeDirectory() 21#include <pwd.h> // Used in GetHomeDirectory()
@@ -250,30 +251,39 @@ void SetYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
250#ifdef _WIN32 251#ifdef _WIN32
251 252
252fs::path GetExeDirectory() { 253fs::path GetExeDirectory() {
253 wchar_t exe_path[MAX_PATH]; 254 WCHAR exe_path[MAX_PATH];
254 255
255 if (GetModuleFileNameW(nullptr, exe_path, MAX_PATH) == 0) { 256 if (SUCCEEDED(GetModuleFileNameW(nullptr, exe_path, MAX_PATH))) {
257 std::wstring wideExePath(exe_path);
258
259 // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with
260 // the Windows library (Filesystem converts the strings literally).
261 return fs::path{Common::UTF16ToUTF8(wideExePath)}.parent_path();
262 } else {
256 LOG_ERROR(Common_Filesystem, 263 LOG_ERROR(Common_Filesystem,
257 "Failed to get the path to the executable of the current process"); 264 "[GetExeDirectory] Failed to get the path to the executable of the current "
265 "process");
258 } 266 }
259 267
260 return fs::path{exe_path}.parent_path(); 268 return fs::path{};
261} 269}
262 270
263fs::path GetAppDataRoamingDirectory() { 271fs::path GetAppDataRoamingDirectory() {
264 PWSTR appdata_roaming_path = nullptr; 272 PWSTR appdata_roaming_path = nullptr;
265 273
266 SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_roaming_path); 274 if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &appdata_roaming_path))) {
267 275 std::wstring wideAppdataRoamingPath(appdata_roaming_path);
268 auto fs_appdata_roaming_path = fs::path{appdata_roaming_path}; 276 CoTaskMemFree(appdata_roaming_path);
269
270 CoTaskMemFree(appdata_roaming_path);
271 277
272 if (fs_appdata_roaming_path.empty()) { 278 // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with
273 LOG_ERROR(Common_Filesystem, "Failed to get the path to the %APPDATA% directory"); 279 // the Windows library (Filesystem converts the strings literally).
280 return fs::path{Common::UTF16ToUTF8(wideAppdataRoamingPath)};
281 } else {
282 LOG_ERROR(Common_Filesystem,
283 "[GetAppDataRoamingDirectory] Failed to get the path to the %APPDATA% directory");
274 } 284 }
275 285
276 return fs_appdata_roaming_path; 286 return fs::path{};
277} 287}
278 288
279#else 289#else
@@ -338,6 +348,57 @@ fs::path GetBundleDirectory() {
338 348
339#endif 349#endif
340 350
351fs::path GetDesktopPath() {
352#if defined(_WIN32)
353 PWSTR DesktopPath = nullptr;
354
355 if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &DesktopPath))) {
356 std::wstring wideDesktopPath(DesktopPath);
357 CoTaskMemFree(DesktopPath);
358
359 // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with
360 // the Windows library (Filesystem converts the strings literally).
361 return fs::path{Common::UTF16ToUTF8(wideDesktopPath)};
362 } else {
363 LOG_ERROR(Common_Filesystem,
364 "[GetDesktopPath] Failed to get the path to the desktop directory");
365 }
366#else
367 fs::path shortcut_path = GetHomeDirectory() / "Desktop";
368 if (fs::exists(shortcut_path)) {
369 return shortcut_path;
370 }
371#endif
372 return fs::path{};
373}
374
375fs::path GetAppsShortcutsPath() {
376#if defined(_WIN32)
377 PWSTR AppShortcutsPath = nullptr;
378
379 if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &AppShortcutsPath))) {
380 std::wstring wideAppShortcutsPath(AppShortcutsPath);
381 CoTaskMemFree(AppShortcutsPath);
382
383 // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with
384 // the Windows library (Filesystem converts the strings literally).
385 return fs::path{Common::UTF16ToUTF8(wideAppShortcutsPath)};
386 } else {
387 LOG_ERROR(Common_Filesystem,
388 "[GetAppsShortcutsPath] Failed to get the path to the App Shortcuts directory");
389 }
390#else
391 fs::path shortcut_path = GetHomeDirectory() / ".local/share/applications";
392 if (!fs::exists(shortcut_path)) {
393 shortcut_path = std::filesystem::path("/usr/share/applications");
394 return shortcut_path;
395 } else {
396 return shortcut_path;
397 }
398#endif
399 return fs::path{};
400}
401
341// vvvvvvvvvv Deprecated vvvvvvvvvv // 402// vvvvvvvvvv Deprecated vvvvvvvvvv //
342 403
343std::string_view RemoveTrailingSlash(std::string_view path) { 404std::string_view RemoveTrailingSlash(std::string_view path) {
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index 63801c924..b88a388d1 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -244,7 +244,6 @@ void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {
244 * @returns The path of the current user's %APPDATA% directory. 244 * @returns The path of the current user's %APPDATA% directory.
245 */ 245 */
246[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory(); 246[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory();
247
248#else 247#else
249 248
250/** 249/**
@@ -275,6 +274,20 @@ void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {
275 274
276#endif 275#endif
277 276
277/**
278 * Gets the path of the current user's desktop directory.
279 *
280 * @returns The path of the current user's desktop directory.
281 */
282[[nodiscard]] std::filesystem::path GetDesktopPath();
283
284/**
285 * Gets the path of the current user's apps directory.
286 *
287 * @returns The path of the current user's apps directory.
288 */
289[[nodiscard]] std::filesystem::path GetAppsShortcutsPath();
290
278// vvvvvvvvvv Deprecated vvvvvvvvvv // 291// vvvvvvvvvv Deprecated vvvvvvvvvv //
279 292
280// Removes the final '/' or '\' if one exists 293// Removes the final '/' or '\' if one exists
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index 2bb1a0239..fbe099661 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -566,10 +566,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
566 QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); 566 QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
567 QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); 567 QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
568 QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); 568 QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
569#ifndef WIN32
570 QAction* create_applications_menu_shortcut = 569 QAction* create_applications_menu_shortcut =
571 shortcut_menu->addAction(tr("Add to Applications Menu")); 570 shortcut_menu->addAction(tr("Add to Applications Menu"));
572#endif
573 context_menu.addSeparator(); 571 context_menu.addSeparator();
574 QAction* properties = context_menu.addAction(tr("Properties")); 572 QAction* properties = context_menu.addAction(tr("Properties"));
575 573
@@ -647,11 +645,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
647 connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { 645 connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {
648 emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); 646 emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);
649 }); 647 });
650#ifndef WIN32
651 connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { 648 connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {
652 emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); 649 emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);
653 }); 650 });
654#endif
655 connect(properties, &QAction::triggered, 651 connect(properties, &QAction::triggered,
656 [this, path]() { emit OpenPerGameGeneralRequested(path); }); 652 [this, path]() { emit OpenPerGameGeneralRequested(path); });
657}; 653};
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 1431cf2fe..e4dc717ed 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -2840,170 +2840,350 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
2840 QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); 2840 QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));
2841} 2841}
2842 2842
2843void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, 2843bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path,
2844 GameListShortcutTarget target) { 2844 const std::string& comment,
2845 // Get path to yuzu executable 2845 const std::filesystem::path& icon_path,
2846 const QStringList args = QApplication::arguments(); 2846 const std::filesystem::path& command,
2847 std::filesystem::path yuzu_command = args[0].toStdString(); 2847 const std::string& arguments, const std::string& categories,
2848 const std::string& keywords, const std::string& name) {
2848 2849
2849 // If relative path, make it an absolute path 2850 bool shortcut_succeeded = false;
2850 if (yuzu_command.c_str()[0] == '.') {
2851 yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
2852 }
2853 2851
2854#if defined(__linux__) 2852 // Replace characters that are illegal in Windows filenames
2855 // Warn once if we are making a shortcut to a volatile AppImage 2853 std::filesystem::path shortcut_path_full =
2856 const std::string appimage_ending = 2854 shortcut_path / Common::FS::UTF8FilenameSantizer(name);
2857 std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); 2855
2858 if (yuzu_command.string().ends_with(appimage_ending) && 2856#if defined(__linux__) || defined(__FreeBSD__)
2859 !UISettings::values.shortcut_already_warned) { 2857 shortcut_path_full += ".desktop";
2860 if (QMessageBox::warning(this, tr("Create Shortcut"), 2858#elif defined(_WIN32)
2861 tr("This will create a shortcut to the current AppImage. This may " 2859 shortcut_path_full += ".lnk";
2862 "not work well if you update. Continue?"), 2860#endif
2863 QMessageBox::StandardButton::Ok | 2861
2864 QMessageBox::StandardButton::Cancel) == 2862 LOG_INFO(Common, "[GMainWindow::CreateShortcutLink] Create shortcut path: {}",
2865 QMessageBox::StandardButton::Cancel) { 2863 shortcut_path_full.string());
2866 return; 2864
2865#if defined(__linux__) || defined(__FreeBSD__)
2866 // This desktop file template was writing referencing
2867 // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html
2868 try {
2869
2870 // Plus 'Type' is required
2871 if (name.empty()) {
2872 LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Name is empty");
2873 shortcut_succeeded = false;
2874 return shortcut_succeeded;
2867 } 2875 }
2868 UISettings::values.shortcut_already_warned = true; 2876 std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc);
2869 }
2870#endif // __linux__
2871 2877
2872 std::filesystem::path target_directory{}; 2878 if (shortcut_stream.is_open()) {
2873 2879
2874 switch (target) { 2880 fmt::print(shortcut_stream, "[Desktop Entry]\n");
2875 case GameListShortcutTarget::Desktop: { 2881 fmt::print(shortcut_stream, "Type=Application\n");
2876 const QString desktop_path = 2882 fmt::print(shortcut_stream, "Version=1.0\n");
2877 QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); 2883 fmt::print(shortcut_stream, "Name={}\n", name);
2878 target_directory = desktop_path.toUtf8().toStdString(); 2884
2879 break; 2885 if (!comment.empty()) {
2880 } 2886 fmt::print(shortcut_stream, "Comment={}\n", comment);
2881 case GameListShortcutTarget::Applications: {
2882 const QString applications_path =
2883 QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
2884 if (applications_path.isEmpty()) {
2885 const char* home = std::getenv("HOME");
2886 if (home != nullptr) {
2887 target_directory = std::filesystem::path(home) / ".local/share/applications";
2888 } 2887 }
2888
2889 if (std::filesystem::is_regular_file(icon_path)) {
2890 fmt::print(shortcut_stream, "Icon={}\n", icon_path.string());
2891 }
2892
2893 fmt::print(shortcut_stream, "TryExec={}\n", command.string());
2894 fmt::print(shortcut_stream, "Exec={}", command.string());
2895
2896 if (!arguments.empty()) {
2897 fmt::print(shortcut_stream, " {}", arguments);
2898 }
2899
2900 fmt::print(shortcut_stream, "\n");
2901
2902 if (!categories.empty()) {
2903 fmt::print(shortcut_stream, "Categories={}\n", categories);
2904 }
2905
2906 if (!keywords.empty()) {
2907 fmt::print(shortcut_stream, "Keywords={}\n", keywords);
2908 }
2909
2910 shortcut_stream.close();
2911 return true;
2912
2889 } else { 2913 } else {
2890 target_directory = applications_path.toUtf8().toStdString(); 2914 LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create shortcut");
2915 return false;
2891 } 2916 }
2892 break; 2917
2918 shortcut_stream.close();
2919 } catch (const std::exception& e) {
2920 LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create shortcut: {}",
2921 e.what());
2893 } 2922 }
2894 default: 2923#elif defined(_WIN32)
2895 return; 2924 // Initialize COM
2925 auto hr = CoInitialize(NULL);
2926 if (FAILED(hr)) {
2927 return shortcut_succeeded;
2896 } 2928 }
2897 2929
2898 const QDir dir(QString::fromStdString(target_directory.generic_string())); 2930 IShellLinkW* ps1;
2899 if (!dir.exists()) { 2931
2900 QMessageBox::critical(this, tr("Create Shortcut"), 2932 auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
2901 tr("Cannot create shortcut. Path \"%1\" does not exist.") 2933 (void**)&ps1);
2902 .arg(QString::fromStdString(target_directory.generic_string())), 2934
2903 QMessageBox::StandardButton::Ok); 2935 // The UTF-16 / UTF-8 conversion is broken in C++, it is necessary to perform these steps and
2904 return; 2936 // resort to the native Windows function.
2937 std::wstring wshortcut_path_full = Common::UTF8ToUTF16W(shortcut_path_full.string());
2938 std::wstring wicon_path = Common::UTF8ToUTF16W(icon_path.string());
2939 std::wstring wcommand = Common::UTF8ToUTF16W(command.string());
2940 std::wstring warguments = Common::UTF8ToUTF16W(arguments);
2941 std::wstring wcomment = Common::UTF8ToUTF16W(comment);
2942
2943 if (SUCCEEDED(hres)) {
2944 if (std::filesystem::is_regular_file(command))
2945 hres = ps1->SetPath(wcommand.data());
2946
2947 if (SUCCEEDED(hres) && !arguments.empty())
2948 hres = ps1->SetArguments(warguments.data());
2949
2950 if (SUCCEEDED(hres) && !comment.empty())
2951 hres = ps1->SetDescription(wcomment.data());
2952
2953 if (SUCCEEDED(hres) && std::filesystem::is_regular_file(icon_path))
2954 hres = ps1->SetIconLocation(wicon_path.data(), 0);
2955
2956 IPersistFile* pPersistFile = nullptr;
2957
2958 if (SUCCEEDED(hres)) {
2959 hres = ps1->QueryInterface(IID_IPersistFile, (void**)&pPersistFile);
2960
2961 if (SUCCEEDED(hres) && pPersistFile != nullptr) {
2962 hres = pPersistFile->Save(wshortcut_path_full.data(), TRUE);
2963 if (SUCCEEDED(hres)) {
2964 shortcut_succeeded = true;
2965 }
2966 }
2967 }
2968
2969 if (pPersistFile != nullptr) {
2970 pPersistFile->Release();
2971 }
2972 } else {
2973 LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create IShellLinkWinstance");
2905 } 2974 }
2906 2975
2907 const std::string game_file_name = std::filesystem::path(game_path).filename().string(); 2976 ps1->Release();
2908 // Determine full paths for icon and shortcut 2977 CoUninitialize();
2909#if defined(__linux__) || defined(__FreeBSD__) 2978#endif
2910 const char* home = std::getenv("HOME"); 2979
2911 const std::filesystem::path home_path = (home == nullptr ? "~" : home); 2980 if (shortcut_succeeded && std::filesystem::is_regular_file(shortcut_path_full)) {
2912 const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); 2981 LOG_INFO(Common, "[GMainWindow::CreateShortcutLink] Shortcut created");
2913 2982 } else {
2914 std::filesystem::path system_icons_path = 2983 LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Shortcut error, failed to create it");
2915 (xdg_data_home == nullptr ? home_path / ".local/share/" 2984 shortcut_succeeded = false;
2916 : std::filesystem::path(xdg_data_home)) / 2985 }
2917 "icons/hicolor/256x256"; 2986
2918 if (!Common::FS::CreateDirs(system_icons_path)) { 2987 return shortcut_succeeded;
2988}
2989
2990// Messages in pre-defined message boxes for less code spaghetti
2991bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, const int& imsg,
2992 const std::string title) {
2993 QMessageBox::StandardButtons buttons;
2994 int result = 0;
2995
2996 switch (imsg) {
2997
2998 case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES:
2999 buttons = QMessageBox::Yes | QMessageBox::No;
3000
3001 result =
3002 QMessageBox::information(parent, tr("Create Shortcut"),
3003 tr("Do you want to launch the game in fullscreen?"), buttons);
3004
3005 LOG_INFO(Frontend, "Shortcut will launch in fullscreen");
3006 return (result == QMessageBox::No) ? false : true;
3007
3008 case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS:
3009 QMessageBox::information(
3010 parent, tr("Create Shortcut"),
3011 tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title)));
3012 LOG_INFO(Frontend, "Successfully created a shortcut to {}", title);
3013 return true;
3014
3015 case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING:
3016 result = QMessageBox::warning(
3017 this, tr("Create Shortcut"),
3018 tr("This will create a shortcut to the current AppImage. This may "
3019 "not work well if you update. Continue?"),
3020 QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel);
3021 return (result == QMessageBox::StandardButton::Cancel) ? true : false;
3022 case GMainWindow::CREATE_SHORTCUT_MSGBOX_ADMIN:
3023 buttons = QMessageBox::Ok;
3024 QMessageBox::critical(parent, tr("Create Shortcut"),
3025 tr("Cannot create shortcut in Apps. Restart yuzu as administrator."),
3026 buttons);
3027 LOG_ERROR(Frontend, "Cannot create shortcut in Apps. Restart yuzu as administrator.");
3028 return true;
3029 default:
3030 buttons = QMessageBox::Ok;
3031 QMessageBox::critical(
3032 parent, tr("Create Shortcut"),
3033 tr("Failed to create a shortcut to %1").arg(QString::fromStdString(title)), buttons);
3034 LOG_ERROR(Frontend, "Failed to create a shortcut to {}", title);
3035 return true;
3036 }
3037
3038 return true;
3039}
3040
3041bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name,
3042 std::filesystem::path& icons_path) {
3043
3044 // Get path to Yuzu icons directory & icon extension
3045 std::string ico_extension = "png";
3046#if defined(_WIN32)
3047 icons_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "icons";
3048 ico_extension = "ico";
3049#elif defined(__linux__) || defined(__FreeBSD__)
3050 icons_path = GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256";
3051#endif
3052
3053 // Create icons directory if it doesn't exist
3054 if (!Common::FS::CreateDirs(icons_path)) {
2919 QMessageBox::critical( 3055 QMessageBox::critical(
2920 this, tr("Create Icon"), 3056 this, tr("Create Icon"),
2921 tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") 3057 tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.")
2922 .arg(QString::fromStdString(system_icons_path)), 3058 .arg(QString::fromStdString(icons_path.string())),
2923 QMessageBox::StandardButton::Ok); 3059 QMessageBox::StandardButton::Ok);
2924 return; 3060 icons_path = ""; // Reset path
3061 return false;
2925 } 3062 }
2926 std::filesystem::path icon_path =
2927 system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name)
2928 : fmt::format("yuzu-{:016X}.png", program_id));
2929 const std::filesystem::path shortcut_path =
2930 target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name)
2931 : fmt::format("yuzu-{:016X}.desktop", program_id));
2932#elif defined(WIN32)
2933 std::filesystem::path icons_path =
2934 Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir);
2935 std::filesystem::path icon_path =
2936 icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name)
2937 : fmt::format("yuzu-{:016X}.ico", program_id)));
2938#else
2939 std::string icon_extension;
2940#endif
2941 3063
2942 // Get title from game file 3064 // Create icon file path
2943 const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), 3065 icons_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension)
2944 system->GetContentProvider()}; 3066 : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension));
2945 const auto control = pm.GetControlMetadata(); 3067 return true;
2946 const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); 3068}
2947 3069
2948 std::string title{fmt::format("{:016X}", program_id)}; 3070void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
3071 GameListShortcutTarget target) {
2949 3072
2950 if (control.first != nullptr) { 3073 // Get path to yuzu executable
2951 title = control.first->GetApplicationName(); 3074 const QStringList args = QApplication::arguments();
2952 } else { 3075 std::filesystem::path yuzu_command = args[0].toStdString();
2953 loader->ReadTitle(title); 3076
3077 // If relative path, make it an absolute path
3078 if (yuzu_command.c_str()[0] == '.') {
3079 yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
2954 } 3080 }
2955 3081
2956 // Get icon from game file 3082 // Shortcut path
2957 std::vector<u8> icon_image_file{}; 3083 std::filesystem::path shortcut_path{};
2958 if (control.second != nullptr) { 3084 if (target == GameListShortcutTarget::Desktop) {
2959 icon_image_file = control.second->ReadAllBytes(); 3085 shortcut_path = Common::FS::GetDesktopPath();
2960 } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { 3086 if (!std::filesystem::exists(shortcut_path)) {
2961 LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); 3087 shortcut_path =
3088 QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString();
3089 }
3090 } else if (target == GameListShortcutTarget::Applications) {
3091
3092#if defined(_WIN32)
3093 HANDLE hProcess = GetCurrentProcess();
3094 if (!IsUserAnAdmin()) {
3095 GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ADMIN,
3096 "");
3097 return;
3098 }
3099 CloseHandle(hProcess);
3100#endif // _WIN32
3101
3102 shortcut_path = Common::FS::GetAppsShortcutsPath();
3103 if (!std::filesystem::exists(shortcut_path)) {
3104 shortcut_path = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)
3105 .toStdString();
3106 }
2962 } 3107 }
2963 3108
2964 QImage icon_data = 3109 // Icon path and title
2965 QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); 3110 std::string title;
2966#if defined(__linux__) || defined(__FreeBSD__) 3111 std::filesystem::path icons_path;
2967 // Convert and write the icon as a PNG 3112 if (std::filesystem::exists(shortcut_path)) {
2968 if (!icon_data.save(QString::fromStdString(icon_path.string()))) { 3113
2969 LOG_ERROR(Frontend, "Could not write icon as PNG to file"); 3114 // Get title from game file
3115 const FileSys::PatchManager pm{program_id, system->GetFileSystemController(),
3116 system->GetContentProvider()};
3117 const auto control = pm.GetControlMetadata();
3118 const auto loader =
3119 Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read));
3120
3121 title = fmt::format("{:016X}", program_id);
3122
3123 if (control.first != nullptr) {
3124 title = control.first->GetApplicationName();
3125 } else {
3126 loader->ReadTitle(title);
3127 }
3128
3129 // Get icon from game file
3130 std::vector<u8> icon_image_file{};
3131 if (control.second != nullptr) {
3132 icon_image_file = control.second->ReadAllBytes();
3133 } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) {
3134 LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
3135 }
3136
3137 QImage icon_data =
3138 QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
3139
3140 if (GMainWindow::MakeShortcutIcoPath(program_id, title, icons_path)) {
3141 if (!SaveIconToFile(icon_data, icons_path)) {
3142 LOG_ERROR(Frontend, "Could not write icon to file");
3143 }
3144 }
3145
2970 } else { 3146 } else {
2971 LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); 3147 GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR,
2972 } 3148 title);
2973#elif defined(WIN32) 3149 LOG_ERROR(Frontend, "[GMainWindow::OnGameListCreateShortcut] Invalid shortcut target");
2974 if (!SaveIconToFile(icon_path.string(), icon_data)) {
2975 LOG_ERROR(Frontend, "Could not write icon to file");
2976 return; 3150 return;
2977 } 3151 }
2978#endif // __linux__
2979 3152
2980#ifdef _WIN32 3153// Special case for AppImages
2981 // Replace characters that are illegal in Windows filenames by a dash 3154#if defined(__linux__)
2982 const std::string illegal_chars = "<>:\"/\\|?*"; 3155 // Warn once if we are making a shortcut to a volatile AppImage
2983 for (char c : illegal_chars) { 3156 const std::string appimage_ending =
2984 std::replace(title.begin(), title.end(), c, '_'); 3157 std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage");
3158 if (yuzu_command.string().ends_with(appimage_ending) &&
3159 !UISettings::values.shortcut_already_warned) {
3160 if (GMainWindow::CreateShortcutMessagesGUI(
3161 this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, title)) {
3162 return;
3163 }
3164 UISettings::values.shortcut_already_warned = true;
2985 } 3165 }
2986 const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); 3166#endif // __linux__
2987#endif
2988 3167
3168 // Create shortcut
3169 std::string arguments = fmt::format("-g \"{:s}\"", game_path);
3170 if (GMainWindow::CreateShortcutMessagesGUI(
3171 this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, title)) {
3172 arguments = "-f " + arguments;
3173 }
2989 const std::string comment = 3174 const std::string comment =
2990 tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); 3175 tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString();
2991 const std::string arguments = fmt::format("-g \"{:s}\"", game_path);
2992 const std::string categories = "Game;Emulator;Qt;"; 3176 const std::string categories = "Game;Emulator;Qt;";
2993 const std::string keywords = "Switch;Nintendo;"; 3177 const std::string keywords = "Switch;Nintendo;";
2994 3178
2995 if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), 3179 if (GMainWindow::CreateShortcutLink(shortcut_path, comment, icons_path, yuzu_command, arguments,
2996 yuzu_command.string(), arguments, categories, keywords)) { 3180 categories, keywords, title)) {
2997 QMessageBox::critical(this, tr("Create Shortcut"), 3181 GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS,
2998 tr("Failed to create a shortcut at %1") 3182 title);
2999 .arg(QString::fromStdString(shortcut_path.string())));
3000 return; 3183 return;
3001 } 3184 }
3002 3185
3003 LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); 3186 GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, title);
3004 QMessageBox::information(
3005 this, tr("Create Shortcut"),
3006 tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title)));
3007} 3187}
3008 3188
3009void GMainWindow::OnGameListOpenDirectory(const QString& directory) { 3189void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
@@ -3998,66 +4178,6 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file
3998 } 4178 }
3999} 4179}
4000 4180
4001bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title,
4002 const std::string& comment, const std::string& icon_path,
4003 const std::string& command, const std::string& arguments,
4004 const std::string& categories, const std::string& keywords) {
4005#if defined(__linux__) || defined(__FreeBSD__)
4006 // This desktop file template was writing referencing
4007 // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html
4008 std::string shortcut_contents{};
4009 shortcut_contents.append("[Desktop Entry]\n");
4010 shortcut_contents.append("Type=Application\n");
4011 shortcut_contents.append("Version=1.0\n");
4012 shortcut_contents.append(fmt::format("Name={:s}\n", title));
4013 shortcut_contents.append(fmt::format("Comment={:s}\n", comment));
4014 shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path));
4015 shortcut_contents.append(fmt::format("TryExec={:s}\n", command));
4016 shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments));
4017 shortcut_contents.append(fmt::format("Categories={:s}\n", categories));
4018 shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords));
4019
4020 std::ofstream shortcut_stream(shortcut_path);
4021 if (!shortcut_stream.is_open()) {
4022 LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path);
4023 return false;
4024 }
4025 shortcut_stream << shortcut_contents;
4026 shortcut_stream.close();
4027
4028 return true;
4029#elif defined(WIN32)
4030 IShellLinkW* shell_link;
4031 auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
4032 (void**)&shell_link);
4033 if (FAILED(hres)) {
4034 return false;
4035 }
4036 shell_link->SetPath(
4037 Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to
4038 shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data());
4039 shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data());
4040 shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0);
4041
4042 IPersistFile* persist_file;
4043 hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file);
4044 if (FAILED(hres)) {
4045 return false;
4046 }
4047
4048 hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE);
4049 if (FAILED(hres)) {
4050 return false;
4051 }
4052
4053 persist_file->Release();
4054 shell_link->Release();
4055
4056 return true;
4057#endif
4058 return false;
4059}
4060
4061void GMainWindow::OnLoadAmiibo() { 4181void GMainWindow::OnLoadAmiibo() {
4062 if (emu_thread == nullptr || !emu_thread->IsRunning()) { 4182 if (emu_thread == nullptr || !emu_thread->IsRunning()) {
4063 return; 4183 return;
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 270a40c5f..bf6756b48 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 <QTimer> 12#include <QTimer>
@@ -151,6 +152,14 @@ class GMainWindow : public QMainWindow {
151 UI_EMU_STOPPING, 152 UI_EMU_STOPPING,
152 }; 153 };
153 154
155 const enum {
156 CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES,
157 CREATE_SHORTCUT_MSGBOX_SUCCESS,
158 CREATE_SHORTCUT_MSGBOX_ERROR,
159 CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING,
160 CREATE_SHORTCUT_MSGBOX_ADMIN,
161 };
162
154public: 163public:
155 void filterBarSetChecked(bool state); 164 void filterBarSetChecked(bool state);
156 void UpdateUITheme(); 165 void UpdateUITheme();
@@ -433,11 +442,14 @@ private:
433 bool ConfirmShutdownGame(); 442 bool ConfirmShutdownGame();
434 443
435 QString GetTasStateDescription() const; 444 QString GetTasStateDescription() const;
436 bool CreateShortcut(const std::string& shortcut_path, const std::string& title, 445 bool CreateShortcutMessagesGUI(QWidget* parent, const int& imsg, const std::string title);
437 const std::string& comment, const std::string& icon_path, 446 bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name,
438 const std::string& command, const std::string& arguments, 447 std::filesystem::path& icons_path);
439 const std::string& categories, const std::string& keywords); 448 bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment,
440 449 const std::filesystem::path& icon_path,
450 const std::filesystem::path& command, const std::string& arguments,
451 const std::string& categories, const std::string& keywords,
452 const std::string& name);
441 /** 453 /**
442 * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog 454 * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog
443 * The only difference is that it returns a boolean. 455 * 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..4d199ebd1 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
45bool SaveIconToFile(const std::string_view path, const QImage& image) { 45bool SaveIconToFile(const QImage& image, const std::filesystem::path& icon_path) {
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,7 +135,16 @@ 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#else 138#elif defined(__linux__) || defined(__FreeBSD__)
139 return false; 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
146 return true;
140#endif 147#endif
148
149 return false;
141} 150}
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h
index 09c14ce3f..8839e160a 100644
--- a/src/yuzu/util/util.h
+++ b/src/yuzu/util/util.h
@@ -3,26 +3,36 @@
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
9/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. 10/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc.
11
10[[nodiscard]] QFont GetMonospaceFont(); 12[[nodiscard]] QFont GetMonospaceFont();
11 13
12/// Convert a size in bytes into a readable format (KiB, MiB, etc.) 14/// Convert a size in bytes into a readable format (KiB, MiB, etc.)
15
13[[nodiscard]] QString ReadableByteSize(qulonglong size); 16[[nodiscard]] QString ReadableByteSize(qulonglong size);
14 17
15/** 18/**
16 * Creates a circle pixmap from a specified color 19 * Creates a circle pixmap from a specified color
20 *
17 * @param color The color the pixmap shall have 21 * @param color The color the pixmap shall have
22 *
18 * @return QPixmap circle pixmap 23 * @return QPixmap circle pixmap
19 */ 24 */
25
20[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color); 26[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);
21 27
22/** 28/**
23 * Saves a windows icon to a file 29 * Saves a windows icon to a file
24 * @param path The icons path 30 *
25 * @param image The image to save 31 * @param image The image to save
32 *
33 * @param path The icons path
34 *
26 * @return bool If the operation succeeded 35 * @return bool If the operation succeeded
27 */ 36 */
28[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); 37
38[[nodiscard]] bool SaveIconToFile(const QImage& image, const std::filesystem::path& icon_path);