Skip to content

Commit 8026e90

Browse files
committed
Qt: Add Create game shortcut functionality
1 parent dc9e531 commit 8026e90

File tree

12 files changed

+725
-0
lines changed

12 files changed

+725
-0
lines changed

bin/resources/icons/AppIconLarge.ico

295 KB
Binary file not shown.

common/HostSys.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,10 @@ namespace Common
214214
void SetMousePosition(int x, int y);
215215
bool AttachMousePositionCb(std::function<void(int,int)> cb);
216216
void DetachMousePositionCb();
217+
218+
#if !defined(__APPLE__)
219+
// Create desktop shortcut for games
220+
void CreateShortcut(const std::string name, const std::string game_path, const std::string passed_cli_args, bool is_desktop);
221+
#endif
222+
217223
} // namespace Common

common/Linux/LnxMisc.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "common/Pcsx2Types.h"
55
#include "common/Console.h"
6+
#include "common/FileSystem.h"
67
#include "common/HostSys.h"
78
#include "common/Path.h"
89
#include "common/ScopedGuard.h"
@@ -11,6 +12,7 @@
1112
#include "common/Threading.h"
1213
#include "common/WindowInfo.h"
1314

15+
#include "pcsx2/Host.h"
1416
#include "fmt/format.h"
1517

1618
#include <dbus/dbus.h>
@@ -366,6 +368,112 @@ bool Common::PlaySoundAsync(const char* path)
366368
#endif
367369
}
368370

371+
void Common::CreateShortcut(const std::string name, const std::string game_path, const std::string passed_cli_args, bool is_desktop)
372+
{
373+
if (name.empty())
374+
{
375+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Failed to create shortcut"), TRANSLATE_SV("LnxMisc", "Cannot create shortcut without a name."));
376+
return;
377+
}
378+
379+
// Sanitize filename and game path
380+
const std::string clean_name = Path::SanitizeFileName(name);
381+
const std::string clean_path = Path::Canonicalize(Path::RealPath(game_path));
382+
if (!Path::IsValidFileName(clean_name))
383+
{
384+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Failed to create shortcut"), TRANSLATE_SV("LnxMisc", "Filename contains illegal character."));
385+
return;
386+
}
387+
388+
// Find the executable path
389+
const std::string executable_path = FileSystem::GetPackagePath();
390+
if (executable_path.empty())
391+
{
392+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Failed to create shortcut"), TRANSLATE_SV("LnxMisc", "Executable path is empty."));
393+
return;
394+
}
395+
396+
// Find home directory
397+
std::string link_path;
398+
if (const char* home = getenv("HOME"))
399+
{
400+
if (is_desktop)
401+
{
402+
if (const char* xdg_desktop_dir = getenv("XDG_DESKTOP_DIR"))
403+
{
404+
link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name);
405+
}
406+
else
407+
{
408+
link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name);
409+
}
410+
}
411+
else
412+
{
413+
if (const char* xdg_data_home = getenv("XDG_DATA_HOME"))
414+
{
415+
link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name);
416+
}
417+
else
418+
{
419+
link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name);
420+
}
421+
}
422+
}
423+
else
424+
{
425+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Failed to create shortcut"), TRANSLATE_SV("LnxMisc", "Home path is empty."));
426+
return;
427+
}
428+
429+
// Checks if a shortcut already exist
430+
if (FileSystem::FileExists(link_path.c_str()))
431+
{
432+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Failed to create shortcut"), TRANSLATE_SV("LnxMisc", "A shortcut with the same name already exist."));
433+
return;
434+
}
435+
436+
const std::string final_args = fmt::format(" {} -- '{}'", StringUtil::StripWhitespace(passed_cli_args), clean_path);
437+
std::string clean_args;
438+
439+
for (size_t i = 0; i < final_args.size(); i++)
440+
{
441+
if (final_args[i] == '\n')
442+
clean_args.push_back(' ');
443+
else
444+
clean_args.push_back(final_args[i]);
445+
}
446+
447+
// Shortcut content
448+
Console.WriteLnFmt("Creating a shortcut for '{}' with arguments '{}'", name, passed_cli_args);
449+
std::string file_content =
450+
"[Desktop Entry]\n"
451+
"Encoding=UTF-8\n"
452+
"Version=1.0\n"
453+
"Type=Application\n"
454+
"Terminal=false\n"
455+
"StartupWMClass=PCSX2\n"
456+
"Exec=\'" + executable_path + "'" + clean_args + "\n"
457+
"Name=" + name + "\n"
458+
"Icon=PCSX2\n"
459+
"Categories=Game;Emulator;\n";
460+
std::string_view sv(file_content);
461+
462+
// Write to .desktop file
463+
if (!FileSystem::WriteStringToFile(link_path.c_str(), sv))
464+
{
465+
Host::ReportErrorAsync(TRANSLATE_SV("LnxMisc", "Error"), TRANSLATE_SV("LnxMisc", "Failed to create .desktop file"));
466+
return;
467+
}
468+
469+
Console.WriteLnFmt(Color_StrongGreen, "{} shortcut for {} has been created succesfully.", is_desktop ? "Desktop" : "Start Menu", clean_name);
470+
471+
if (chmod(link_path.c_str(), S_IRWXU) != 0) // enables user to execute file
472+
{
473+
Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno);
474+
}
475+
}
476+
369477
void Threading::Sleep(int ms)
370478
{
371479
usleep(1000 * ms);

common/Windows/WinMisc.cpp

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@
44
#include "common/Console.h"
55
#include "common/FileSystem.h"
66
#include "common/HostSys.h"
7+
#include "common/Path.h"
78
#include "common/RedtapeWindows.h"
89
#include "common/StringUtil.h"
910
#include "common/Threading.h"
1011
#include "common/WindowInfo.h"
1112

13+
#include "pcsx2/Host.h"
14+
#include "fmt/format.h"
15+
16+
#include <Windows.h>
17+
#include <shlobj.h>
18+
#include <winnls.h>
19+
#include <shobjidl.h>
20+
#include <objbase.h>
21+
#include <objidl.h>
22+
#include <shlguid.h>
23+
#include <comdef.h>
1224
#include <mmsystem.h>
1325
#include <timeapi.h>
1426
#include <VersionHelpers.h>
1527

28+
#include <wrl/client.h>
29+
1630
// If anything tries to read this as an initializer, we're in trouble.
1731
static const LARGE_INTEGER lfreq = []() {
1832
LARGE_INTEGER ret = {};
@@ -156,6 +170,150 @@ bool Common::PlaySoundAsync(const char* path)
156170
return PlaySoundW(wpath.c_str(), NULL, SND_ASYNC | SND_NODEFAULT);
157171
}
158172

173+
void Common::CreateShortcut(const std::string name, const std::string game_path, const std::string passed_cli_args, bool is_desktop)
174+
{
175+
if (name.empty())
176+
{
177+
Console.Error("Cannot create shortcuts without a name.");
178+
return;
179+
}
180+
181+
// Sanitize filename
182+
const std::string clean_name = Path::SanitizeFileName(name).c_str();
183+
if (!Path::IsValidFileName(clean_name))
184+
{
185+
Host::ReportErrorAsync(TRANSLATE_SV("WinMisc", "Failed to create shortcut"), TRANSLATE_SV("WinMisc", "Filename contains illegal character."));
186+
return;
187+
}
188+
189+
// Locate home directory
190+
std::string link_file;
191+
if (const char* home = getenv("USERPROFILE"))
192+
{
193+
if (is_desktop)
194+
{
195+
link_file = Path::ToNativePath(fmt::format("{}/Desktop/{}.lnk", home, clean_name));
196+
}
197+
else
198+
{
199+
const std::string start_menu_dir = Path::ToNativePath(fmt::format("{}/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/PCSX2", home));
200+
if (!FileSystem::EnsureDirectoryExists(start_menu_dir.c_str(), false))
201+
{
202+
Host::ReportErrorAsync(TRANSLATE_SV("WinMisc", "Failed to create shortcut"), TRANSLATE_SV("WinMisc", "Could not create start menu directory."));
203+
return;
204+
}
205+
206+
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", start_menu_dir, clean_name));
207+
}
208+
}
209+
else
210+
{
211+
Host::ReportErrorAsync(TRANSLATE_SV("WinMisc", "Failed to create shortcut"), TRANSLATE_SV("WinMisc", "Home path is empty."));
212+
return;
213+
}
214+
215+
// Check if the same shortcut already exists
216+
if (FileSystem::FileExists(link_file.c_str()))
217+
{
218+
Host::ReportErrorAsync(TRANSLATE_SV("WinMisc", "Failed to create shortcut"), TRANSLATE_SV("WinMisc", "A shortcut with the same name already exist."));
219+
return;
220+
}
221+
222+
const std::string final_args = fmt::format(" {} -- \"{}\"", StringUtil::StripWhitespace(passed_cli_args), game_path);
223+
Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args);
224+
const auto str_error = [](HRESULT hr) -> std::string {
225+
_com_error err(hr);
226+
const TCHAR* errMsg = err.ErrorMessage();
227+
return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr);
228+
};
229+
230+
// Construct the shortcut
231+
// https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32
232+
HRESULT res = CoInitialize(NULL);
233+
if (FAILED(res))
234+
{
235+
Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res));
236+
return;
237+
}
238+
239+
Microsoft::WRL::ComPtr<IShellLink> pShellLink;
240+
Microsoft::WRL::ComPtr<IPersistFile> pPersistFile;
241+
242+
const auto cleanup = [&](bool return_value, const std::string& fail_reason) -> bool {
243+
if (!return_value)
244+
Console.ErrorFmt("Failed to create shortcut: {}", fail_reason);
245+
CoUninitialize();
246+
return return_value;
247+
};
248+
249+
res = CoCreateInstance(__uuidof(ShellLink), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
250+
if (FAILED(res))
251+
{
252+
cleanup(false, "CoCreateInstance failed");
253+
return;
254+
}
255+
256+
// Set path to the executable
257+
const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath());
258+
res = pShellLink->SetPath(target_file.c_str());
259+
if (FAILED(res))
260+
{
261+
cleanup(false, fmt::format("SetPath failed ({})", str_error(res)));
262+
return;
263+
}
264+
265+
// Set the working directory
266+
const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory());
267+
res = pShellLink->SetWorkingDirectory(working_dir.c_str());
268+
if (FAILED(res))
269+
{
270+
cleanup(false, fmt::format("SetWorkingDirectory failed ({})", str_error(res)));
271+
return;
272+
}
273+
274+
// Set the launch arguments
275+
if (!final_args.empty())
276+
{
277+
const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args);
278+
res = pShellLink->SetArguments(target_cli_args.c_str());
279+
if (FAILED(res))
280+
{
281+
cleanup(false, fmt::format("SetArguments failed ({})", str_error(res)));
282+
return;
283+
}
284+
}
285+
286+
// Set the icon
287+
std::string icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico"));
288+
const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(icon_path);
289+
res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0);
290+
if (FAILED(res))
291+
{
292+
cleanup(false, fmt::format("SetIconLocation failed ({})", str_error(res)));
293+
return;
294+
}
295+
296+
// Use the IPersistFile object to save the shell link
297+
res = pShellLink.As(&pPersistFile);
298+
if (FAILED(res))
299+
{
300+
cleanup(false, fmt::format("QueryInterface failed ({})", str_error(res)));
301+
return;
302+
}
303+
304+
// Save shortcut link to disk
305+
const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file);
306+
res = pPersistFile->Save(w_link_file.c_str(), TRUE);
307+
if (FAILED(res))
308+
{
309+
cleanup(false, fmt::format("Failed to save the shortcut ({})", str_error(res)));
310+
return;
311+
}
312+
313+
Console.WriteLnFmt(Color_StrongGreen, "{} shortcut for {} has been created succesfully.", is_desktop ? "Desktop" : "Start Menu", clean_name);
314+
cleanup(true, {});
315+
}
316+
159317
void Threading::Sleep(int ms)
160318
{
161319
::Sleep(ms);

pcsx2-qt/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ target_sources(pcsx2-qt PRIVATE
264264
resources/resources.qrc
265265
)
266266

267+
if (NOT APPLE)
268+
target_sources(pcsx2-qt PRIVATE
269+
ShortcutCreationDialog.cpp
270+
ShortcutCreationDialog.h
271+
ShortcutCreationDialog.ui
272+
)
273+
endif()
274+
267275
file(GLOB TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Translations/*.ts)
268276

269277
target_precompile_headers(pcsx2-qt PRIVATE PrecompiledHeader.h)

pcsx2-qt/MainWindow.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
#include "Tools/InputRecording/InputRecordingViewer.h"
2222
#include "Tools/InputRecording/NewInputRecordingDlg.h"
2323

24+
#if !defined(__APPLE__)
25+
#include "ShortcutCreationDialog.h"
26+
#endif
27+
2428
#include "pcsx2/Achievements.h"
29+
#include "common/HostSys.h"
2530
#include "pcsx2/CDVD/CDVDcommon.h"
2631
#include "pcsx2/CDVD/CDVDdiscReader.h"
2732
#include "pcsx2/GS.h"
@@ -1448,6 +1453,10 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
14481453
action = menu.addAction(tr("Set Cover Image..."));
14491454
connect(action, &QAction::triggered, [this, entry]() { setGameListEntryCoverImage(entry); });
14501455

1456+
#if !defined(__APPLE__)
1457+
connect(menu.addAction(tr("Create Game Shortcut")), &QAction::triggered, [this]() { MainWindow::onCreateGameShortcutTriggered(); });
1458+
#endif
1459+
14511460
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
14521461
[this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
14531462

@@ -1751,6 +1760,17 @@ void MainWindow::onToolsCoverDownloaderTriggered()
17511760
dlg.exec();
17521761
}
17531762

1763+
#if !defined(__APPLE__)
1764+
void MainWindow::onCreateGameShortcutTriggered()
1765+
{
1766+
const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
1767+
const QString title = QString::fromStdString(entry->GetTitle());
1768+
const QString path = QString::fromStdString(entry->path);
1769+
VMLock lock(pauseAndLockVM());
1770+
ShortcutCreationDialog dlg(lock.getDialogParent(), title, path);
1771+
dlg.exec();
1772+
}
1773+
#endif
17541774
void MainWindow::onToolsEditCheatsPatchesTriggered(bool cheats)
17551775
{
17561776
if (s_current_disc_serial.isEmpty() || s_current_running_crc == 0)

pcsx2-qt/MainWindow.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ private Q_SLOTS:
170170
void onAboutActionTriggered();
171171
void onToolsOpenDataDirectoryTriggered();
172172
void onToolsCoverDownloaderTriggered();
173+
#if !defined(__APPLE__)
174+
void onCreateGameShortcutTriggered();
175+
#endif
173176
void onToolsEditCheatsPatchesTriggered(bool cheats);
174177
void onCreateMemoryCardOpenRequested();
175178
void updateTheme();

0 commit comments

Comments
 (0)