[Workspaces] Saving app properties on launch and recapture (#36751)

* [Workspaces] Implementing set and get GUID to/from HWND to distinguish windows moved by the Workspaces tool

* After launch and capture copy the CLI args from the "original" project

* Fix getting GUID

* spell check

* modification to be able to handle different data sizes on different systems

* code optimisation

* Replacing string parameter by InvokePoint

* renaming variable
This commit is contained in:
Laszlo Nemeth 2025-01-16 10:56:38 +01:00 коммит произвёл GitHub
Родитель 603379a1ad
Коммит f5f332cbba
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 107 добавлений и 26 удалений

1
.github/actions/spell-check/expect.txt поставляемый
Просмотреть файл

@ -769,6 +769,7 @@ LOWORD
lparam lparam
LPBITMAPINFOHEADER LPBITMAPINFOHEADER
LPCITEMIDLIST LPCITEMIDLIST
LPCLSID
lpcmi lpcmi
LPCMINVOKECOMMANDINFO LPCMINVOKECOMMANDINFO
LPCREATESTRUCT LPCREATESTRUCT

Просмотреть файл

@ -7,10 +7,70 @@ namespace WorkspacesWindowProperties
namespace Properties namespace Properties
{ {
const wchar_t LaunchedByWorkspacesID[] = L"PowerToys_LaunchedByWorkspaces"; const wchar_t LaunchedByWorkspacesID[] = L"PowerToys_LaunchedByWorkspaces";
const wchar_t WorkspacesAppID[] = L"PowerToys_WorkspacesAppId";
} }
inline void StampWorkspacesLaunchedProperty(HWND window) inline void StampWorkspacesLaunchedProperty(HWND window)
{ {
::SetPropW(window, Properties::LaunchedByWorkspacesID, reinterpret_cast<HANDLE>(1)); ::SetPropW(window, Properties::LaunchedByWorkspacesID, reinterpret_cast<HANDLE>(1));
} }
inline void StampWorkspacesGuidProperty(HWND window, const std::wstring& appId)
{
GUID guid;
HRESULT hr = CLSIDFromString(appId.c_str(), static_cast<LPCLSID> (&guid));
if (hr != S_OK)
{
return;
}
const size_t workspacesAppIDLength = wcslen(Properties::WorkspacesAppID);
wchar_t* workspacesAppIDPart = new wchar_t[workspacesAppIDLength + 2];
std::memcpy(&workspacesAppIDPart[0], &Properties::WorkspacesAppID, workspacesAppIDLength * sizeof(wchar_t));
workspacesAppIDPart[workspacesAppIDLength + 1] = 0;
// the size of the HANDLE type can vary on different systems: 4 or 8 bytes. As we can set only a HANDLE as a property, we need more properties (2 or 4) to be able to store a GUID (16 bytes)
const int numberOfProperties = sizeof(GUID) / sizeof(HANDLE);
uint64_t parts[numberOfProperties];
std::memcpy(&parts[0], &guid, sizeof(GUID));
for (unsigned char partIndex = 0; partIndex < numberOfProperties; partIndex++)
{
workspacesAppIDPart[workspacesAppIDLength] = '0' + partIndex;
::SetPropW(window, workspacesAppIDPart, reinterpret_cast<HANDLE>(parts[partIndex]));
}
}
inline const std::wstring GetGuidFromHwnd(HWND window)
{
const size_t workspacesAppIDLength = wcslen(Properties::WorkspacesAppID);
wchar_t* workspacesAppIDPart = new wchar_t[workspacesAppIDLength + 2];
std::memcpy(&workspacesAppIDPart[0], &Properties::WorkspacesAppID, workspacesAppIDLength * sizeof(wchar_t));
workspacesAppIDPart[workspacesAppIDLength + 1] = 0;
// the size of the HANDLE type can vary on different systems: 4 or 8 bytes. As we can set only a HANDLE as a property, we need more properties (2 or 4) to be able to store a GUID (16 bytes)
const int numberOfProperties = sizeof(GUID) / sizeof(HANDLE);
uint64_t parts[numberOfProperties];
for (unsigned char partIndex = 0; partIndex < numberOfProperties; partIndex++)
{
workspacesAppIDPart[workspacesAppIDLength] = '0' + partIndex;
HANDLE rawData = GetPropW(window, workspacesAppIDPart);
if (rawData)
{
parts[partIndex] = reinterpret_cast<uint64_t>(rawData);
}
else
{
return L"";
}
}
GUID guid;
std::memcpy(&guid, &parts[0], sizeof(GUID));
WCHAR* guidString;
StringFromCLSID(guid, &guidString);
return guidString;
}
} }

Просмотреть файл

@ -342,12 +342,22 @@ namespace WorkspacesEditor.Models
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
} }
public void UpdateAfterLaunchAndEdit(Project other) public void UpdateAfterLaunchAndEdit(Project projectBeforeLaunch)
{ {
Id = other.Id; Id = projectBeforeLaunch.Id;
Name = other.Name; Name = projectBeforeLaunch.Name;
IsRevertEnabled = true; IsRevertEnabled = true;
MoveExistingWindows = other.MoveExistingWindows; MoveExistingWindows = projectBeforeLaunch.MoveExistingWindows;
foreach (Application app in Applications)
{
var sameAppBefore = projectBeforeLaunch.Applications.Where(x => x.Id.Equals(app.Id, StringComparison.OrdinalIgnoreCase));
if (sameAppBefore.Any())
{
var appBefore = sameAppBefore.FirstOrDefault();
app.CommandLineArguments = appBefore.CommandLineArguments;
app.IsElevated = appBefore.IsElevated;
}
}
} }
internal void CloseExpanders() internal void CloseExpanders()

Просмотреть файл

@ -40,6 +40,7 @@ namespace WorkspacesEditor.ViewModels
private Timer lastUpdatedTimer; private Timer lastUpdatedTimer;
private WorkspacesSettings settings; private WorkspacesSettings settings;
private PwaHelper _pwaHelper; private PwaHelper _pwaHelper;
private bool _isExistingProjectLaunched;
public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>(); public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>();
@ -256,12 +257,12 @@ namespace WorkspacesEditor.ViewModels
{ {
CancelSnapshot(); CancelSnapshot();
await Task.Run(() => RunSnapshotTool()); await Task.Run(() => RunSnapshotTool(_isExistingProjectLaunched));
Project project = _workspacesEditorIO.ParseTempProject(); Project project = _workspacesEditorIO.ParseTempProject();
if (project != null) if (project != null)
{ {
if (editedProject != null) if (_isExistingProjectLaunched)
{ {
project.UpdateAfterLaunchAndEdit(projectBeforeLaunch); project.UpdateAfterLaunchAndEdit(projectBeforeLaunch);
project.EditorWindowTitle = Properties.Resources.EditWorkspace; project.EditorWindowTitle = Properties.Resources.EditWorkspace;
@ -431,15 +432,12 @@ namespace WorkspacesEditor.ViewModels
} }
} }
private void RunSnapshotTool(string filename = null) private void RunSnapshotTool(bool isExistingProjectLaunched)
{ {
Process process = new Process(); Process process = new Process();
process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesSnapshotTool.exe"); process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesSnapshotTool.exe");
process.StartInfo.CreateNoWindow = true; process.StartInfo.CreateNoWindow = true;
if (!string.IsNullOrEmpty(filename)) process.StartInfo.Arguments = isExistingProjectLaunched ? $"{(int)InvokePoint.LaunchAndEdit}" : string.Empty;
{
process.StartInfo.Arguments = filename;
}
try try
{ {
@ -484,6 +482,7 @@ namespace WorkspacesEditor.ViewModels
internal void EnterSnapshotMode(bool isExistingProjectLaunched) internal void EnterSnapshotMode(bool isExistingProjectLaunched)
{ {
_isExistingProjectLaunched = isExistingProjectLaunched;
_mainWindow.WindowState = System.Windows.WindowState.Minimized; _mainWindow.WindowState = System.Windows.WindowState.Minimized;
_overlayWindows.Clear(); _overlayWindows.Clear();
foreach (var screen in MonitorHelper.GetDpiUnawareScreens()) foreach (var screen in MonitorHelper.GetDpiUnawareScreens())

Просмотреть файл

@ -397,6 +397,5 @@ namespace Utils
{ {
return installPath.ends_with(NonLocalizable::ChromeFilename); return installPath.ends_with(NonLocalizable::ChromeFilename);
} }
} }
} }

Просмотреть файл

@ -40,17 +40,6 @@ CommandLineArgs split(std::wstring s, const std::wstring& delimiter)
{ {
cmdArgs.isRestarted = true; cmdArgs.isRestarted = true;
} }
else if (!cmdArgs.workspaceId.empty())
{
try
{
auto invokePoint = static_cast<InvokePoint>(std::stoi(token));
cmdArgs.invokePoint = invokePoint;
}
catch (std::exception)
{
}
}
else else
{ {
auto guid = GuidFromString(token); auto guid = GuidFromString(token);
@ -58,6 +47,17 @@ CommandLineArgs split(std::wstring s, const std::wstring& delimiter)
{ {
cmdArgs.workspaceId = token; cmdArgs.workspaceId = token;
} }
else
{
try
{
auto invokePoint = static_cast<InvokePoint>(std::stoi(token));
cmdArgs.invokePoint = invokePoint;
}
catch (std::exception)
{
}
}
} }
} }

Просмотреть файл

@ -10,6 +10,7 @@
#include <WorkspacesLib/AppUtils.h> #include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/PwaHelper.h> #include <WorkspacesLib/PwaHelper.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#pragma comment(lib, "ntdll.lib") #pragma comment(lib, "ntdll.lib")
@ -38,7 +39,7 @@ namespace SnapshotUtils
return false; return false;
} }
std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect) std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(bool isGuidNeeded, const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect)
{ {
Utils::PwaHelper pwaHelper{}; Utils::PwaHelper pwaHelper{};
std::vector<WorkspacesData::WorkspacesProject::Application> apps{}; std::vector<WorkspacesData::WorkspacesProject::Application> apps{};
@ -157,7 +158,10 @@ namespace SnapshotUtils
rect.bottom = monitorRect.top + monitorRect.height; rect.bottom = monitorRect.top + monitorRect.height;
} }
std::wstring guid = isGuidNeeded ? WorkspacesWindowProperties::GetGuidFromHwnd(window) : L"";
WorkspacesData::WorkspacesProject::Application app{ WorkspacesData::WorkspacesProject::Application app{
.id = guid,
.name = appData.name, .name = appData.name,
.title = title, .title = title,
.path = appData.installPath, .path = appData.installPath,

Просмотреть файл

@ -4,5 +4,5 @@
namespace SnapshotUtils namespace SnapshotUtils
{ {
std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect); std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(bool isGuidNeeded, const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect);
}; };

Просмотреть файл

@ -13,6 +13,7 @@
#include <common/utils/gpo.h> #include <common/utils/gpo.h>
#include <common/utils/logger_helper.h> #include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h> #include <common/utils/UnhandledExceptionHandler.h>
#include <WorkspacesLib/utils.h>
const std::wstring moduleName = L"Workspaces\\WorkspacesSnapshotTool"; const std::wstring moduleName = L"Workspaces\\WorkspacesSnapshotTool";
const std::wstring internalPath = L""; const std::wstring internalPath = L"";
@ -46,13 +47,17 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
return -1; return -1;
} }
std::wstring cmdLineStr{ GetCommandLineW() };
auto cmdArgs = split(cmdLineStr, L" ");
// create new project // create new project
time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
WorkspacesData::WorkspacesProject project{ .id = CreateGuidString(), .creationTime = creationTime }; WorkspacesData::WorkspacesProject project{ .id = CreateGuidString(), .creationTime = creationTime };
Logger::trace(L"Creating workspace {}:{}", project.name, project.id); Logger::trace(L"Creating workspace {}:{}", project.name, project.id);
project.monitors = MonitorUtils::IdentifyMonitors(); project.monitors = MonitorUtils::IdentifyMonitors();
project.apps = SnapshotUtils::GetApps([&](HWND window) -> unsigned int { bool isGuidNeeded = cmdArgs.invokePoint == InvokePoint::LaunchAndEdit;
project.apps = SnapshotUtils::GetApps(isGuidNeeded, [&](HWND window) -> unsigned int {
auto windowMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY); auto windowMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
unsigned int monitorNumber = 0; unsigned int monitorNumber = 0;
for (const auto& monitor : project.monitors) for (const auto& monitor : project.monitors)

Просмотреть файл

@ -460,6 +460,7 @@ bool WindowArranger::moveWindow(HWND window, const WorkspacesData::WorkspacesPro
if (PlacementHelper::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect)) if (PlacementHelper::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{ {
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window); WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
WorkspacesWindowProperties::StampWorkspacesGuidProperty(window, app.id);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
return true; return true;
} }

Просмотреть файл

@ -1,3 +1,5 @@
#pragma once
#include <shlobj.h> #include <shlobj.h>
inline std::optional<GUID> GuidFromString(const std::wstring& str) noexcept inline std::optional<GUID> GuidFromString(const std::wstring& str) noexcept