diff options
| -rw-r--r-- | src/common/fs/fs_util.cpp | 59 | ||||
| -rw-r--r-- | src/common/fs/fs_util.h | 22 | ||||
| -rw-r--r-- | src/common/fs/path_util.cpp | 87 | ||||
| -rw-r--r-- | src/common/fs/path_util.h | 15 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 4 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 490 | ||||
| -rw-r--r-- | src/yuzu/main.h | 22 | ||||
| -rw-r--r-- | src/yuzu/util/util.cpp | 17 | ||||
| -rw-r--r-- | src/yuzu/util/util.h | 14 | 
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) {      return ToUTF8String(path.u8string());  } +std::u8string U8FilenameSantizer(const std::u8string_view u8filename) { +    std::u8string u8path_santized{u8filename.begin(), u8filename.end()}; +    size_t eSizeSanitized = u8path_santized.size(); + +    // Special case for ":", for example: 'Pepe: La secuela' --> 'Pepe - La +    // secuela' or 'Pepe : La secuela' --> 'Pepe - La secuela' +    for (size_t i = 0; i < eSizeSanitized; i++) { +        switch (u8path_santized[i]) { +        case u8':': +            if (i == 0 || i == eSizeSanitized - 1) { +                u8path_santized.replace(i, 1, u8"_"); +            } else if (u8path_santized[i - 1] == u8' ') { +                u8path_santized.replace(i, 1, u8"-"); +            } else { +                u8path_santized.replace(i, 1, u8" -"); +                eSizeSanitized++; +            } +            break; +        case u8'\\': +        case u8'/': +        case u8'*': +        case u8'?': +        case u8'\"': +        case u8'<': +        case u8'>': +        case u8'|': +        case u8'\0': +            u8path_santized.replace(i, 1, u8"_"); +            break; +        default: +            break; +        } +    } + +    // Delete duplicated spaces || Delete duplicated dots (MacOS i think) +    for (size_t i = 0; i < eSizeSanitized - 1; i++) { +        if ((u8path_santized[i] == u8' ' && u8path_santized[i + 1] == u8' ') || +            (u8path_santized[i] == u8'.' && u8path_santized[i + 1] == u8'.')) { +            u8path_santized.erase(i, 1); +            i--; +        } +    } + +    // Delete all spaces and dots at the end (Windows almost) +    while (u8path_santized.back() == u8' ' || u8path_santized.back() == u8'.') { +        u8path_santized.pop_back(); +    } + +    if (u8path_santized.empty()) { +        return u8""; +    } + +    return u8path_santized; +} + +std::string UTF8FilenameSantizer(const std::string_view filename) { +    return ToUTF8String(U8FilenameSantizer(ToU8String(filename))); +} +  } // 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>;   */  [[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path); -} // namespace Common::FS +/** + * Fix filename (remove invalid characters) + * + * @param u8_string dirty encoded filename string + * + * @returns utf8_string santized filename string + * + */ +[[nodiscard]] std::u8string U8FilenameSantizer(const std::u8string_view u8filename); + +/** + * Fix filename (remove invalid characters) + * + * @param utf8_string dirty encoded filename string + * + * @returns utf8_string santized filename string + * + */ +[[nodiscard]] std::string UTF8FilenameSantizer(const std::string_view filename); + +} // 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 @@  #include <unordered_map>  #include "common/fs/fs.h" +#include "common/string_util.h"  #ifdef ANDROID  #include "common/fs/fs_android.h"  #endif @@ -14,7 +15,7 @@  #include "common/logging/log.h"  #ifdef _WIN32 -#include <shlobj.h> // Used in GetExeDirectory() +#include <shlobj.h> // Used in GetExeDirectory() and GetWindowsDesktop()  #else  #include <cstdlib>     // Used in Get(Home/Data)Directory()  #include <pwd.h>       // Used in GetHomeDirectory() @@ -250,30 +251,39 @@ void SetYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {  #ifdef _WIN32  fs::path GetExeDirectory() { -    wchar_t exe_path[MAX_PATH]; +    WCHAR exe_path[MAX_PATH]; -    if (GetModuleFileNameW(nullptr, exe_path, MAX_PATH) == 0) { +    if (SUCCEEDED(GetModuleFileNameW(nullptr, exe_path, MAX_PATH))) { +        std::wstring wideExePath(exe_path); + +        // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with +        // the Windows library (Filesystem converts the strings literally). +        return fs::path{Common::UTF16ToUTF8(wideExePath)}.parent_path(); +    } else {          LOG_ERROR(Common_Filesystem, -                  "Failed to get the path to the executable of the current process"); +                  "[GetExeDirectory] Failed to get the path to the executable of the current " +                  "process");      } -    return fs::path{exe_path}.parent_path(); +    return fs::path{};  }  fs::path GetAppDataRoamingDirectory() {      PWSTR appdata_roaming_path = nullptr; -    SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_roaming_path); - -    auto fs_appdata_roaming_path = fs::path{appdata_roaming_path}; - -    CoTaskMemFree(appdata_roaming_path); +    if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &appdata_roaming_path))) { +        std::wstring wideAppdataRoamingPath(appdata_roaming_path); +        CoTaskMemFree(appdata_roaming_path); -    if (fs_appdata_roaming_path.empty()) { -        LOG_ERROR(Common_Filesystem, "Failed to get the path to the %APPDATA% directory"); +        // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with +        // the Windows library (Filesystem converts the strings literally). +        return fs::path{Common::UTF16ToUTF8(wideAppdataRoamingPath)}; +    } else { +        LOG_ERROR(Common_Filesystem, +                  "[GetAppDataRoamingDirectory] Failed to get the path to the %APPDATA% directory");      } -    return fs_appdata_roaming_path; +    return fs::path{};  }  #else @@ -338,6 +348,57 @@ fs::path GetBundleDirectory() {  #endif +fs::path GetDesktopPath() { +#if defined(_WIN32) +    PWSTR DesktopPath = nullptr; + +    if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &DesktopPath))) { +        std::wstring wideDesktopPath(DesktopPath); +        CoTaskMemFree(DesktopPath); + +        // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with +        // the Windows library (Filesystem converts the strings literally). +        return fs::path{Common::UTF16ToUTF8(wideDesktopPath)}; +    } else { +        LOG_ERROR(Common_Filesystem, +                  "[GetDesktopPath] Failed to get the path to the desktop directory"); +    } +#else +    fs::path shortcut_path = GetHomeDirectory() / "Desktop"; +    if (fs::exists(shortcut_path)) { +        return shortcut_path; +    } +#endif +    return fs::path{}; +} + +fs::path GetAppsShortcutsPath() { +#if defined(_WIN32) +    PWSTR AppShortcutsPath = nullptr; + +    if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &AppShortcutsPath))) { +        std::wstring wideAppShortcutsPath(AppShortcutsPath); +        CoTaskMemFree(AppShortcutsPath); + +        // UTF-16 filesystem lib to UTF-8 is broken, so we need to convert to UTF-8 with the with +        // the Windows library (Filesystem converts the strings literally). +        return fs::path{Common::UTF16ToUTF8(wideAppShortcutsPath)}; +    } else { +        LOG_ERROR(Common_Filesystem, +                  "[GetAppsShortcutsPath] Failed to get the path to the App Shortcuts directory"); +    } +#else +    fs::path shortcut_path = GetHomeDirectory() / ".local/share/applications"; +    if (!fs::exists(shortcut_path)) { +        shortcut_path = std::filesystem::path("/usr/share/applications"); +        return shortcut_path; +    } else { +        return shortcut_path; +    } +#endif +    return fs::path{}; +} +  // vvvvvvvvvv Deprecated vvvvvvvvvv //  std::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) {   * @returns The path of the current user's %APPDATA% directory.   */  [[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory(); -  #else  /** @@ -275,6 +274,20 @@ void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {  #endif +/** + * Gets the path of the current user's desktop directory. + * + * @returns The path of the current user's desktop directory. + */ +[[nodiscard]] std::filesystem::path GetDesktopPath(); + +/** + * Gets the path of the current user's apps directory. + * + * @returns The path of the current user's apps directory. + */ +[[nodiscard]] std::filesystem::path GetAppsShortcutsPath(); +  // vvvvvvvvvv Deprecated vvvvvvvvvv //  // 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      QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));      QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));      QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); -#ifndef WIN32      QAction* create_applications_menu_shortcut =          shortcut_menu->addAction(tr("Add to Applications Menu")); -#endif      context_menu.addSeparator();      QAction* properties = context_menu.addAction(tr("Properties")); @@ -647,11 +645,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri      connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {          emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);      }); -#ifndef WIN32      connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {          emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);      }); -#endif      connect(properties, &QAction::triggered,              [this, path]() { emit OpenPerGameGeneralRequested(path); });  }; 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,      QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory));  } -void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, -                                           GameListShortcutTarget target) { -    // Get path to yuzu executable -    const QStringList args = QApplication::arguments(); -    std::filesystem::path yuzu_command = args[0].toStdString(); +bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, +                                     const std::string& comment, +                                     const std::filesystem::path& icon_path, +                                     const std::filesystem::path& command, +                                     const std::string& arguments, const std::string& categories, +                                     const std::string& keywords, const std::string& name) { -    // If relative path, make it an absolute path -    if (yuzu_command.c_str()[0] == '.') { -        yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; -    } +    bool shortcut_succeeded = false; -#if defined(__linux__) -    // Warn once if we are making a shortcut to a volatile AppImage -    const std::string appimage_ending = -        std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); -    if (yuzu_command.string().ends_with(appimage_ending) && -        !UISettings::values.shortcut_already_warned) { -        if (QMessageBox::warning(this, tr("Create Shortcut"), -                                 tr("This will create a shortcut to the current AppImage. This may " -                                    "not work well if you update. Continue?"), -                                 QMessageBox::StandardButton::Ok | -                                     QMessageBox::StandardButton::Cancel) == -            QMessageBox::StandardButton::Cancel) { -            return; +    // Replace characters that are illegal in Windows filenames +    std::filesystem::path shortcut_path_full = +        shortcut_path / Common::FS::UTF8FilenameSantizer(name); + +#if defined(__linux__) || defined(__FreeBSD__) +    shortcut_path_full += ".desktop"; +#elif defined(_WIN32) +    shortcut_path_full += ".lnk"; +#endif + +    LOG_INFO(Common, "[GMainWindow::CreateShortcutLink] Create shortcut path: {}", +             shortcut_path_full.string()); + +#if defined(__linux__) || defined(__FreeBSD__) +    // This desktop file template was writing referencing +    // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html +    try { + +        // Plus 'Type' is required +        if (name.empty()) { +            LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Name is empty"); +            shortcut_succeeded = false; +            return shortcut_succeeded;          } -        UISettings::values.shortcut_already_warned = true; -    } -#endif // __linux__ +        std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); -    std::filesystem::path target_directory{}; +        if (shortcut_stream.is_open()) { -    switch (target) { -    case GameListShortcutTarget::Desktop: { -        const QString desktop_path = -            QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); -        target_directory = desktop_path.toUtf8().toStdString(); -        break; -    } -    case GameListShortcutTarget::Applications: { -        const QString applications_path = -            QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); -        if (applications_path.isEmpty()) { -            const char* home = std::getenv("HOME"); -            if (home != nullptr) { -                target_directory = std::filesystem::path(home) / ".local/share/applications"; +            fmt::print(shortcut_stream, "[Desktop Entry]\n"); +            fmt::print(shortcut_stream, "Type=Application\n"); +            fmt::print(shortcut_stream, "Version=1.0\n"); +            fmt::print(shortcut_stream, "Name={}\n", name); + +            if (!comment.empty()) { +                fmt::print(shortcut_stream, "Comment={}\n", comment);              } + +            if (std::filesystem::is_regular_file(icon_path)) { +                fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); +            } + +            fmt::print(shortcut_stream, "TryExec={}\n", command.string()); +            fmt::print(shortcut_stream, "Exec={}", command.string()); + +            if (!arguments.empty()) { +                fmt::print(shortcut_stream, " {}", arguments); +            } + +            fmt::print(shortcut_stream, "\n"); + +            if (!categories.empty()) { +                fmt::print(shortcut_stream, "Categories={}\n", categories); +            } + +            if (!keywords.empty()) { +                fmt::print(shortcut_stream, "Keywords={}\n", keywords); +            } + +            shortcut_stream.close(); +            return true; +          } else { -            target_directory = applications_path.toUtf8().toStdString(); +            LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create shortcut"); +            return false;          } -        break; + +        shortcut_stream.close(); +    } catch (const std::exception& e) { +        LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create shortcut: {}", +                  e.what());      } -    default: -        return; +#elif defined(_WIN32) +    // Initialize COM +    auto hr = CoInitialize(NULL); +    if (FAILED(hr)) { +        return shortcut_succeeded;      } -    const QDir dir(QString::fromStdString(target_directory.generic_string())); -    if (!dir.exists()) { -        QMessageBox::critical(this, tr("Create Shortcut"), -                              tr("Cannot create shortcut. Path \"%1\" does not exist.") -                                  .arg(QString::fromStdString(target_directory.generic_string())), -                              QMessageBox::StandardButton::Ok); -        return; +    IShellLinkW* ps1; + +    auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, +                                 (void**)&ps1); + +    // The UTF-16 / UTF-8 conversion is broken in C++, it is necessary to perform these steps and +    // resort to the native Windows function. +    std::wstring wshortcut_path_full = Common::UTF8ToUTF16W(shortcut_path_full.string()); +    std::wstring wicon_path = Common::UTF8ToUTF16W(icon_path.string()); +    std::wstring wcommand = Common::UTF8ToUTF16W(command.string()); +    std::wstring warguments = Common::UTF8ToUTF16W(arguments); +    std::wstring wcomment = Common::UTF8ToUTF16W(comment); + +    if (SUCCEEDED(hres)) { +        if (std::filesystem::is_regular_file(command)) +            hres = ps1->SetPath(wcommand.data()); + +        if (SUCCEEDED(hres) && !arguments.empty()) +            hres = ps1->SetArguments(warguments.data()); + +        if (SUCCEEDED(hres) && !comment.empty()) +            hres = ps1->SetDescription(wcomment.data()); + +        if (SUCCEEDED(hres) && std::filesystem::is_regular_file(icon_path)) +            hres = ps1->SetIconLocation(wicon_path.data(), 0); + +        IPersistFile* pPersistFile = nullptr; + +        if (SUCCEEDED(hres)) { +            hres = ps1->QueryInterface(IID_IPersistFile, (void**)&pPersistFile); + +            if (SUCCEEDED(hres) && pPersistFile != nullptr) { +                hres = pPersistFile->Save(wshortcut_path_full.data(), TRUE); +                if (SUCCEEDED(hres)) { +                    shortcut_succeeded = true; +                } +            } +        } + +        if (pPersistFile != nullptr) { +            pPersistFile->Release(); +        } +    } else { +        LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Failed to create IShellLinkWinstance");      } -    const std::string game_file_name = std::filesystem::path(game_path).filename().string(); -    // Determine full paths for icon and shortcut -#if defined(__linux__) || defined(__FreeBSD__) -    const char* home = std::getenv("HOME"); -    const std::filesystem::path home_path = (home == nullptr ? "~" : home); -    const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); - -    std::filesystem::path system_icons_path = -        (xdg_data_home == nullptr ? home_path / ".local/share/" -                                  : std::filesystem::path(xdg_data_home)) / -        "icons/hicolor/256x256"; -    if (!Common::FS::CreateDirs(system_icons_path)) { +    ps1->Release(); +    CoUninitialize(); +#endif + +    if (shortcut_succeeded && std::filesystem::is_regular_file(shortcut_path_full)) { +        LOG_INFO(Common, "[GMainWindow::CreateShortcutLink] Shortcut created"); +    } else { +        LOG_ERROR(Common, "[GMainWindow::CreateShortcutLink] Shortcut error, failed to create it"); +        shortcut_succeeded = false; +    } + +    return shortcut_succeeded; +} + +// Messages in pre-defined message boxes for less code spaghetti +bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, const int& imsg, +                                            const std::string title) { +    QMessageBox::StandardButtons buttons; +    int result = 0; + +    switch (imsg) { + +    case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES: +        buttons = QMessageBox::Yes | QMessageBox::No; + +        result = +            QMessageBox::information(parent, tr("Create Shortcut"), +                                     tr("Do you want to launch the game in fullscreen?"), buttons); + +        LOG_INFO(Frontend, "Shortcut will launch in fullscreen"); +        return (result == QMessageBox::No) ? false : true; + +    case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: +        QMessageBox::information( +            parent, tr("Create Shortcut"), +            tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); +        LOG_INFO(Frontend, "Successfully created a shortcut to {}", title); +        return true; + +    case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING: +        result = QMessageBox::warning( +            this, tr("Create Shortcut"), +            tr("This will create a shortcut to the current AppImage. This may " +               "not work well if you update. Continue?"), +            QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel); +        return (result == QMessageBox::StandardButton::Cancel) ? true : false; +    case GMainWindow::CREATE_SHORTCUT_MSGBOX_ADMIN: +        buttons = QMessageBox::Ok; +        QMessageBox::critical(parent, tr("Create Shortcut"), +                              tr("Cannot create shortcut in Apps. Restart yuzu as administrator."), +                              buttons); +        LOG_ERROR(Frontend, "Cannot create shortcut in Apps. Restart yuzu as administrator."); +        return true; +    default: +        buttons = QMessageBox::Ok; +        QMessageBox::critical( +            parent, tr("Create Shortcut"), +            tr("Failed to create a shortcut to %1").arg(QString::fromStdString(title)), buttons); +        LOG_ERROR(Frontend, "Failed to create a shortcut to {}", title); +        return true; +    } + +    return true; +} + +bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, +                                      std::filesystem::path& icons_path) { + +    // Get path to Yuzu icons directory & icon extension +    std::string ico_extension = "png"; +#if defined(_WIN32) +    icons_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "icons"; +    ico_extension = "ico"; +#elif defined(__linux__) || defined(__FreeBSD__) +    icons_path = GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256"; +#endif + +    // Create icons directory if it doesn't exist +    if (!Common::FS::CreateDirs(icons_path)) {          QMessageBox::critical(              this, tr("Create Icon"),              tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") -                .arg(QString::fromStdString(system_icons_path)), +                .arg(QString::fromStdString(icons_path.string())),              QMessageBox::StandardButton::Ok); -        return; +        icons_path = ""; // Reset path +        return false;      } -    std::filesystem::path icon_path = -        system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name) -                                             : fmt::format("yuzu-{:016X}.png", program_id)); -    const std::filesystem::path shortcut_path = -        target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) -                                            : fmt::format("yuzu-{:016X}.desktop", program_id)); -#elif defined(WIN32) -    std::filesystem::path icons_path = -        Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); -    std::filesystem::path icon_path = -        icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) -                                       : fmt::format("yuzu-{:016X}.ico", program_id))); -#else -    std::string icon_extension; -#endif -    // Get title from game file -    const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), -                                   system->GetContentProvider()}; -    const auto control = pm.GetControlMetadata(); -    const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); +    // Create icon file path +    icons_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension) +                                   : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension)); +    return true; +} -    std::string title{fmt::format("{:016X}", program_id)}; +void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, +                                           GameListShortcutTarget target) { -    if (control.first != nullptr) { -        title = control.first->GetApplicationName(); -    } else { -        loader->ReadTitle(title); +    // Get path to yuzu executable +    const QStringList args = QApplication::arguments(); +    std::filesystem::path yuzu_command = args[0].toStdString(); + +    // If relative path, make it an absolute path +    if (yuzu_command.c_str()[0] == '.') { +        yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;      } -    // Get icon from game file -    std::vector<u8> icon_image_file{}; -    if (control.second != nullptr) { -        icon_image_file = control.second->ReadAllBytes(); -    } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { -        LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); +    // Shortcut path +    std::filesystem::path shortcut_path{}; +    if (target == GameListShortcutTarget::Desktop) { +        shortcut_path = Common::FS::GetDesktopPath(); +        if (!std::filesystem::exists(shortcut_path)) { +            shortcut_path = +                QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); +        } +    } else if (target == GameListShortcutTarget::Applications) { + +#if defined(_WIN32) +        HANDLE hProcess = GetCurrentProcess(); +        if (!IsUserAnAdmin()) { +            GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ADMIN, +                                                   ""); +            return; +        } +        CloseHandle(hProcess); +#endif // _WIN32 + +        shortcut_path = Common::FS::GetAppsShortcutsPath(); +        if (!std::filesystem::exists(shortcut_path)) { +            shortcut_path = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) +                                .toStdString(); +        }      } -    QImage icon_data = -        QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); -#if defined(__linux__) || defined(__FreeBSD__) -    // Convert and write the icon as a PNG -    if (!icon_data.save(QString::fromStdString(icon_path.string()))) { -        LOG_ERROR(Frontend, "Could not write icon as PNG to file"); +    // Icon path and title +    std::string title; +    std::filesystem::path icons_path; +    if (std::filesystem::exists(shortcut_path)) { + +        // Get title from game file +        const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), +                                       system->GetContentProvider()}; +        const auto control = pm.GetControlMetadata(); +        const auto loader = +            Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); + +        title = fmt::format("{:016X}", program_id); + +        if (control.first != nullptr) { +            title = control.first->GetApplicationName(); +        } else { +            loader->ReadTitle(title); +        } + +        // Get icon from game file +        std::vector<u8> icon_image_file{}; +        if (control.second != nullptr) { +            icon_image_file = control.second->ReadAllBytes(); +        } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { +            LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); +        } + +        QImage icon_data = +            QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); + +        if (GMainWindow::MakeShortcutIcoPath(program_id, title, icons_path)) { +            if (!SaveIconToFile(icon_data, icons_path)) { +                LOG_ERROR(Frontend, "Could not write icon to file"); +            } +        } +      } else { -        LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); -    } -#elif defined(WIN32) -    if (!SaveIconToFile(icon_path.string(), icon_data)) { -        LOG_ERROR(Frontend, "Could not write icon to file"); +        GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, +                                               title); +        LOG_ERROR(Frontend, "[GMainWindow::OnGameListCreateShortcut] Invalid shortcut target");          return;      } -#endif // __linux__ -#ifdef _WIN32 -    // Replace characters that are illegal in Windows filenames by a dash -    const std::string illegal_chars = "<>:\"/\\|?*"; -    for (char c : illegal_chars) { -        std::replace(title.begin(), title.end(), c, '_'); +// Special case for AppImages +#if defined(__linux__) +    // Warn once if we are making a shortcut to a volatile AppImage +    const std::string appimage_ending = +        std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); +    if (yuzu_command.string().ends_with(appimage_ending) && +        !UISettings::values.shortcut_already_warned) { +        if (GMainWindow::CreateShortcutMessagesGUI( +                this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, title)) { +            return; +        } +        UISettings::values.shortcut_already_warned = true;      } -    const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); -#endif +#endif // __linux__ +    // Create shortcut +    std::string arguments = fmt::format("-g \"{:s}\"", game_path); +    if (GMainWindow::CreateShortcutMessagesGUI( +            this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, title)) { +        arguments = "-f " + arguments; +    }      const std::string comment =          tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); -    const std::string arguments = fmt::format("-g \"{:s}\"", game_path);      const std::string categories = "Game;Emulator;Qt;";      const std::string keywords = "Switch;Nintendo;"; -    if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), -                        yuzu_command.string(), arguments, categories, keywords)) { -        QMessageBox::critical(this, tr("Create Shortcut"), -                              tr("Failed to create a shortcut at %1") -                                  .arg(QString::fromStdString(shortcut_path.string()))); +    if (GMainWindow::CreateShortcutLink(shortcut_path, comment, icons_path, yuzu_command, arguments, +                                        categories, keywords, title)) { +        GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS, +                                               title);          return;      } -    LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); -    QMessageBox::information( -        this, tr("Create Shortcut"), -        tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); +    GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, title);  }  void GMainWindow::OnGameListOpenDirectory(const QString& directory) { @@ -3998,66 +4178,6 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file      }  } -bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::string& title, -                                 const std::string& comment, const std::string& icon_path, -                                 const std::string& command, const std::string& arguments, -                                 const std::string& categories, const std::string& keywords) { -#if defined(__linux__) || defined(__FreeBSD__) -    // This desktop file template was writing referencing -    // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html -    std::string shortcut_contents{}; -    shortcut_contents.append("[Desktop Entry]\n"); -    shortcut_contents.append("Type=Application\n"); -    shortcut_contents.append("Version=1.0\n"); -    shortcut_contents.append(fmt::format("Name={:s}\n", title)); -    shortcut_contents.append(fmt::format("Comment={:s}\n", comment)); -    shortcut_contents.append(fmt::format("Icon={:s}\n", icon_path)); -    shortcut_contents.append(fmt::format("TryExec={:s}\n", command)); -    shortcut_contents.append(fmt::format("Exec={:s} {:s}\n", command, arguments)); -    shortcut_contents.append(fmt::format("Categories={:s}\n", categories)); -    shortcut_contents.append(fmt::format("Keywords={:s}\n", keywords)); - -    std::ofstream shortcut_stream(shortcut_path); -    if (!shortcut_stream.is_open()) { -        LOG_WARNING(Common, "Failed to create file {:s}", shortcut_path); -        return false; -    } -    shortcut_stream << shortcut_contents; -    shortcut_stream.close(); - -    return true; -#elif defined(WIN32) -    IShellLinkW* shell_link; -    auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, -                                 (void**)&shell_link); -    if (FAILED(hres)) { -        return false; -    } -    shell_link->SetPath( -        Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to -    shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); -    shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); -    shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); - -    IPersistFile* persist_file; -    hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); -    if (FAILED(hres)) { -        return false; -    } - -    hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); -    if (FAILED(hres)) { -        return false; -    } - -    persist_file->Release(); -    shell_link->Release(); - -    return true; -#endif -    return false; -} -  void GMainWindow::OnLoadAmiibo() {      if (emu_thread == nullptr || !emu_thread->IsRunning()) {          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 @@  #include <memory>  #include <optional> +#include <filesystem>  #include <QMainWindow>  #include <QMessageBox>  #include <QTimer> @@ -151,6 +152,14 @@ class GMainWindow : public QMainWindow {          UI_EMU_STOPPING,      }; +    const enum { +        CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, +        CREATE_SHORTCUT_MSGBOX_SUCCESS, +        CREATE_SHORTCUT_MSGBOX_ERROR, +        CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, +        CREATE_SHORTCUT_MSGBOX_ADMIN, +    }; +  public:      void filterBarSetChecked(bool state);      void UpdateUITheme(); @@ -433,11 +442,14 @@ private:      bool ConfirmShutdownGame();      QString GetTasStateDescription() const; -    bool CreateShortcut(const std::string& shortcut_path, const std::string& title, -                        const std::string& comment, const std::string& icon_path, -                        const std::string& command, const std::string& arguments, -                        const std::string& categories, const std::string& keywords); - +    bool CreateShortcutMessagesGUI(QWidget* parent, const int& imsg, const std::string title); +    bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, +                             std::filesystem::path& icons_path); +    bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, +                            const std::filesystem::path& icon_path, +                            const std::filesystem::path& command, const std::string& arguments, +                            const std::string& categories, const std::string& keywords, +                            const std::string& name);      /**       * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog       * 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) {      return circle_pixmap;  } -bool SaveIconToFile(const std::string_view path, const QImage& image) { +bool SaveIconToFile(const QImage& image, const std::filesystem::path& icon_path) {  #if defined(WIN32)  #pragma pack(push, 2)      struct IconDir { @@ -73,7 +73,7 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) {          .id_count = static_cast<WORD>(scale_sizes.size()),      }; -    Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, +    Common::FS::IOFile icon_file(icon_path.string(), Common::FS::FileAccessMode::Write,                                   Common::FS::FileType::BinaryFile);      if (!icon_file.IsOpen()) {          return false; @@ -135,7 +135,16 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) {      icon_file.Close();      return true; -#else -    return false; +#elif defined(__linux__) || defined(__FreeBSD__) +    // Convert and write the icon as a PNG +    if (!image.save(QString::fromStdString(icon_path.string()))) { +        LOG_ERROR(Frontend, "Could not write icon as PNG to file"); +    } else { +        LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); +    } + +    return true;  #endif + +    return false;  } 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 @@  #pragma once +#include <filesystem>  #include <QFont>  #include <QString>  /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. +  [[nodiscard]] QFont GetMonospaceFont();  /// Convert a size in bytes into a readable format (KiB, MiB, etc.) +  [[nodiscard]] QString ReadableByteSize(qulonglong size);  /**   * Creates a circle pixmap from a specified color + *   * @param color The color the pixmap shall have + *   * @return QPixmap circle pixmap   */ +  [[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color);  /**   * Saves a windows icon to a file - * @param path The icons path + *   * @param image The image to save + * + * @param path The icons path + *   * @return bool If the operation succeeded   */ -[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); + +[[nodiscard]] bool SaveIconToFile(const QImage& image, const std::filesystem::path& icon_path); | 
