diff options
| -rw-r--r-- | src/yuzu/game_list.cpp | 14 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 7 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 167 | ||||
| -rw-r--r-- | src/yuzu/main.h | 7 | ||||
| -rw-r--r-- | src/yuzu/uisettings.h | 1 | 
5 files changed, 196 insertions, 0 deletions
| diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 5c33c1b0f..22aa19c56 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -554,6 +554,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri      QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));      QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));      QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); +#ifndef WIN32 +    QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); +    QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); +    QAction* create_applications_menu_shortcut = +        shortcut_menu->addAction(tr("Add to Applications Menu")); +#endif      context_menu.addSeparator();      QAction* properties = context_menu.addAction(tr("Properties")); @@ -619,6 +625,14 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri      connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {          emit NavigateToGamedbEntryRequested(program_id, compatibility_list);      }); +#ifndef WIN32 +    connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { +        emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); +    }); +    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/game_list.h b/src/yuzu/game_list.h index cdf085019..f7ff93ed9 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -52,6 +52,11 @@ enum class DumpRomFSTarget {      SDMC,  }; +enum class GameListShortcutTarget { +    Desktop, +    Applications, +}; +  enum class InstalledEntryType {      Game,      Update, @@ -108,6 +113,8 @@ signals:                               const std::string& game_path);      void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);      void CopyTIDRequested(u64 program_id); +    void CreateShortcut(u64 program_id, const std::string& game_path, +                        GameListShortcutTarget target);      void NavigateToGamedbEntryRequested(u64 program_id,                                          const CompatibilityList& compatibility_list);      void OpenPerGameGeneralRequested(const std::string& file); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 6c204416f..7ae30abde 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -4,6 +4,8 @@  #include <cinttypes>  #include <clocale>  #include <cmath> +#include <fstream> +#include <iostream>  #include <memory>  #include <thread>  #ifdef __APPLE__ @@ -1249,6 +1251,7 @@ void GMainWindow::ConnectWidgetEvents() {      connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID);      connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,              &GMainWindow::OnGameListNavigateToGamedbEntry); +    connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);      connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);      connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,              &GMainWindow::OnGameListAddDirectory); @@ -2378,6 +2381,138 @@ 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(); + +#if defined(__linux__) || defined(__FreeBSD__) +    // If relative path, make it an absolute path +    if (yuzu_command.c_str()[0] == '.') { +        yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; +    } + +#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; +        } +        UISettings::values.shortcut_already_warned = true; +    } +#endif // __linux__ +#endif // __linux__ || __FreeBSD__ + +    std::filesystem::path target_directory{}; +    // Determine target directory for 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"); + +    if (target == GameListShortcutTarget::Desktop) { +        target_directory = home_path / "Desktop"; +        if (!Common::FS::IsDir(target_directory)) { +            QMessageBox::critical(this, tr("Create Shortcut"), +                                  tr("Cannot create shortcut on desktop. Path doesn't exist"), +                                  QMessageBox::StandardButton::Ok); +            return; +        } +    } else if (target == GameListShortcutTarget::Applications) { +        target_directory = +            (xdg_data_home == nullptr ? home_path / ".local/share/applications" : xdg_data_home); +        if (!Common::FS::IsDir(target_directory)) { +            QMessageBox::critical( +                this, tr("Create Shortcut"), +                tr("Cannot create shortcut in applications menu. Path doesn't exist"), +                QMessageBox::StandardButton::Ok); +            return; +        } +    } +#endif + +    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 std::filesystem::path icon_path = +        Common::FS::GetYuzuPath(Common::FS::YuzuPath::DumpDir) / +        (program_id == 0 ? fmt::format("{}.png", game_file_name) +                         : fmt::format("{:016X}/icon.png", program_id)); +    const std::filesystem::path shortcut_path = +        target_directory / (program_id == 0 ? fmt::format("{}.desktop", game_file_name) +                                            : fmt::format("{:016X}.desktop", program_id)); +#else +    const std::filesystem::path icon_path{}; +    const std::filesystem::path shortcut_path{}; +#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)); + +    std::string 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_jpeg = QImage::fromData(icon_image_file.data(), icon_image_file.size()); +#if defined(__linux__) || defined(__FreeBSD__) +    // Convert and write the icon as a PNG +    if (!icon_jpeg.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); +    } +#endif // __linux__ + +#if defined(__linux__) || defined(__FreeBSD__) +    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;"; +#else +    const std::string comment{}; +    const std::string arguments{}; +    const std::string categories{}; +    const std::string keywords{}; +#endif +    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()))); +        return; +    } + +    LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path); +    QMessageBox::information( +        this, tr("Create Shortcut"), +        tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); +} +  void GMainWindow::OnGameListOpenDirectory(const QString& directory) {      std::filesystem::path fs_path;      if (directory == QStringLiteral("SDMC")) { @@ -3296,6 +3431,38 @@ 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 writting 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; +#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 62d629973..a0f7aa77d 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -38,6 +38,7 @@ class QProgressDialog;  class WaitTreeWidget;  enum class GameListOpenTarget;  enum class GameListRemoveTarget; +enum class GameListShortcutTarget;  enum class DumpRomFSTarget;  enum class InstalledEntryType;  class GameListPlaceholder; @@ -293,6 +294,8 @@ private slots:      void OnGameListCopyTID(u64 program_id);      void OnGameListNavigateToGamedbEntry(u64 program_id,                                           const CompatibilityList& compatibility_list); +    void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, +                                  GameListShortcutTarget target);      void OnGameListOpenDirectory(const QString& directory);      void OnGameListAddDirectory();      void OnGameListShowList(bool show); @@ -365,6 +368,10 @@ private:      bool CheckDarkMode();      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);      std::unique_ptr<Ui::MainWindow> ui; diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 452038cd9..2006b883e 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -138,6 +138,7 @@ struct Values {      bool configuration_applied;      bool reset_to_defaults; +    bool shortcut_already_warned{false};      Settings::Setting<bool> disable_web_applet{true, "disable_web_applet"};  }; | 
