diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index 619a036b32..10e9473258 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -273,4 +273,4 @@ mengyuanchen testhost #Tools -OIP +OIP \ No newline at end of file diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 6a2041ce18..1e8b3d91b5 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -8,6 +8,7 @@ Acceleratorkeys ACCEPTFILES ACCESSDENIED ACCESSTOKEN +acfs AClient AColumn acrt @@ -523,6 +524,7 @@ FZE gacutil Gaeilge Gaidhlig +gameid GC'ed GCLP gdi @@ -712,6 +714,7 @@ INPUTSINK INPUTTYPE INSTALLDESKTOPSHORTCUT INSTALLDIR +installdir INSTALLFOLDER INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER INSTALLFOLDERTOPREVIOUSINSTALLFOLDER @@ -1569,6 +1572,7 @@ stdcpp stdcpplatest STDMETHODCALLTYPE STDMETHODIMP +steamapps STGC STGM STGMEDIUM @@ -1969,4 +1973,4 @@ zoomit ZOOMITX ZXk ZXNs -zzz +zzz \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp index 4d4b47ea12..7075da2ea7 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp @@ -121,6 +121,22 @@ namespace AppLauncher // packaged apps: try launching first by AppUserModel.ID // usage example: elevated Terminal if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty()) + { + Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName); + auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated); + if (res.isOk()) + { + launched = true; + } + else + { + launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() }); + } + } + + // win32 app with appUserModelId: + // usage example: steam games + if (!launched && !app.appUserModelId.empty()) { Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId); auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated); diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp index 19b33214b7..a37d82f8ca 100644 --- a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp +++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "AppUtils.h" +#include "SteamHelper.h" #include #include @@ -34,6 +35,8 @@ namespace Utils constexpr const wchar_t* EdgeFilename = L"msedge.exe"; constexpr const wchar_t* ChromeFilename = L"chrome.exe"; + + constexpr const wchar_t* SteamUrlProtocol = L"steam:"; } AppList IterateAppsFolder() @@ -138,6 +141,34 @@ namespace Utils else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp) { data.installPath = propVariantString.m_pData; + + if (!data.installPath.empty()) + { + const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0; + + if (isSteamProtocol) + { + Logger::info(L"Found steam game: protocol path: {}", data.installPath); + data.protocolPath = data.installPath; + + try + { + auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath); + auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId); + + if (gameFolder) + { + data.installPath = gameFolder->gameInstallationPath; + Logger::info(L"Found steam game: physical path: {}", data.installPath); + } + } + catch (std::exception ex) + { + Logger::error(L"Failed to get installPath for game {}", data.installPath); + Logger::error("Error: {}", ex.what()); + } + } + } } } @@ -397,5 +428,10 @@ namespace Utils { return installPath.ends_with(NonLocalizable::ChromeFilename); } + + bool AppData::IsSteamGame() const + { + return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0; + } } } \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.h b/src/modules/Workspaces/WorkspacesLib/AppUtils.h index 3c81049f83..80b5e2fd49 100644 --- a/src/modules/Workspaces/WorkspacesLib/AppUtils.h +++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.h @@ -13,10 +13,12 @@ namespace Utils std::wstring packageFullName; std::wstring appUserModelId; std::wstring pwaAppId; + std::wstring protocolPath; bool canLaunchElevated = false; bool IsEdge() const; bool IsChrome() const; + bool IsSteamGame() const; }; using AppList = std::vector; diff --git a/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp new file mode 100644 index 0000000000..404002e284 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp @@ -0,0 +1,171 @@ +#include "pch.h" +#include "SteamHelper.h" +#include +#include +#include +#include +#include +#include + +namespace Utils +{ + + static std::wstring Utf8ToWide(const std::string& utf8) + { + if (utf8.empty()) + return L""; + + int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + if (size <= 0) + return L""; + + std::wstring wide(size, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), wide.data(), size); + return wide; + } + + namespace Steam + { + using namespace std; + namespace fs = std::filesystem; + + static std::optional GetSteamExePathFromRegistry() + { + static std::optional cachedPath; + if (cachedPath.has_value()) + { + return cachedPath; + } + + const std::vector roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS }; + const std::vector subKeys = { + L"steam\\shell\\open\\command", + L"Software\\Classes\\steam\\shell\\open\\command", + }; + + for (HKEY root : roots) + { + for (const auto& subKey : subKeys) + { + HKEY hKey; + if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS) + { + wchar_t value[512]; + DWORD size = sizeof(value); + DWORD type = 0; + + if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast(value), &size) == ERROR_SUCCESS && + (type == REG_SZ || type == REG_EXPAND_SZ)) + { + std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim"); + std::wcmatch match; + if (std::regex_search(value, match, exeRegex) && match.size() > 1) + { + RegCloseKey(hKey); + cachedPath = match[1].str(); + return cachedPath; + } + } + + RegCloseKey(hKey); + } + } + } + + cachedPath = std::nullopt; + return std::nullopt; + } + + static fs::path GetSteamBasePath() + { + auto steamFolderOpt = GetSteamExePathFromRegistry(); + if (!steamFolderOpt) + { + return {}; + } + + return fs::path(*steamFolderOpt).parent_path() / L"steamapps"; + } + + static fs::path GetAcfFilePath(const std::wstring& gameId) + { + auto steamFolderOpt = GetSteamExePathFromRegistry(); + if (!steamFolderOpt) + { + return {}; + } + + return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf"); + } + + static fs::path GetGameInstallPath(const std::wstring& gameFolderName) + { + auto steamFolderOpt = GetSteamExePathFromRegistry(); + if (!steamFolderOpt) + { + return {}; + } + + return GetSteamBasePath() / L"common" / gameFolderName; + } + + static unordered_map ParseAcfFile(const fs::path& acfPath) + { + unordered_map result; + + ifstream file(acfPath); + if (!file.is_open()) + return result; + + string line; + while (getline(file, line)) + { + smatch matches; + static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim"); + + if (regex_search(line, matches, pattern) && matches.size() == 3) + { + wstring key = Utf8ToWide(matches[1].str()); + wstring value = Utf8ToWide(matches[2].str()); + result[key] = value; + } + } + + return result; + } + + std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId) + { + fs::path acfPath = Steam::GetAcfFilePath(gameId); + + if (!fs::exists(acfPath)) + return nullptr; + + auto kv = ParseAcfFile(acfPath); + if (kv.empty() || kv.find(L"installdir") == kv.end()) + return nullptr; + + fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]); + if (!fs::exists(gamePath)) + return nullptr; + + auto game = std::make_unique(); + game->gameId = gameId; + game->gameInstallationPath = gamePath.wstring(); + return game; + } + + std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath) + { + const std::wstring steamGamePrefix = L"steam://rungameid/"; + + if (urlPath.rfind(steamGamePrefix, 0) == 0) + { + return urlPath.substr(steamGamePrefix.length()); + } + + return L""; + } + + } +} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/SteamHelper.h b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h new file mode 100644 index 0000000000..a80a942f4a --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h @@ -0,0 +1,24 @@ +#pragma once + +#include "pch.h" + +namespace Utils +{ + namespace NonLocalizable + { + const std::wstring AcfFileNameTemplate = L"appmanifest_.acfs"; + } + + namespace Steam + { + struct SteamGame + { + std::wstring gameId; + std::wstring gameInstallationPath; + }; + + std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId); + + std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath); + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj index 27394e29ee..7d29741a0d 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj @@ -41,6 +41,7 @@ + @@ -57,6 +58,7 @@ Create + diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters index b066c16a57..f4f17c55ee 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters @@ -53,6 +53,9 @@ Header Files + + Header Files + @@ -88,6 +91,9 @@ Source Files + + Source Files + diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp index 1d5bc8a179..a8b7c13108 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp @@ -71,6 +71,8 @@ namespace SnapshotUtils continue; } + Logger::info("Try to get window app:{}", reinterpret_cast(window)); + DWORD pid{}; GetWindowThreadProcessId(window, &pid); @@ -118,10 +120,19 @@ namespace SnapshotUtils auto data = Utils::Apps::GetApp(processPath, pid, installedApps); if (!data.has_value() || data->name.empty()) { - Logger::info(L"Installed app not found: {}", processPath); + Logger::info(L"Installed app not found:{},{}", reinterpret_cast(window), processPath); continue; } + if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window)) + { + // Only care about steam games if it has no thick frame to remain consistent with + // the behavior as before. + continue; + } + + Logger::info(L"Found app for window:{},{}", reinterpret_cast(window), processPath); + auto appData = data.value(); bool isEdge = appData.IsEdge(); diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp index 7b04135d1a..538579979f 100644 --- a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp @@ -200,6 +200,14 @@ std::optional WindowArranger::GetNearestWindow(const Workspa } auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps); + + if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window)) + { + // Only care about steam games if it has no thick frame to remain consistent with + // the behavior as before. + continue; + } + if (!data.has_value()) { continue; diff --git a/src/modules/Workspaces/workspaces-common/WindowFilter.h b/src/modules/Workspaces/workspaces-common/WindowFilter.h index c76ad81237..8ae1a5411b 100644 --- a/src/modules/Workspaces/workspaces-common/WindowFilter.h +++ b/src/modules/Workspaces/workspaces-common/WindowFilter.h @@ -9,10 +9,12 @@ namespace WindowFilter { auto style = GetWindowLong(window, GWL_STYLE); bool isPopup = WindowUtils::HasStyle(style, WS_POPUP); - bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME); bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION); bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX); - if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons))) + + Logger::info("Style for window: {}, {:#x}", reinterpret_cast(window), style); + + if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons)) { // popup windows we want to snap: e.g. Calculator, Telegram // popup windows we don't want to snap: start menu, notification popup, tray window, etc. diff --git a/src/modules/Workspaces/workspaces-common/WindowUtils.h b/src/modules/Workspaces/workspaces-common/WindowUtils.h index 8424591dfa..79051f4ea2 100644 --- a/src/modules/Workspaces/workspaces-common/WindowUtils.h +++ b/src/modules/Workspaces/workspaces-common/WindowUtils.h @@ -121,4 +121,11 @@ namespace WindowUtils return std::wstring(title); } + + + inline bool HasThickFrame(HWND window) + { + auto style = GetWindowLong(window, GWL_STYLE); + return WindowUtils::HasStyle(style, WS_THICKFRAME); + } } \ No newline at end of file