forked from eden-emu/eden
		
	yuzu qt: Create shortcuts on Linux
This creates a Desktop Entry file and a PNG icon for the entry when the
user right-clicks a game and selects "Create Shortcut -> Create
{Application,Desktop} Shortcut". This uses the current executable's path
to create the shortcut.
yuzu qt: Add more error checking and OS gating for shortcuts
main: Remove FreeBSD gating for shortcuts
I'm not going to test FreeBSD, so I don't know if they follow
Freedesktop.org or not. I just have to let someone else verify that it
works there and let them enable it.
main: Move shortcut function to its own function
This function should really be in a common library, at least among
frontends.
main: Remove image manip references
main: Fix difference in MinGW and native GCC versions
main: Fix negation in creat shortcut
Addresses review comment
Co-authored-by: Jan Beich <jbeich@FreeBSD.org>
main: Re-enable freedesktop shorcuts for FreeBSD
			
			
This commit is contained in:
		
							parent
							
								
									a4696285af
								
							
						
					
					
						commit
						18bdf45868
					
				
					 5 changed files with 196 additions and 0 deletions
				
			
		|  | @ -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* 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* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); | ||||||
|     QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); |     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(); |     context_menu.addSeparator(); | ||||||
|     QAction* properties = context_menu.addAction(tr("Properties")); |     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]() { |     connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { | ||||||
|         emit NavigateToGamedbEntryRequested(program_id, compatibility_list); |         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, |     connect(properties, &QAction::triggered, | ||||||
|             [this, path]() { emit OpenPerGameGeneralRequested(path); }); |             [this, path]() { emit OpenPerGameGeneralRequested(path); }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -52,6 +52,11 @@ enum class DumpRomFSTarget { | ||||||
|     SDMC, |     SDMC, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | enum class GameListShortcutTarget { | ||||||
|  |     Desktop, | ||||||
|  |     Applications, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| enum class InstalledEntryType { | enum class InstalledEntryType { | ||||||
|     Game, |     Game, | ||||||
|     Update, |     Update, | ||||||
|  | @ -108,6 +113,8 @@ signals: | ||||||
|                              const std::string& game_path); |                              const std::string& game_path); | ||||||
|     void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); |     void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); | ||||||
|     void CopyTIDRequested(u64 program_id); |     void CopyTIDRequested(u64 program_id); | ||||||
|  |     void CreateShortcut(u64 program_id, const std::string& game_path, | ||||||
|  |                         GameListShortcutTarget target); | ||||||
|     void NavigateToGamedbEntryRequested(u64 program_id, |     void NavigateToGamedbEntryRequested(u64 program_id, | ||||||
|                                         const CompatibilityList& compatibility_list); |                                         const CompatibilityList& compatibility_list); | ||||||
|     void OpenPerGameGeneralRequested(const std::string& file); |     void OpenPerGameGeneralRequested(const std::string& file); | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
| #include <clocale> | #include <clocale> | ||||||
| #include <cmath> | #include <cmath> | ||||||
|  | #include <fstream> | ||||||
|  | #include <iostream> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <thread> | #include <thread> | ||||||
| #ifdef __APPLE__ | #ifdef __APPLE__ | ||||||
|  | @ -1249,6 +1251,7 @@ void GMainWindow::ConnectWidgetEvents() { | ||||||
|     connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); |     connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); | ||||||
|     connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, |     connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, | ||||||
|             &GMainWindow::OnGameListNavigateToGamedbEntry); |             &GMainWindow::OnGameListNavigateToGamedbEntry); | ||||||
|  |     connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); | ||||||
|     connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); |     connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); | ||||||
|     connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, |     connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, | ||||||
|             &GMainWindow::OnGameListAddDirectory); |             &GMainWindow::OnGameListAddDirectory); | ||||||
|  | @ -2378,6 +2381,138 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, | ||||||
|     QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); |     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) { | void GMainWindow::OnGameListOpenDirectory(const QString& directory) { | ||||||
|     std::filesystem::path fs_path; |     std::filesystem::path fs_path; | ||||||
|     if (directory == QStringLiteral("SDMC")) { |     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() { | void GMainWindow::OnLoadAmiibo() { | ||||||
|     if (emu_thread == nullptr || !emu_thread->IsRunning()) { |     if (emu_thread == nullptr || !emu_thread->IsRunning()) { | ||||||
|         return; |         return; | ||||||
|  |  | ||||||
|  | @ -38,6 +38,7 @@ class QProgressDialog; | ||||||
| class WaitTreeWidget; | class WaitTreeWidget; | ||||||
| enum class GameListOpenTarget; | enum class GameListOpenTarget; | ||||||
| enum class GameListRemoveTarget; | enum class GameListRemoveTarget; | ||||||
|  | enum class GameListShortcutTarget; | ||||||
| enum class DumpRomFSTarget; | enum class DumpRomFSTarget; | ||||||
| enum class InstalledEntryType; | enum class InstalledEntryType; | ||||||
| class GameListPlaceholder; | class GameListPlaceholder; | ||||||
|  | @ -293,6 +294,8 @@ private slots: | ||||||
|     void OnGameListCopyTID(u64 program_id); |     void OnGameListCopyTID(u64 program_id); | ||||||
|     void OnGameListNavigateToGamedbEntry(u64 program_id, |     void OnGameListNavigateToGamedbEntry(u64 program_id, | ||||||
|                                          const CompatibilityList& compatibility_list); |                                          const CompatibilityList& compatibility_list); | ||||||
|  |     void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, | ||||||
|  |                                   GameListShortcutTarget target); | ||||||
|     void OnGameListOpenDirectory(const QString& directory); |     void OnGameListOpenDirectory(const QString& directory); | ||||||
|     void OnGameListAddDirectory(); |     void OnGameListAddDirectory(); | ||||||
|     void OnGameListShowList(bool show); |     void OnGameListShowList(bool show); | ||||||
|  | @ -365,6 +368,10 @@ private: | ||||||
|     bool CheckDarkMode(); |     bool CheckDarkMode(); | ||||||
| 
 | 
 | ||||||
|     QString GetTasStateDescription() const; |     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; |     std::unique_ptr<Ui::MainWindow> ui; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -138,6 +138,7 @@ struct Values { | ||||||
| 
 | 
 | ||||||
|     bool configuration_applied; |     bool configuration_applied; | ||||||
|     bool reset_to_defaults; |     bool reset_to_defaults; | ||||||
|  |     bool shortcut_already_warned{false}; | ||||||
|     Settings::Setting<bool> disable_web_applet{true, "disable_web_applet"}; |     Settings::Setting<bool> disable_web_applet{true, "disable_web_applet"}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 lat9nq
						lat9nq