Compare commits

...

17 Commits

Author SHA1 Message Date
boludoz
b7964a20c1 Clang format fixed 2023-10-09 03:46:03 -03:00
Franco M
23ae0cfdc4 Merge branch 'master' into Shortcuts-for-Windows-(Desktop-and-Apps) 2023-10-08 19:35:38 -03:00
boludoz
9ce9497d85 UTF-8 U compatible 2023-10-08 04:17:07 -03:00
boludoz
eb30a1e4c8 Clang format fixes 2023-10-08 00:51:07 -03:00
boludoz
46efdeb984 More fixes 2023-10-07 15:20:48 -03:00
boludoz
6f664f6811 Replaced native with "Boost locale" for string conversions multiplataform like (Tested) 2023-10-07 14:38:59 -03:00
boludoz
840de27fa0 It's not something I care about for the purposes of this simple modification. 2023-10-06 23:04:41 -03:00
boludoz
7455763407 Fixes for others plataforms 2023-10-06 02:50:43 -03:00
boludoz
a950cda919 Clang-format fixed 2023-10-06 00:07:35 -03:00
boludoz
f5fbf3a1f5 @DanielSvoboda ICO conversion multiplataform code 2023-10-06 00:06:05 -03:00
boludoz
e0a3ed8689 Clang-format fixes. 2023-10-05 20:14:10 -03:00
boludoz
70ca03e810 Bugfix icons 2023-10-05 19:44:58 -03:00
boludoz
05186e2599 Code refactoring and suggestions 2023-10-05 19:36:12 -03:00
boludoz
98248d59fd Fixed unicode conversion on Windows almost 2023-10-04 03:08:22 -03:00
boludoz
aec95e0200 Fixes for linux 2023-10-04 02:58:45 -03:00
boludoz
590c2a8d8b Clang format fixed 2023-10-03 20:37:52 -03:00
boludoz
604b1b6c86 Full shortcut Windows support + question for fullscreen mode 2023-10-03 20:15:53 -03:00
11 changed files with 526 additions and 182 deletions

View File

@@ -2,14 +2,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <iostream>
#include <string>
#include <boost/locale.hpp>
#include "common/fs/fs_util.h"
#include "common/polyfill_ranges.h"
namespace Common::FS {
std::u8string ToU8String(std::string_view utf8_string) {
return std::u8string{utf8_string.begin(), utf8_string.end()};
std::u8string ToU8String(std::wstring_view w_string) {
try {
auto utf8_string = boost::locale::conv::utf_to_utf<char>(w_string.data(),
w_string.data() + w_string.size());
return std::u8string(utf8_string.begin(), utf8_string.end());
} catch (const boost::locale::conv::conversion_error) {
return std::u8string{reinterpret_cast<const char8_t*>(w_string.data())};
} catch (const boost::locale::conv::invalid_charset_error) {
return std::u8string{reinterpret_cast<const char8_t*>(w_string.data())};
}
}
std::u8string ToU8String(std::string_view string) {
return std::u8string{string.begin(), string.end()};
}
std::u8string BufferToU8String(std::span<const u8> buffer) {
@@ -20,6 +35,17 @@ std::u8string_view BufferToU8StringView(std::span<const u8> buffer) {
return std::u8string_view{reinterpret_cast<const char8_t*>(buffer.data())};
}
std::wstring ToWString(std::u8string_view utf8_string) {
try {
return boost::locale::conv::utf_to_utf<wchar_t>(utf8_string.data(),
utf8_string.data() + utf8_string.size());
} catch (const boost::locale::conv::conversion_error) {
return std::wstring(utf8_string.begin(), utf8_string.end());
} catch (const boost::locale::conv::invalid_charset_error) {
return std::wstring(utf8_string.begin(), utf8_string.end());
}
}
std::string ToUTF8String(std::u8string_view u8_string) {
return std::string{u8_string.begin(), u8_string.end()};
}
@@ -36,4 +62,62 @@ std::string PathToUTF8String(const std::filesystem::path& path) {
return ToUTF8String(path.u8string());
}
std::u8string UTF8FilenameSantizer(std::u8string u8filename) {
std::u8string u8path_santized(u8filename);
size_t eSizeSanitized = u8path_santized.size();
/* Special case for ":", for example: 'Example: The Test' --> 'Example - The Test' or 'Example :
* The Test' --> 'Example - The Test'. */
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; 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;
}
} // namespace Common::FS

View File

@@ -18,11 +18,28 @@ concept IsChar = std::same_as<T, char>;
/**
* Converts a UTF-8 encoded std::string or std::string_view to a std::u8string.
*
* @param utf8_string UTF-8 encoded string
* @param string
*
* @returns UTF-8 encoded std::u8string.
*/
[[nodiscard]] std::u8string ToU8String(std::string_view utf8_string);
[[nodiscard]] std::u8string ToU8String(std::string_view string);
/**
* Converts a std::wstring or std::wstring_view to a std::u8string.
*
* @param wide encoded string
*
* @returns UTF-8 encoded std::u8string.
*/
[[nodiscard]] std::u8string ToU8String(std::wstring_view w_string);
/** Converts a UTF-8 encoded std::u8string or std::u8string_view to a std::wstring.
*
* @param utf8_string UTF-8 encoded string
*
* @returns UTF-8 encoded std::wstring.
*/
[[nodiscard]] std::wstring ToWString(std::u8string_view utf8_string);
/**
* Converts a buffer of bytes to a UTF8-encoded std::u8string.
@@ -82,4 +99,13 @@ concept IsChar = std::same_as<T, char>;
*/
[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path);
} // namespace Common::FS
/**
* Sanitizes a UTF-8 encoded filename.
*
* @param u8filename u8string_view encoded filename
*
* @returns Sanitized u8string encoded filename.
*/
[[nodiscard]] std::u8string UTF8FilenameSantizer(std::u8string u8filename);
} // namespace Common::FS

View File

@@ -14,7 +14,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()
@@ -276,6 +276,39 @@ fs::path GetAppDataRoamingDirectory() {
return fs_appdata_roaming_path;
}
fs::path GetWindowsDesktopPath() {
PWSTR DesktopPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &DesktopPath))) {
std::wstring wideDesktopPath(DesktopPath);
CoTaskMemFree(DesktopPath);
return fs::path{wideDesktopPath};
} else {
LOG_ERROR(Common_Filesystem,
"[GetWindowsDesktopPath] Failed to get the path to the desktop directory");
}
return fs::path{};
}
fs::path GetWindowsAppShortcutsPath() {
PWSTR AppShortcutsPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &AppShortcutsPath))) {
std::wstring wideAppShortcutsPath(AppShortcutsPath);
CoTaskMemFree(AppShortcutsPath);
return fs::path{wideAppShortcutsPath};
} else {
LOG_ERROR(
Common_Filesystem,
"[GetWindowsAppShortcutsPath] Failed to get the path to the App Shortcuts directory");
}
return fs::path{};
}
#else
fs::path GetHomeDirectory() {

View File

@@ -245,6 +245,20 @@ void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {
*/
[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory();
/**
* Gets the path of the current user's desktop directory.
*
* @returns The path of the current user's desktop directory.
*/
[[nodiscard]] std::filesystem::path GetWindowsDesktopPath();
/**
* Gets FOLDERID_ApplicationShortcuts directory path on Windows.
*
* @returns The path of the current user's FOLDERID_ApplicationShortcuts directory.
*/
[[nodiscard]] std::filesystem::path GetWindowsAppShortcutsPath();
#else
/**

View File

@@ -515,7 +515,7 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
case GameListItemType::Game:
AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
selected.data(GameListItemPath::FullPathRole).toString().toStdString());
selected.data(GameListItemPath::FullPathRole).toString());
break;
case GameListItemType::CustomDir:
AddPermDirPopup(context_menu, selected);
@@ -535,7 +535,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
}
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const QString& qpath) {
const std::string path = qpath.toStdString();
QAction* favorite = context_menu.addAction(tr("Favorite"));
context_menu.addSeparator();
QAction* start_game = context_menu.addAction(tr("Start Game"));
@@ -569,7 +570,6 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
#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"));
@@ -644,14 +644,12 @@ 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);
});
connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);
connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, qpath]() {
emit CreateShortcut(program_id, qpath, GameListShortcutTarget::Desktop);
});
#ifndef WIN32
connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);
connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, qpath]() {
emit CreateShortcut(program_id, qpath, GameListShortcutTarget::Applications);
});
#endif
connect(properties, &QAction::triggered,
[this, path]() { emit OpenPerGameGeneralRequested(path); });
};

View File

@@ -120,8 +120,7 @@ signals:
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void VerifyIntegrityRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id);
void CreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target);
void CreateShortcut(u64 program_id, const QString& game_path, GameListShortcutTarget target);
void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const std::string& file);
@@ -150,7 +149,7 @@ private:
void RemoveFavorite(u64 program_id);
void PopupContextMenu(const QPoint& menu_location);
void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
void AddGamePopup(QMenu& context_menu, u64 program_id, const QString& path);
void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
void AddFavoritesPopup(QMenu& context_menu);

View File

@@ -172,7 +172,9 @@ Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
#ifdef _WIN32
#include <shobjidl.h>
#include <windows.h>
extern "C" {
// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable
// graphics
@@ -2837,8 +2839,155 @@ 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,
#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
bool GMainWindow::SaveShortcutLink(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) {
bool shortcut_success = false;
try {
// Plus 'Type' is required
if (name.empty()) {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Name is empty");
return shortcut_success;
}
std::ofstream shortcut_stream(shortcut_path, std::ios::binary | std::ios::trunc);
if (shortcut_stream.is_open()) {
shortcut_stream << "[Desktop Entry]" << std::endl;
shortcut_stream << "Type=Application" << std::endl;
shortcut_stream << "Version=1.0" << std::endl;
shortcut_stream << "Name=" << name << std::endl;
if (!comment.empty()) {
shortcut_stream << "Comment=" << comment << std::endl;
}
if (std::filesystem::is_regular_file(icon_path)) {
shortcut_stream << "Icon=" << icon_path.string() << std::endl;
}
shortcut_stream << "TryExec=" << command.string() << std::endl;
shortcut_stream << "Exec=" << command.string();
if (!arguments.empty()) {
shortcut_stream << " " << arguments;
}
shortcut_stream << std::endl;
if (!categories.empty()) {
shortcut_stream << "Categories=" << categories << std::endl;
}
if (!keywords.empty()) {
shortcut_stream << "Keywords=" << keywords << std::endl;
}
if (std::filesystem::is_regular_file(shortcut_path)) {
LOG_INFO(Common, "[GMainWindow::SaveShortcutLink] Shortcut created");
shortcut_success = true;
} else {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Shortcut created but icon dont "
"exists, please check if the icon path is correct");
}
} else {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Failed to create shortcut");
}
shortcut_stream.close();
} catch (const std::exception& e) {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Failed to create shortcut: {}",
e.what());
}
return shortcut_success;
}
#elif defined(_WIN32)
bool GMainWindow::SaveShortcutLink(const std::filesystem::path& shortcut_path, const auto& comment,
const std::filesystem::path& icon_path,
const std::filesystem::path& command, const auto& arguments,
const auto& categories, const auto& keywords, const auto& name) {
// Initialize COM
CoInitialize(NULL);
IShellLinkW* pShellLink;
IPersistFile* pPersistFile;
auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW,
(void**)&pShellLink);
bool shortcut_success = false;
if (!FAILED(hres)) {
auto wcommand = Common::FS::ToWString(command.u8string());
if (!wcommand.empty())
pShellLink->SetPath(wcommand.c_str());
auto warguments = Common::FS::ToWString(arguments);
if (!warguments.empty())
pShellLink->SetArguments(warguments.c_str());
auto wcomment = Common::FS::ToWString(comment);
if (!wcomment.empty())
pShellLink->SetDescription(wcomment.c_str());
if (std::filesystem::exists(icon_path) && std::filesystem::is_regular_file(icon_path))
pShellLink->SetIconLocation(Common::FS::ToWString(icon_path.u8string()).c_str(), 0);
hres = pShellLink->QueryInterface(IID_IPersistFile, (void**)&pPersistFile);
if (FAILED(hres)) {
LOG_ERROR(Common,
"[GMainWindow::SaveShortcutLink] Failed to create IPersistFile instance");
} else {
hres =
pPersistFile->Save(Common::FS::ToWString(shortcut_path.u8string()).c_str(), TRUE);
if (FAILED(hres)) {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Failed to save shortcut");
} else {
if (std::filesystem::is_regular_file(shortcut_path)) {
LOG_INFO(Common, "[GMainWindow::SaveShortcutLink] Shortcut created");
shortcut_success = true;
} else {
LOG_ERROR(Common,
"[GMainWindow::SaveShortcutLink] Shortcut created but icon dont "
"exists, please check if the icon path is correct");
}
}
pPersistFile->Release();
}
} else {
LOG_ERROR(Common, "[GMainWindow::SaveShortcutLink] Failed to create IShellLinkW instance");
}
pShellLink->Release();
CoUninitialize();
return shortcut_success;
}
#else
bool GMainWindow::SaveShortcutLink(const std::filesystem::path& shortcut_path_, const auto& comment,
const std::filesystem::path& icon_path_,
const std::filesystem::path& command_, const auto& arguments,
const auto& categories, const auto& keywords, const auto& name) {
return false;
}
#endif
void GMainWindow::OnGameListCreateShortcut(u64 program_id, const QString& game_path_q,
GameListShortcutTarget target) {
const std::string game_path = game_path_q.toStdString();
// Get path to yuzu executable
const QStringList args = QApplication::arguments();
std::filesystem::path yuzu_command = args[0].toStdString();
@@ -2867,6 +3016,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
#endif // __linux__
std::filesystem::path target_directory{};
// Determine target directory for shortcut
#if defined(WIN32)
const char* home = std::getenv("USERPROFILE");
@@ -2920,12 +3070,16 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
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)));
#elif defined(_WIN32)
const std::filesystem::path IconYuzuPath =
Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir);
std::u8string u8game_ico = Common::FS::UTF8FilenameSantizer(
Common::FS::ToU8String((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name)
: fmt::format("yuzu-{:016X}.ico", program_id))));
const std::filesystem::path icon_path = IconYuzuPath / (u8game_ico);
#else
std::string icon_extension;
#endif
@@ -2955,46 +3109,112 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
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");
} 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");
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, '_');
const auto name = title;
#elif defined(_WIN32)
// ICO is only for Windows
// Convert QImage to QPixmap
QPixmap pixmap = QPixmap::fromImage(icon_jpeg);
// Save the QPixmap as an .ico file
QList<QPixmap> pixmaps;
pixmaps.append(pixmap);
if (!savePixmapsToICO(pixmaps,
QString::fromStdWString(Common::FS::ToWString(icon_path.u8string())))) {
LOG_ERROR(Frontend, "Could not write icon as ICO to file");
} else {
LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string());
}
const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str();
#endif
#if defined(__linux__) || defined(__FreeBSD__)
QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
int result = QMessageBox::information(
this, tr("Create Shortcut"), tr("Do you want to launch the game in fullscreen?"), buttons);
std::string arguments;
if (result == QMessageBox::Yes) {
arguments = fmt::format("-f -g \"{:s}\"", game_path);
} else {
arguments = fmt::format("-g \"{:s}\"", game_path);
}
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())));
return;
const std::string keywords = u8"Switch;Nintendo;";
const std::string categories = u8"Game;Emulator;Qt;";
#elif defined(_WIN32)
QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
int result = QMessageBox::information(
this, tr("Create Shortcut"), tr("Do you want to launch the game in fullscreen?"), buttons);
const auto file_path =
std::filesystem::path{Common::U16StringFromBuffer(game_path_q.utf16(), game_path_q.size())};
std::u8string arguments = u8"-g \"" + std::filesystem::path(file_path).u8string() + u8"\"";
if (result == QMessageBox::Yes) {
arguments = u8"-f " + arguments;
}
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)));
auto qtcomment = tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title));
const std::u8string comment = Common::FS::ToU8String(qtcomment.toStdString());
std::u8string title_u8 = Common::FS::ToU8String(title);
title_u8 = Common::FS::UTF8FilenameSantizer(title_u8);
if (target == GameListShortcutTarget::Desktop) {
target_directory = Common::FS::GetWindowsDesktopPath();
} else {
target_directory = Common::FS::GetWindowsAppShortcutsPath();
}
const std::filesystem::path sanitized_title = title_u8 + u8".lnk";
const std::filesystem::path shortcut_path = target_directory / (sanitized_title);
const std::u8string keywords = u8"Switch;Nintendo;";
const std::u8string categories = u8"Game;Emulator;Qt;";
const auto name = title_u8;
#else
const std::string comment{};
const std::string arguments{};
const std::string categories{};
const std::string keywords{};
const auto name = title;
#endif
if (GMainWindow::SaveShortcutLink(shortcut_path, comment, icon_path, yuzu_command, arguments,
categories, keywords, name)) {
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)));
} else {
#if defined(_WIN32)
if (GameListShortcutTarget::Applications == target) {
QMessageBox::critical(this, tr("Create Shortcut"),
tr("Failed to create a shortcut at %1, check your admin rights")
.arg(QString::fromStdString(title)));
return;
}
#endif // _WIN32
QMessageBox::critical(
this, tr("Create Shortcut"),
tr("Failed to create a shortcut at %1").arg(QString::fromStdString(title)));
}
}
void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
@@ -3976,66 +4196,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;

View File

@@ -6,6 +6,9 @@
#include <memory>
#include <optional>
#include <filesystem>
#include <iostream>
#include <QMainWindow>
#include <QTimer>
#include <QTranslator>
@@ -333,7 +336,7 @@ 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,
void OnGameListCreateShortcut(u64 program_id, const QString& game_path,
GameListShortcutTarget target);
void OnGameListOpenDirectory(const QString& directory);
void OnGameListAddDirectory();
@@ -425,10 +428,11 @@ private:
void ConfigureFilesystemProvider(const std::string& filepath);
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 SaveShortcutLink(const std::filesystem::path& shortcut_path, const auto& comment,
const std::filesystem::path& icon_path,
const std::filesystem::path& command, const auto& arguments,
const auto& categories, const auto& keywords, const auto& name);
std::unique_ptr<Ui::MainWindow> ui;

View File

@@ -3,12 +3,18 @@
#include <array>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <QFile>
#include <QFileInfo>
#include <QImage>
#include <QList>
#include <QPainter>
#include <QPixmap>
#include <QString>
#include <QTemporaryFile>
#include "yuzu/util/util.h"
#ifdef _WIN32
#include <windows.h>
#include "common/fs/file.h"
#endif
QFont GetMonospaceFont() {
QFont font(QStringLiteral("monospace"));
@@ -42,75 +48,84 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) {
return circle_pixmap;
}
bool SaveIconToFile(const std::string_view path, const QImage& image) {
#if defined(WIN32)
#pragma pack(push, 2)
struct IconDir {
WORD id_reserved;
WORD id_type;
WORD id_count;
};
template <typename T>
void write(QFile& f, const T t) {
f.write((const char*)&t, sizeof(t));
}
struct IconDirEntry {
BYTE width;
BYTE height;
BYTE color_count;
BYTE reserved;
WORD planes;
WORD bit_count;
DWORD bytes_in_res;
DWORD image_offset;
};
#pragma pack(pop)
bool savePixmapsToICO(const QList<QPixmap>& pixmaps, const QString& path) {
static_assert(sizeof(short) == 2, "short int is not 2 bytes");
static_assert(sizeof(int) == 4, "int is not 4 bytes");
QImage source_image = image.convertToFormat(QImage::Format_RGB32);
constexpr int bytes_per_pixel = 4;
const int image_size = source_image.width() * source_image.height() * bytes_per_pixel;
BITMAPINFOHEADER info_header{};
info_header.biSize = sizeof(BITMAPINFOHEADER), info_header.biWidth = source_image.width(),
info_header.biHeight = source_image.height() * 2, info_header.biPlanes = 1,
info_header.biBitCount = bytes_per_pixel * 8, info_header.biCompression = BI_RGB;
const IconDir icon_dir{.id_reserved = 0, .id_type = 1, .id_count = 1};
const IconDirEntry icon_entry{.width = static_cast<BYTE>(source_image.width()),
.height = static_cast<BYTE>(source_image.height() * 2),
.color_count = 0,
.reserved = 0,
.planes = 1,
.bit_count = bytes_per_pixel * 8,
.bytes_in_res =
static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size),
.image_offset = sizeof(IconDir) + sizeof(IconDirEntry)};
Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write,
Common::FS::FileType::BinaryFile);
if (!icon_file.IsOpen()) {
QFile f(path);
if (!f.open(QFile::OpenModeFlag::WriteOnly))
return false;
}
if (!icon_file.Write(icon_dir)) {
return false;
}
if (!icon_file.Write(icon_entry)) {
return false;
}
if (!icon_file.Write(info_header)) {
return false;
}
// Header
write<short>(f, 0);
write<short>(f, 1);
write<short>(f, pixmaps.count());
for (int y = 0; y < image.height(); y++) {
const auto* line = source_image.scanLine(source_image.height() - 1 - y);
std::vector<u8> line_data(source_image.width() * bytes_per_pixel);
std::memcpy(line_data.data(), line, line_data.size());
if (!icon_file.Write(line_data)) {
QList<int> images_size;
for (int ii = 0; ii < pixmaps.count(); ++ii) {
QTemporaryFile temp;
temp.setAutoRemove(true);
if (!temp.open())
return false;
}
const auto& pixmap = pixmaps[ii];
pixmap.save(&temp, "PNG");
temp.close();
images_size.push_back(QFileInfo(temp).size());
}
icon_file.Close();
// Images directory
constexpr unsigned int entry_size = sizeof(char) + sizeof(char) + sizeof(char) + sizeof(char) +
sizeof(short) + sizeof(short) + sizeof(unsigned int) +
sizeof(unsigned int);
static_assert(entry_size == 16, "wrong entry size");
unsigned int offset = 3 * sizeof(short) + pixmaps.count() * entry_size;
for (int ii = 0; ii < pixmaps.count(); ++ii) {
const auto& pixmap = pixmaps[ii];
if (pixmap.width() > 256 || pixmap.height() > 256)
continue;
write<char>(f, pixmap.width() == 256 ? 0 : pixmap.width());
write<char>(f, pixmap.height() == 256 ? 0 : pixmap.height());
write<char>(f, 0); // palette size
write<char>(f, 0); // reserved
write<short>(f, 1); // color planes
write<short>(f, pixmap.depth()); // bits-per-pixel
write<unsigned int>(f, images_size[ii]); // size of image in bytes
write<unsigned int>(f, offset); // offset
offset += images_size[ii];
}
for (int ii = 0; ii < pixmaps.count(); ++ii) {
const auto& pixmap = pixmaps[ii];
if (pixmap.width() > 256 || pixmap.height() > 256)
continue;
pixmap.save(&f, "PNG");
}
// Close the file before renaming it
f.close();
// Remove the .png extension Add the .ico extension and Rename the file
QString qPath = path;
qPath.chop(3);
qPath = qPath % QString::fromStdString("ico");
QFile::rename(path, qPath);
return true;
#else
return false;
#endif
}
#else
bool SaveAsIco(const QList<QPixmap>& pixmaps, const QString& path) {
return false;
}
#endif

View File

@@ -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 savePixmapsToICO(const QList<QPixmap>& pixmaps, const QString& path);

View File

@@ -22,6 +22,7 @@
"boost-test",
"boost-timer",
"boost-variant",
"boost-locale",
"fmt",
"lz4",
"nlohmann-json",