From 0a34403ef8be3d63187ccaa9a0eaaea07c8a7dd2 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 12 Feb 2022 02:19:46 +0100 Subject: [PATCH] Qt: create rpcs3 shortcuts --- rpcs3/Emu/system_utils.cpp | 27 +++ rpcs3/Emu/system_utils.hpp | 2 + rpcs3/rpcs3.vcxproj | 4 +- rpcs3/rpcs3.vcxproj.filters | 6 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/game_list_frame.cpp | 29 +++ rpcs3/rpcs3qt/qt_utils.cpp | 55 ++++++ rpcs3/rpcs3qt/qt_utils.h | 3 + rpcs3/rpcs3qt/shortcut_utils.cpp | 289 ++++++++++++++++++++++++++++++ rpcs3/rpcs3qt/shortcut_utils.h | 11 ++ rpcs3/rpcs3qt/update_manager.cpp | 26 +-- 11 files changed, 429 insertions(+), 24 deletions(-) create mode 100644 rpcs3/rpcs3qt/shortcut_utils.cpp create mode 100644 rpcs3/rpcs3qt/shortcut_utils.h diff --git a/rpcs3/Emu/system_utils.cpp b/rpcs3/Emu/system_utils.cpp index a528c6ff14..56bea1c7fe 100644 --- a/rpcs3/Emu/system_utils.cpp +++ b/rpcs3/Emu/system_utils.cpp @@ -16,6 +16,8 @@ #ifdef _WIN32 #include +#else +#include #endif LOG_CHANNEL(sys_log, "SYS"); @@ -113,6 +115,31 @@ namespace rpcs3::utils const usz last = path_to_exe.find_last_of('\\'); return last == std::string::npos ? std::string("") : path_to_exe.substr(0, last + 1); } +#elif !defined(__APPLE__) + std::string get_executable_path() + { + if (const char* appimage_path = ::getenv("APPIMAGE")) + { + sys_log.notice("Found AppImage path: %s", appimage_path); + return std::string(appimage_path); + } + + sys_log.warning("Failed to find AppImage path"); + + char exe_path[PATH_MAX]; + const ssize_t len = ::readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + + if (len == -1) + { + sys_log.error("Failed to find executable path"); + return {}; + } + + exe_path[len] = '\0'; + sys_log.trace("Found exec path: %s", exe_path); + + return std::string(exe_path); + } #endif std::string get_emu_dir() diff --git a/rpcs3/Emu/system_utils.hpp b/rpcs3/Emu/system_utils.hpp index 424b1c2030..525cd959f9 100644 --- a/rpcs3/Emu/system_utils.hpp +++ b/rpcs3/Emu/system_utils.hpp @@ -15,6 +15,8 @@ namespace rpcs3::utils #ifdef _WIN32 std::string get_exe_dir(); +#elif !defined(__APPLE__) + std::string get_executable_path(); #endif std::string get_emu_dir(); diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 707b6a3de5..525939a3eb 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -616,6 +616,7 @@ + @@ -1137,6 +1138,7 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) @@ -1517,4 +1519,4 @@ - + \ No newline at end of file diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index baaca3c499..04fdc6c4b5 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -804,6 +804,9 @@ Generated Files\Release + + Gui\utils + @@ -947,6 +950,9 @@ Generated Files + + Gui\utils + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 1803d4c8e3..f900c39769 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -68,6 +68,7 @@ set(SRC_FILES sendmessage_dialog_frame.cpp settings.cpp settings_dialog.cpp + shortcut_utils.cpp skylander_dialog.cpp syntax_highlighter.cpp tooltips.cpp diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 1dbfa46abc..b9264adbdd 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -1,5 +1,6 @@ #include "game_list_frame.h" #include "qt_utils.h" +#include "shortcut_utils.h" #include "settings_dialog.h" #include "pad_settings_dialog.h" #include "table_item_delegate.h" @@ -980,6 +981,34 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) : tr("&Create Custom Gamepad Configuration")); QAction* configure_patches = menu.addAction(tr("&Manage Game Patches")); QAction* create_ppu_cache = menu.addAction(tr("&Create PPU Cache")); +#ifndef __APPLE__ + menu.addSeparator(); + const auto on_shortcut = [this, gameinfo](bool is_desktop_shortcut) + { + const std::string target_cli_args = fmt::format("--no-gui \"%s\"", gameinfo->info.path); + const std::string target_icon_dir = fmt::format("%sIcons/game_icons/%s/", fs::get_config_dir(), gameinfo->info.serial); + + if (gui::utils::create_shortcut(gameinfo->info.name, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, is_desktop_shortcut)) + { + game_list_log.success("Created %s shortcut for %s", is_desktop_shortcut ? "desktop" : "application menu", sstr(qstr(gameinfo->info.name).simplified())); + QMessageBox::information(this, tr("Success!"), tr("Successfully created a shortcut.")); + } + else + { + game_list_log.error("Failed to create %s shortcut for %s", is_desktop_shortcut ? "desktop" : "application menu", sstr(qstr(gameinfo->info.name).simplified())); + QMessageBox::warning(this, tr("Warning!"), tr("Failed to create a shortcut!")); + } + }; + QMenu* shortcut_menu = menu.addMenu(tr("&Create Shortcut")); + QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("&Create Desktop Shortcut")); + connect(create_desktop_shortcut, &QAction::triggered, this, [this, gameinfo, on_shortcut](){ on_shortcut(true); }); +#ifdef _WIN32 + QAction* create_start_menu_shortcut = shortcut_menu->addAction(tr("&Create Start Menu Shortcut")); +#else + QAction* create_start_menu_shortcut = shortcut_menu->addAction(tr("&Create Application Menu Shortcut")); +#endif + connect(create_start_menu_shortcut, &QAction::triggered, this, [this, gameinfo, on_shortcut](){ on_shortcut(false); }); +#endif menu.addSeparator(); QAction* rename_title = menu.addAction(tr("&Rename In Game List")); QAction* hide_serial = menu.addAction(tr("&Hide From Game List")); diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index ae0c817a97..e3d0a9b6cb 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -10,6 +10,7 @@ #include "Emu/system_utils.hpp" #include "Utilities/File.h" +#include inline std::string sstr(const QString& _in) { return _in.toStdString(); } constexpr auto qstr = QString::fromStdString; @@ -40,6 +41,60 @@ namespace gui return QRect(frame_x, frame_y, target_width, target_height); } + bool create_square_pixmap(QPixmap& pixmap, int target_size) + { + if (pixmap.isNull()) + return false; + + QSize canvas_size(target_size, target_size); + QSize pixmap_size(pixmap.size()); + QPoint target_pos; + + // Let's upscale the original pixmap to at least fit into the outer rect. + if (pixmap_size.width() < target_size || pixmap_size.height() < target_size) + { + pixmap_size.scale(target_size, target_size, Qt::KeepAspectRatio); + } + + canvas_size = pixmap_size; + + // Calculate the centered size and position of the icon on our canvas. + if (pixmap_size.width() != target_size || pixmap_size.height() != target_size) + { + ensure(pixmap_size.height() > 0); + constexpr double target_ratio = 1.0; // square icon + + if ((pixmap_size.width() / static_cast(pixmap_size.height())) > target_ratio) + { + canvas_size.setHeight(std::ceil(pixmap_size.width() / target_ratio)); + } + else + { + canvas_size.setWidth(std::ceil(pixmap_size.height() * target_ratio)); + } + + target_pos.setX(std::max(0, (canvas_size.width() - pixmap_size.width()) / 2.0)); + target_pos.setY(std::max(0, (canvas_size.height() - pixmap_size.height()) / 2.0)); + } + + // Create a canvas large enough to fit our entire scaled icon + QPixmap canvas(canvas_size); + canvas.fill(Qt::transparent); + + // Create a painter for our canvas + QPainter painter(&canvas); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // Draw the icon onto our canvas + painter.drawPixmap(target_pos.x(), target_pos.y(), pixmap_size.width(), pixmap_size.height(), pixmap); + + // Finish the painting + painter.end(); + + pixmap = canvas; + return true; + } + QPixmap get_colorized_pixmap(const QPixmap& old_pixmap, const QColor& old_color, const QColor& new_color, bool use_special_masks, bool colorize_all) { QPixmap pixmap = old_pixmap; diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index acfe442550..2d8e075188 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -45,6 +45,9 @@ namespace gui // while still considering screen boundaries. QRect create_centered_window_geometry(const QScreen* screen, const QRect& base, s32 target_width, s32 target_height); + // Creates a square pixmap while keeping the original aspect ratio of the image. + bool create_square_pixmap(QPixmap& pixmap, int target_size); + // Returns a custom colored QPixmap based on another QPixmap. // use colorize_all to repaint every opaque pixel with the chosen color // use_special_masks is only used for pixmaps with multiple predefined colors diff --git a/rpcs3/rpcs3qt/shortcut_utils.cpp b/rpcs3/rpcs3qt/shortcut_utils.cpp new file mode 100644 index 0000000000..718cc2fc0c --- /dev/null +++ b/rpcs3/rpcs3qt/shortcut_utils.cpp @@ -0,0 +1,289 @@ +#include "stdafx.h" +#include "shortcut_utils.h" +#include "qt_utils.h" +#include "Emu/system_utils.hpp" +#include "Emu/VFS.h" +#include "Utilities/StrUtil.h" + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include +#include +#elif !defined(__APPLE__) +#include +#include +#endif + +#include +#include + +LOG_CHANNEL(sys_log, "SYS"); + +namespace gui::utils +{ + bool create_square_shortcut_icon_file(const std::string& src_icon_path, const std::string& target_icon_dir, std::string& target_icon_path, const std::string& extension, int size) + { + if (src_icon_path.empty() || target_icon_dir.empty() || extension.empty()) + { + sys_log.error("Failed to create shortcut. Icon parameters empty."); + return false; + } + + QPixmap icon(QString::fromStdString(src_icon_path)); + if (!gui::utils::create_square_pixmap(icon, size)) + { + sys_log.error("Failed to create shortcut. Icon empty."); + return false; + } + + target_icon_path = target_icon_dir + "shortcut." + fmt::to_lower(extension); + + QFile icon_file(QString::fromStdString(target_icon_path)); + if (!icon_file.open(QFile::OpenModeFlag::ReadWrite | QFile::OpenModeFlag::Truncate)) + { + sys_log.error("Failed to create icon file: %s", target_icon_path); + return false; + } + + if (!icon.save(&icon_file, fmt::to_upper(extension).c_str())) + { + sys_log.error("Failed to write icon file: %s", target_icon_path); + return false; + } + + icon_file.close(); + return true; + } + + bool create_shortcut(const std::string& name, + [[maybe_unused]] const std::string& target_cli_args, + [[maybe_unused]] const std::string& description, + [[maybe_unused]] const std::string& src_icon_path, + [[maybe_unused]] const std::string& target_icon_dir, + bool is_desktop_shortcut) + { + if (name.empty()) + { + sys_log.error("Cannot create shortcuts without a name"); + return false; + } + + // Remove illegal characters from filename + const std::string simple_name = QString::fromStdString(vfs::escape(name, true)).simplified().toStdString(); + if (simple_name.empty() || simple_name == "." || simple_name == "..") + { + sys_log.error("Failed to create shortcut: Cleaned file name empty or not allowed"); + return false; + } + +#ifdef _WIN32 + std::string link_file; + + if (const char* home = getenv("USERPROFILE")) + { + if (is_desktop_shortcut) + { + link_file = fmt::format("%s/Desktop/%s.lnk", home, simple_name); + } + else + { + const std::string programs_dir = fmt::format("%s/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/RPCS3", home); + if (!fs::create_path(programs_dir)) + { + sys_log.error("Failed to create shortcut: Could not create start menu directory: %s", programs_dir); + return false; + } + link_file = fmt::format("%s/%s.lnk", programs_dir, simple_name); + } + } + else + { + sys_log.error("Failed to create shortcut: home path empty"); + return false; + } + + sys_log.notice("Creating shortcut '%s' with arguments '%s' and .ico dir '%s'", link_file, target_cli_args, target_icon_dir); + + const auto str_error = [](HRESULT hr) -> std::string + { + _com_error err(hr); + const TCHAR* errMsg = err.ErrorMessage(); + return fmt::format("%s [%d]", wchar_to_utf8(errMsg), hr); + }; + + // https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32 + HRESULT res = CoInitialize(NULL); + if (FAILED(res)) + { + sys_log.error("Failed to create shortcut: CoInitialize failed (%s)", str_error(res)); + return false; + } + + IShellLink* pShellLink = nullptr; + IPersistFile* pPersistFile = nullptr; + + const auto cleanup = [&](bool return_value, const std::string& fail_reason) -> bool + { + if (!return_value) sys_log.error("Failed to create shortcut: %s", fail_reason); + if (pPersistFile) pPersistFile->Release(); + if (pShellLink) pShellLink->Release(); + CoUninitialize(); + return return_value; + }; + + res = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&pShellLink); + if (FAILED(res)) + return cleanup(false, "CoCreateInstance failed"); + + const std::string working_dir{ rpcs3::utils::get_exe_dir() }; + const std::string rpcs3_path{ working_dir + "rpcs3.exe" }; + + const std::wstring w_target_file = utf8_to_wchar(rpcs3_path); + res = pShellLink->SetPath(w_target_file.c_str()); + if (FAILED(res)) + return cleanup(false, fmt::format("SetPath failed (%s)", str_error(res))); + + const std::wstring w_working_dir = utf8_to_wchar(working_dir); + res = pShellLink->SetWorkingDirectory(w_working_dir.c_str()); + if (FAILED(res)) + return cleanup(false, fmt::format("SetWorkingDirectory failed (%s)", str_error(res))); + + if (!target_cli_args.empty()) + { + const std::wstring w_target_cli_args = utf8_to_wchar(target_cli_args); + res = pShellLink->SetArguments(w_target_cli_args.c_str()); + if (FAILED(res)) + return cleanup(false, fmt::format("SetArguments failed (%s)", str_error(res))); + } + + if (!description.empty()) + { + const std::wstring w_descpription = utf8_to_wchar(description); + res = pShellLink->SetDescription(w_descpription.c_str()); + if (FAILED(res)) + return cleanup(false, fmt::format("SetDescription failed (%s)", str_error(res))); + } + + if (!src_icon_path.empty() && !target_icon_dir.empty()) + { + std::string target_icon_path; + if (!create_square_shortcut_icon_file(src_icon_path, target_icon_dir, target_icon_path, "ico", 512)) + return cleanup(false, ".ico creation failed"); + + const std::wstring w_icon_path = utf8_to_wchar(target_icon_path); + res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0); + if (FAILED(res)) + return cleanup(false, fmt::format("SetIconLocation failed (%s)", str_error(res))); + } + + // Use the IPersistFile object to save the shell link + res = pShellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&pPersistFile); + if (FAILED(res)) + return cleanup(false, fmt::format("QueryInterface failed (%s)", str_error(res))); + + // Save shortcut + const std::wstring w_link_file = utf8_to_wchar(link_file); + res = pPersistFile->Save(w_link_file.c_str(), TRUE); + if (FAILED(res)) + { + if (is_desktop_shortcut) + { + return cleanup(false, fmt::format("Saving file to desktop failed (%s)", str_error(res))); + } + else + { + return cleanup(false, fmt::format("Saving file to start menu failed (%s)", str_error(res))); + } + } + + return cleanup(true, {}); + +#elif !defined(__APPLE__) + + const std::string exe_path = rpcs3::utils::get_executable_path(); + if (exe_path.empty()) + { + sys_log.error("Failed to create shortcut. Executable path empty."); + return false; + } + + std::string link_path; + + if (const char* home = ::getenv("HOME")) + { + if (is_desktop_shortcut) + { + link_path = fmt::format("%s/Desktop/%s.desktop", home, simple_name); + } + else + { + link_path = fmt::format("%s/.local/share/applications/%s.desktop", home, simple_name); + } + } + else + { + sys_log.error("Failed to create shortcut. home path empty."); + return false; + } + + std::string file_content; + fmt::append(file_content, "[Desktop Entry]\n"); + fmt::append(file_content, "Encoding=UTF-8\n"); + fmt::append(file_content, "Version=1.0\n"); + fmt::append(file_content, "Type=Application\n"); + fmt::append(file_content, "Terminal=false\n"); + fmt::append(file_content, "Exec=\"%s\" %s\n", exe_path, target_cli_args); + fmt::append(file_content, "Name=%s\n", name); + fmt::append(file_content, "Categories=Application;Game\n"); + + if (!description.empty()) + { + fmt::append(file_content, "Comment=%s\n", QString::fromStdString(description).simplified().toStdString()); + } + + if (!src_icon_path.empty() && !target_icon_dir.empty()) + { + std::string target_icon_path; + if (!create_square_shortcut_icon_file(src_icon_path, target_icon_dir, target_icon_path, "png", 512)) + { + // Error is logged in create_square_shortcut_icon_file + return false; + } + + fmt::append(file_content, "Icon=%s\n", src_icon_path); + } + + fs::file shortcut_file(link_path, fs::read + fs::rewrite); + if (!shortcut_file) + { + sys_log.error("Failed to create .desktop file: %s", link_path); + return false; + } + if (shortcut_file.write(file_content.data(), file_content.size()) != file_content.size()) + { + sys_log.error("Failed to write .desktop file: %s", link_path); + return false; + } + shortcut_file.close(); + + if (is_desktop_shortcut) + { + if (chmod(link_path.c_str(), S_IRWXU) != 0) // enables user to execute file + { + // Simply log failure. At least we have the file. + sys_log.error("Failed to change file permissions for .desktop file: %s (%d)", strerror(errno), errno); + } + } + + return true; +#else + sys_log.error("Cannot create shortcuts on this operating system"); + return false; +#endif + } +} diff --git a/rpcs3/rpcs3qt/shortcut_utils.h b/rpcs3/rpcs3qt/shortcut_utils.h new file mode 100644 index 0000000000..ea8fad8def --- /dev/null +++ b/rpcs3/rpcs3qt/shortcut_utils.h @@ -0,0 +1,11 @@ +#pragma once + +namespace gui::utils +{ + bool create_shortcut(const std::string& name, + const std::string& target_cli_args, + const std::string& description, + const std::string& src_icon_path, + const std::string& target_icon_dir, + bool is_desktop_shortcut); +} diff --git a/rpcs3/rpcs3qt/update_manager.cpp b/rpcs3/rpcs3qt/update_manager.cpp index 729da28277..da1b8d7845 100644 --- a/rpcs3/rpcs3qt/update_manager.cpp +++ b/rpcs3/rpcs3qt/update_manager.cpp @@ -584,30 +584,10 @@ bool update_manager::handle_rpcs3(const QByteArray& data, bool auto_accept) #else - std::string replace_path; - - const char* appimage_path = ::getenv("APPIMAGE"); - if (appimage_path != nullptr) + std::string replace_path = rpcs3::utils::get_executable_path(); + if (replace_path.empty()) { - replace_path = appimage_path; - update_log.notice("Found AppImage path: %s", appimage_path); - } - else - { - update_log.warning("Failed to find AppImage path"); - char exe_path[PATH_MAX]; - ssize_t len = ::readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); - - if (len == -1) - { - update_log.error("Failed to find executable path"); - return false; - } - - exe_path[len] = '\0'; - update_log.trace("Found exec path: %s", exe_path); - - replace_path = exe_path; + return false; } // Move the appimage/exe and replace with new appimage