[Workspaces] Handle admin windows repositioning. (#34965)

This commit is contained in:
Seraphima Zykova
2024-09-25 12:13:38 +03:00
committed by GitHub
parent 499dc9bb7a
commit 1e18e83af6
65 changed files with 2531 additions and 891 deletions

View File

@@ -1861,6 +1861,7 @@ workarounds
WORKSPACESEDITOR WORKSPACESEDITOR
WORKSPACESLAUNCHER WORKSPACESLAUNCHER
WORKSPACESSNAPSHOTTOOL WORKSPACESSNAPSHOTTOOL
WORKSPACESWINDOWARRANGER
wox wox
wparam wparam
wpf wpf

View File

@@ -194,6 +194,7 @@
"PowerToys.WorkspacesSnapshotTool.exe", "PowerToys.WorkspacesSnapshotTool.exe",
"PowerToys.WorkspacesLauncher.exe", "PowerToys.WorkspacesLauncher.exe",
"PowerToys.WorkspacesWindowArranger.exe",
"PowerToys.WorkspacesEditor.exe", "PowerToys.WorkspacesEditor.exe",
"PowerToys.WorkspacesEditor.dll", "PowerToys.WorkspacesEditor.dll",
"PowerToys.WorkspacesLauncherUI.exe", "PowerToys.WorkspacesLauncherUI.exe",

View File

@@ -619,6 +619,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\mod
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesWindowArranger", "src\modules\Workspaces\WorkspacesWindowArranger\WorkspacesWindowArranger.vcxproj", "{37D07516-4185-43A4-924F-3C7A5D95ECF6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64 Debug|ARM64 = Debug|ARM64
@@ -2719,6 +2721,18 @@ Global
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64 {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|x64 {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|x64 {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.ActiveCfg = Debug|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.Build.0 = Debug|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.ActiveCfg = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.Build.0 = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.ActiveCfg = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.Build.0 = Debug|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.ActiveCfg = Release|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.Build.0 = Release|ARM64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.ActiveCfg = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.Build.0 = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.ActiveCfg = Release|x64
{37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -2946,6 +2960,7 @@ Global
{3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
} }
processes.resize(bytes / sizeof(processes[0])); processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 36> processesToTerminate = { std::array<std::wstring_view, 37> processesToTerminate = {
L"PowerToys.PowerLauncher.exe", L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe", L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe", L"PowerToys.AdvancedPaste.exe",
@@ -1259,6 +1259,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.WorkspacesLauncher.exe", L"PowerToys.WorkspacesLauncher.exe",
L"PowerToys.WorkspacesLauncherUI.exe", L"PowerToys.WorkspacesLauncherUI.exe",
L"PowerToys.WorkspacesEditor.exe", L"PowerToys.WorkspacesEditor.exe",
L"PowerToys.WorkspacesWindowArranger.exe",
L"PowerToys.exe", L"PowerToys.exe",
}; };

View File

@@ -72,6 +72,8 @@ struct LogSettings
inline const static std::string newLoggerName = "NewPlus"; inline const static std::string newLoggerName = "NewPlus";
inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher"; inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher";
inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt"; inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt";
inline const static std::string workspacesWindowArrangerLoggerName = "workspaces-window-arranger";
inline const static std::wstring workspacesWindowArrangerLogPath = L"workspaces-window-arranger-log.txt";
inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool"; inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt"; inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt";
inline const static int retention = 30; inline const static int retention = 30;

View File

@@ -1,150 +1,27 @@
#include "pch.h" #include "pch.h"
#include "AppLauncher.h" #include "AppLauncher.h"
#include <filesystem>
#include <shellapi.h>
#include <winrt/Windows.Management.Deployment.h> #include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.Core.h> #include <winrt/Windows.ApplicationModel.Core.h>
#include <shellapi.h> #include <common/utils/winapi_error.h>
#include <ShellScalingApi.h>
#include <filesystem>
#include <workspaces-common/MonitorEnumerator.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <WorkspacesLib/AppUtils.h> #include <WorkspacesLib/AppUtils.h>
#include <common/Display/dpi_aware.h>
#include <common/utils/winapi_error.h>
#include <LaunchingApp.h>
#include <LauncherUIHelper.h>
#include <RegistryUtils.h> #include <RegistryUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
using namespace winrt; using namespace winrt;
using namespace Windows::Foundation; using namespace Windows::Foundation;
using namespace Windows::Management::Deployment; using namespace Windows::Management::Deployment;
namespace FancyZones namespace AppLauncher
{ {
inline bool allMonitorsHaveSameDpiScaling() void UpdatePackagedApps(std::vector<WorkspacesData::WorkspacesProject::Application>& apps, const Utils::Apps::AppList& installedApps)
{ {
auto monitors = MonitorEnumerator::Enumerate();
if (monitors.size() < 2)
{
return true;
}
UINT firstMonitorDpiX;
UINT firstMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[0].first, MDT_EFFECTIVE_DPI, &firstMonitorDpiX, &firstMonitorDpiY))
{
return false;
}
for (int i = 1; i < monitors.size(); i++)
{
UINT iteratedMonitorDpiX;
UINT iteratedMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[i].first, MDT_EFFECTIVE_DPI, &iteratedMonitorDpiX, &iteratedMonitorDpiY) ||
iteratedMonitorDpiX != firstMonitorDpiX)
{
return false;
}
}
return true;
}
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
DPIAware::Convert(monitor, rect);
auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset);
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
}
inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (isMinimized)
{
placement.showCmd = SW_MINIMIZE;
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
// make sure window is moved to the correct monitor before maximize.
if (isMaximized)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
}
namespace
{
LaunchingApps Prepare(std::vector<WorkspacesData::WorkspacesProject::Application>& apps, const Utils::Apps::AppList& installedApps)
{
LaunchingApps launchedApps{};
launchedApps.reserve(apps.size());
for (auto& app : apps) for (auto& app : apps)
{ {
// Packaged apps have version in the path, it will be outdated after update. // Packaged apps have version in the path, it will be outdated after update.
@@ -160,85 +37,10 @@ namespace
Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName); Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName);
} }
} }
launchedApps.push_back({ app, nullptr, L"waiting" });
}
return launchedApps;
}
bool AllWindowsFound(const LaunchingApps& launchedApps)
{
return std::find_if(launchedApps.begin(), launchedApps.end(), [&](const LaunchingApp& val) {
return val.window == nullptr;
}) == launchedApps.end();
};
bool AddOpenedWindows(LaunchingApps& launchedApps, const std::vector<HWND>& windows, const Utils::Apps::AppList& installedApps)
{
bool statusChanged = false;
for (HWND window : windows)
{
auto installedAppData = Utils::Apps::GetApp(window, installedApps);
if (!installedAppData.has_value())
{
continue;
}
auto insertionIter = launchedApps.end();
for (auto iter = launchedApps.begin(); iter != launchedApps.end(); ++iter)
{
if (iter->window == nullptr && installedAppData.value().name == iter->application.name)
{
insertionIter = iter;
}
// keep the window at the same position if it's already opened
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
UINT dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForMonitor(monitor, dpi);
float x = static_cast<float>(placement.rcNormalPosition.left);
float y = static_cast<float>(placement.rcNormalPosition.top);
float width = static_cast<float>(placement.rcNormalPosition.right - placement.rcNormalPosition.left);
float height = static_cast<float>(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top);
DPIAware::InverseConvert(monitor, x, y);
DPIAware::InverseConvert(monitor, width, height);
WorkspacesData::WorkspacesProject::Application::Position windowPosition{
.x = static_cast<int>(std::round(x)),
.y = static_cast<int>(std::round(y)),
.width = static_cast<int>(std::round(width)),
.height = static_cast<int>(std::round(height)),
};
if (iter->application.position == windowPosition)
{
Logger::debug(L"{} window already found at {} {}.", iter->application.name, iter->application.position.x, iter->application.position.y);
insertionIter = iter;
break;
} }
} }
if (insertionIter != launchedApps.end()) Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
{
insertionIter->window = window;
insertionIter->state = L"launched";
statusChanged = true;
}
if (AllWindowsFound(launchedApps))
{
break;
}
}
return statusChanged;
}
}
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors)
{ {
std::wstring dir = std::filesystem::path(appPath).parent_path(); std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -254,13 +56,12 @@ bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs,
if (!ShellExecuteEx(&sei)) if (!ShellExecuteEx(&sei))
{ {
auto error = GetLastError(); std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", get_last_error_or_default(error)); Logger::error(L"Failed to launch process. {}", error);
launchErrors.push_back({ std::filesystem::path(appPath).filename(), get_last_error_or_default(error) }); return Error(error);
return false;
} }
return true; return Ok(sei);
} }
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors) bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
@@ -314,7 +115,15 @@ bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList
std::wstring uriProtocolName = names[0]; std::wstring uriProtocolName = names[0];
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs); std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
launched = LaunchApp(command, L"", app.isElevated, launchErrors); auto res = LaunchApp(command, L"", app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
} }
else else
{ {
@@ -327,7 +136,15 @@ bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty()) if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{ {
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId); Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
launched = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated, launchErrors); 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() });
}
} }
// packaged apps: try launching by package full name // packaged apps: try launching by package full name
@@ -350,132 +167,43 @@ bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList
return false; return false;
} }
launched = LaunchApp(app.path, app.commandLineArgs, app.isElevated, launchErrors); auto res = LaunchApp(app.path, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
}
} }
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path); Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
return launched; return launched;
} }
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors) bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors)
{ {
bool launchedSuccessfully{ true }; bool launchedSuccessfully{ true };
LauncherUIHelper uiHelper;
uiHelper.LaunchUI();
// Get the set of windows before launching the app
std::vector<HWND> windowsBefore = WindowEnumerator::Enumerate(WindowFilter::Filter);
auto installedApps = Utils::Apps::GetAppsList(); auto installedApps = Utils::Apps::GetAppsList();
auto launchedApps = Prepare(project.apps, installedApps); UpdatePackagedApps(project.apps, installedApps);
uiHelper.UpdateLaunchStatus(launchedApps);
// Launch apps // Launch apps
for (auto& app : launchedApps) for (auto& app : project.apps)
{ {
if (!app.window) if (!Launch(app, launchErrors))
{ {
if (!Launch(app.application, launchErrors)) Logger::error(L"Failed to launch {}", app.name);
{ launchingStatus.Update(app, LaunchingState::Failed);
Logger::error(L"Failed to launch {}", app.application.name);
app.state = L"failed";
uiHelper.UpdateLaunchStatus(launchedApps);
launchedSuccessfully = false; launchedSuccessfully = false;
} }
}
}
// Get newly opened windows after launching apps, keep retrying for 5 seconds
Logger::trace(L"Find new windows");
for (int attempt = 0; attempt < 50 && !AllWindowsFound(launchedApps); attempt++)
{
std::vector<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> windowsDiff{};
std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(windowsBefore.begin(), windowsBefore.end(), window) == windowsBefore.end(); });
if (AddOpenedWindows(launchedApps, windowsDiff, installedApps))
{
uiHelper.UpdateLaunchStatus(launchedApps);
}
// check if all windows were found
if (AllWindowsFound(launchedApps))
{
Logger::trace(L"All windows found.");
break;
}
else else
{ {
Logger::trace(L"Not all windows found, retry."); launchingStatus.Update(app, LaunchingState::Launched);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// Check single-instance app windows
Logger::trace(L"Find single-instance app windows");
if (!AllWindowsFound(launchedApps))
{
if (AddOpenedWindows(launchedApps, WindowEnumerator::Enumerate(WindowFilter::Filter), installedApps))
{
uiHelper.UpdateLaunchStatus(launchedApps);
}
}
// Place windows
for (const auto& [app, window, status] : launchedApps)
{
if (window == nullptr)
{
Logger::warn(L"{} window not found.", app.name);
launchedSuccessfully = false;
continue;
}
auto snapMonitorIter = std::find_if(project.monitors.begin(), project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (snapMonitorIter == project.monitors.end())
{
Logger::error(L"No monitor saved for launching the app");
continue;
}
bool launchMinimized = app.isMinimized;
bool launchMaximized = app.isMaximized;
HMONITOR currentMonitor{};
UINT currentDpi = DPIAware::DEFAULT_DPI;
auto currentMonitorIter = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (currentMonitorIter != monitors.end())
{
currentMonitor = currentMonitorIter->monitor;
currentDpi = currentMonitorIter->dpi;
}
else
{
currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);
DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi);
launchMinimized = true;
launchMaximized = false;
}
RECT rect = app.position.toRect();
float mult = static_cast<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
else
{
Logger::error(L"Failed placing {}", app.name);
launchedSuccessfully = false;
} }
} }
return launchedSuccessfully; return launchedSuccessfully;
} }
}

View File

@@ -1,7 +1,16 @@
#pragma once #pragma once
#include <shellapi.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h> #include <WorkspacesLib/WorkspacesData.h>
namespace AppLauncher
{
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>; using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors); Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated);
bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors);
}

View File

@@ -0,0 +1,123 @@
#include "pch.h"
#include "Launcher.h"
#include <common/utils/json.h>
#include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/trace.h>
#include <AppLauncher.h>
Launcher::Launcher(const WorkspacesData::WorkspacesProject& project,
std::vector<WorkspacesData::WorkspacesProject>& workspaces,
InvokePoint invokePoint) :
m_project(project),
m_workspaces(workspaces),
m_invokePoint(invokePoint),
m_start(std::chrono::high_resolution_clock::now()),
m_uiHelper(std::make_unique<LauncherUIHelper>()),
m_windowArrangerHelper(std::make_unique<WindowArrangerHelper>(std::bind(&Launcher::handleWindowArrangerMessage, this, std::placeholders::_1))),
m_launchingStatus(m_project, std::bind(&LauncherUIHelper::UpdateLaunchStatus, m_uiHelper.get(), std::placeholders::_1))
{
m_uiHelper->LaunchUI();
m_uiHelper->UpdateLaunchStatus(m_launchingStatus.Get());
bool launchElevated = std::find_if(m_project.apps.begin(), m_project.apps.end(), [](const WorkspacesData::WorkspacesProject::Application& app) { return app.isElevated; }) != m_project.apps.end();
m_windowArrangerHelper->Launch(m_project.id, launchElevated, [&]() -> bool
{
if (m_launchingStatus.AllLaunchedAndMoved())
{
return false;
}
if (m_launchingStatus.AllLaunched())
{
static auto arrangerTimeDelay = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> timeDiff = currentTime - arrangerTimeDelay;
if (timeDiff.count() >= 5)
{
return false;
}
}
return true;
});
}
Launcher::~Launcher()
{
Logger::trace(L"Finalizing launch");
// update last-launched time
if (m_invokePoint != InvokePoint::LaunchAndEdit)
{
time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
m_project.lastLaunchedTime = launchedTime;
for (int i = 0; i < m_workspaces.size(); i++)
{
if (m_workspaces[i].id == m_project.id)
{
m_workspaces[i] = m_project;
break;
}
}
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(m_workspaces));
}
// telemetry
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - m_start;
Logger::trace(L"Launching time: {} s", duration.count());
auto monitors = MonitorUtils::IdentifyMonitors();
bool differentSetup = monitors.size() != m_project.monitors.size();
if (!differentSetup)
{
for (const auto& monitor : m_project.monitors)
{
auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; });
if (setup == monitors.end())
{
differentSetup = true;
break;
}
}
}
Trace::Workspaces::Launch(m_launchedSuccessfully, m_project, m_invokePoint, duration.count(), differentSetup, m_launchErrors);
}
void Launcher::Launch()
{
Logger::info(L"Launch Workspace {} : {}", m_project.name, m_project.id);
m_launchedSuccessfully = AppLauncher::Launch(m_project, m_launchingStatus, m_launchErrors);
}
void Launcher::handleWindowArrangerMessage(const std::wstring& msg)
{
if (msg == L"ready")
{
Launch();
}
else
{
try
{
auto data = WorkspacesData::AppLaunchInfoJSON::FromJson(json::JsonValue::Parse(msg).GetObjectW());
if (data.has_value())
{
m_launchingStatus.Update(data.value().application, data.value().state);
}
else
{
Logger::error(L"Failed to parse message from WorkspacesWindowArranger");
}
}
catch (const winrt::hresult_error&)
{
Logger::error(L"Failed to parse message from WorkspacesWindowArranger");
}
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <workspaces-common/InvokePoint.h>
#include <LauncherUIHelper.h>
#include <WindowArrangerHelper.h>
class Launcher
{
public:
Launcher(const WorkspacesData::WorkspacesProject& project, std::vector<WorkspacesData::WorkspacesProject>& workspaces, InvokePoint invokePoint);
~Launcher();
void Launch();
private:
WorkspacesData::WorkspacesProject m_project;
std::vector<WorkspacesData::WorkspacesProject>& m_workspaces;
const InvokePoint m_invokePoint;
const std::chrono::steady_clock::time_point m_start;
std::unique_ptr<LauncherUIHelper> m_uiHelper;
std::unique_ptr<WindowArrangerHelper> m_windowArrangerHelper;
LaunchingStatus m_launchingStatus;
bool m_launchedSuccessfully{};
std::vector<std::pair<std::wstring, std::wstring>> m_launchErrors{};
void handleWindowArrangerMessage(const std::wstring& msg);
};

View File

@@ -7,12 +7,22 @@
#include <common/utils/OnThreadExecutor.h> #include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h> #include <common/utils/winapi_error.h>
#include <AppLauncher.h>
LauncherUIHelper::LauncherUIHelper() :
m_processId{},
m_ipcHelper(IPCHelperStrings::LauncherUIPipeName, IPCHelperStrings::UIPipeName, nullptr)
{
}
LauncherUIHelper::~LauncherUIHelper() LauncherUIHelper::~LauncherUIHelper()
{ {
OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] { OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] {
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::this_thread::sleep_for(std::chrono::milliseconds(1000));
HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, uiProcessId); Logger::info(L"Stopping WorkspacesLauncherUI with pid {}", m_processId);
HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId);
if (uiProcess) if (uiProcess)
{ {
bool res = TerminateProcess(uiProcess, 0); bool res = TerminateProcess(uiProcess, 0);
@@ -25,8 +35,6 @@ LauncherUIHelper::~LauncherUIHelper()
{ {
Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError())); Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError()));
} }
std::filesystem::remove(WorkspacesData::LaunchWorkspacesFile());
} }).wait(); } }).wait();
} }
@@ -34,45 +42,32 @@ void LauncherUIHelper::LaunchUI()
{ {
Logger::trace(L"Starting WorkspacesLauncherUI"); Logger::trace(L"Starting WorkspacesLauncherUI");
STARTUPINFO info = { sizeof(info) };
PROCESS_INFORMATION pi = { 0 };
TCHAR buffer[MAX_PATH] = { 0 }; TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH); GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring path = std::filesystem::path(buffer).parent_path(); std::wstring path = std::filesystem::path(buffer).parent_path();
path.append(L"\\PowerToys.WorkspacesLauncherUI.exe");
auto succeeded = CreateProcessW(path.c_str(), nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &info, &pi); auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesLauncherUI.exe", L"", false);
if (succeeded) if (res.isOk())
{ {
if (pi.hProcess) auto value = res.value();
{ m_processId = GetProcessId(value.hProcess);
uiProcessId = pi.dwProcessId; CloseHandle(value.hProcess);
CloseHandle(pi.hProcess); Logger::info(L"WorkspacesLauncherUI started with pid {}", m_processId);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
} }
else else
{ {
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError())); Logger::error(L"Failed to launch PowerToys.WorkspacesLauncherUI: {}", res.error());
} }
} }
void LauncherUIHelper::UpdateLaunchStatus(LaunchingApps launchedApps) void LauncherUIHelper::UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const
{ {
WorkspacesData::AppLaunchData appData = WorkspacesData::AppLaunchData(); WorkspacesData::AppLaunchData appData;
appData.appLaunchInfoList.reserve(launchedApps.size());
appData.launcherProcessID = GetCurrentProcessId(); appData.launcherProcessID = GetCurrentProcessId();
for (auto& app : launchedApps) for (auto& [app, data] : launchedApps)
{ {
WorkspacesData::AppLaunchInfo appLaunchInfo = WorkspacesData::AppLaunchInfo(); appData.appsStateList.insert({ app, { app, nullptr, data.state } });
appLaunchInfo.name = app.application.name;
appLaunchInfo.path = app.application.path;
appLaunchInfo.state = app.state;
appData.appLaunchInfoList.push_back(appLaunchInfo);
} }
json::to_file(WorkspacesData::LaunchWorkspacesFile(), WorkspacesData::AppLaunchDataJSON::ToJson(appData)); m_ipcHelper.send(WorkspacesData::AppLaunchDataJSON::ToJson(appData).ToString().c_str());
} }

View File

@@ -1,16 +1,18 @@
#pragma once #pragma once
#include <LaunchingApp.h> #include <WorkspacesLib/WorkspacesData.h>
#include <WorkspacesLib/IPCHelper.h>
class LauncherUIHelper class LauncherUIHelper
{ {
public: public:
LauncherUIHelper() = default; LauncherUIHelper();
~LauncherUIHelper(); ~LauncherUIHelper();
void LaunchUI(); void LaunchUI();
void UpdateLaunchStatus(LaunchingApps launchedApps); void UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const;
private: private:
DWORD uiProcessId; DWORD m_processId;
IPCHelper m_ipcHelper;
}; };

View File

@@ -1,13 +0,0 @@
#pragma once
#include <Windows.h>
#include <WorkspacesLib/WorkspacesData.h>
struct LaunchingApp
{
WorkspacesData::WorkspacesProject::Application application;
HWND window;
std::wstring state;
};
using LaunchingApps = std::vector<LaunchingApp>;

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include "WindowArrangerHelper.h"
#include <filesystem>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <AppLauncher.h>
WindowArrangerHelper::WindowArrangerHelper(std::function<void(const std::wstring&)> ipcCallback) :
m_processId{},
m_ipcHelper(IPCHelperStrings::LauncherArrangerPipeName, IPCHelperStrings::WindowArrangerPipeName, ipcCallback)
{
}
WindowArrangerHelper::~WindowArrangerHelper()
{
Logger::info(L"Stopping WorkspacesWindowArranger with pid {}", m_processId);
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId);
if (process)
{
bool res = TerminateProcess(process, 0);
if (!res)
{
Logger::error(L"Unable to terminate PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError()));
}
}
else
{
Logger::error(L"Unable to find PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError()));
}
}
void WindowArrangerHelper::Launch(const std::wstring& projectId, bool elevated, std::function<bool()> keepWaitingCallback)
{
Logger::trace(L"Starting WorkspacesWindowArranger");
TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring path = std::filesystem::path(buffer).parent_path();
auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesWindowArranger.exe", projectId, elevated);
if (res.isOk())
{
auto value = res.value();
m_processId = GetProcessId(value.hProcess);
Logger::info(L"WorkspacesWindowArranger started with pid {}", m_processId);
std::atomic_bool timeoutExpired = false;
m_threadExecutor.submit(OnThreadExecutor::task_t{
[&] {
HANDLE process = value.hProcess;
while (keepWaitingCallback())
{
WaitForSingleObject(process, 100);
}
Logger::trace(L"Finished waiting WorkspacesWindowArranger");
CloseHandle(process);
}}).wait();
timeoutExpired = true;
}
else
{
Logger::error(L"Failed to launch PowerToys.WorkspacesWindowArranger: {}", res.error());
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/utils/OnThreadExecutor.h>
class WindowArrangerHelper
{
public:
WindowArrangerHelper(std::function<void(const std::wstring&)> ipcCallback);
~WindowArrangerHelper();
void Launch(const std::wstring& projectId, bool elevated, std::function<bool()> keepWaitingCallback);
private:
DWORD m_processId;
IPCHelper m_ipcHelper;
OnThreadExecutor m_threadExecutor;
};

View File

@@ -127,21 +127,23 @@
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="AppLauncher.cpp" /> <ClCompile Include="AppLauncher.cpp" />
<ClCompile Include="Launcher.cpp" />
<ClCompile Include="LauncherUIHelper.cpp" /> <ClCompile Include="LauncherUIHelper.cpp" />
<ClCompile Include="main.cpp" /> <ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="RegistryUtils.cpp" /> <ClCompile Include="RegistryUtils.cpp" />
<ClCompile Include="WindowArrangerHelper.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="AppLauncher.h" /> <ClInclude Include="AppLauncher.h" />
<ClInclude Include="Launcher.h" />
<ClInclude Include="LauncherUIHelper.h" /> <ClInclude Include="LauncherUIHelper.h" />
<ClInclude Include="LaunchingApp.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="RegistryUtils.h" /> <ClInclude Include="RegistryUtils.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="utils.h" /> <ClInclude Include="WindowArrangerHelper.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -27,13 +27,13 @@
<ClInclude Include="RegistryUtils.h"> <ClInclude Include="RegistryUtils.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LauncherUIHelper.h"> <ClInclude Include="LauncherUIHelper.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="LaunchingApp.h"> <ClInclude Include="WindowArrangerHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Launcher.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
@@ -53,6 +53,12 @@
<ClCompile Include="LauncherUIHelper.cpp"> <ClCompile Include="LauncherUIHelper.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="WindowArrangerHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Launcher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -1,16 +1,12 @@
#include "pch.h" #include "pch.h"
#include <WorkspacesLib/WorkspacesData.h> #include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/trace.h> #include <WorkspacesLib/utils.h>
#include <AppLauncher.h> #include <Launcher.h>
#include <utils.h>
#include <Generated Files/resource.h> #include <Generated Files/resource.h>
#include <workspaces-common/InvokePoint.h>
#include <workspaces-common/MonitorUtils.h>
#include <common/utils/elevation.h> #include <common/utils/elevation.h>
#include <common/utils/gpo.h> #include <common/utils/gpo.h>
#include <common/utils/logger_helper.h> #include <common/utils/logger_helper.h>
@@ -58,16 +54,16 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
std::string cmdLineStr(cmdline); std::wstring cmdLineStr{ GetCommandLineW() };
auto cmdArgs = split(cmdLineStr, " "); auto cmdArgs = split(cmdLineStr, L" ");
if (cmdArgs.size() < 1) if (cmdArgs.size() < 2)
{ {
Logger::warn("Incorrect command line arguments"); Logger::warn("Incorrect command line arguments");
MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1; return 1;
} }
std::wstring id(cmdArgs[0].begin(), cmdArgs[0].end()); std::wstring id(cmdArgs[1].begin(), cmdArgs[1].end());
if (id.empty()) if (id.empty())
{ {
Logger::warn("Incorrect command line arguments: no workspace id"); Logger::warn("Incorrect command line arguments: no workspace id");
@@ -76,11 +72,11 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
} }
InvokePoint invokePoint = InvokePoint::EditorButton; InvokePoint invokePoint = InvokePoint::EditorButton;
if (cmdArgs.size() > 1) if (cmdArgs.size() > 2)
{ {
try try
{ {
invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[1])); invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[2]));
} }
catch (std::exception) catch (std::exception)
{ {
@@ -95,79 +91,51 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (invokePoint == InvokePoint::LaunchAndEdit) if (invokePoint == InvokePoint::LaunchAndEdit)
{ {
// check the temp file in case the project is just created and not saved to the workspaces.json yet // check the temp file in case the project is just created and not saved to the workspaces.json yet
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile())) auto file = WorkspacesData::TempWorkspacesFile();
auto res = JsonUtils::ReadSingleWorkspace(file);
if (res.isOk() && projectToLaunch.id == id)
{ {
try projectToLaunch = res.getValue();
{
auto savedWorkspacesJson = json::from_file(WorkspacesData::TempWorkspacesFile());
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesProjectJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
if (savedWorkspaces.value().id == id)
{
projectToLaunch = savedWorkspaces.value();
}
} }
else else
{ {
Logger::critical("Incorrect Workspaces file"); std::wstring formattedMessage{};
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile()); switch (res.error())
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
else
{ {
Logger::critical("Incorrect Workspaces file"); case JsonUtils::WorkspacesFileError::FileReadingError:
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile()); formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
break;
case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
}
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1; return 1;
} }
} }
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempWorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
}
if (projectToLaunch.id.empty()) if (projectToLaunch.id.empty())
{ {
try auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{ {
auto savedWorkspacesJson = json::from_file(WorkspacesData::WorkspacesFile()); workspaces = res.getValue();
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
workspaces = savedWorkspaces.value();
} }
else else
{ {
Logger::critical("Incorrect Workspaces file"); std::wstring formattedMessage{};
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile()); switch (res.error())
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
}
else
{ {
Logger::critical("Incorrect Workspaces file"); case JsonUtils::WorkspacesFileError::FileReadingError:
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile()); formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); break;
return 1; case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
} }
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::WorkspacesFile());
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1; return 1;
} }
@@ -175,7 +143,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (workspaces.empty()) if (workspaces.empty())
{ {
Logger::warn("Workspaces file is empty"); Logger::warn("Workspaces file is empty");
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), WorkspacesData::WorkspacesFile()); std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), file);
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1; return 1;
} }
@@ -198,50 +166,9 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
return 1; return 1;
} }
// launch apps Launcher launcher(projectToLaunch, workspaces, invokePoint);
Logger::info(L"Launch Workspace {} : {}", projectToLaunch.name, projectToLaunch.id);
auto monitors = MonitorUtils::IdentifyMonitors();
std::vector<std::pair<std::wstring, std::wstring>> launchErrors{};
auto start = std::chrono::high_resolution_clock::now();
bool launchedSuccessfully = Launch(projectToLaunch, monitors, launchErrors);
// update last-launched time
if (invokePoint != InvokePoint::LaunchAndEdit)
{
time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
projectToLaunch.lastLaunchedTime = launchedTime;
for (int i = 0; i < workspaces.size(); i++)
{
if (workspaces[i].id == projectToLaunch.id)
{
workspaces[i] = projectToLaunch;
break;
}
}
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(workspaces));
}
// telemetry
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
Logger::trace(L"Launching time: {} s", duration.count());
bool differentSetup = monitors.size() != projectToLaunch.monitors.size();
if (!differentSetup)
{
for (const auto& monitor : projectToLaunch.monitors)
{
auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; });
if (setup == monitors.end())
{
differentSetup = true;
break;
}
}
}
Trace::Workspaces::Launch(launchedSuccessfully, projectToLaunch, invokePoint, duration.count(), differentSetup, launchErrors);
Logger::trace("Finished");
CoUninitialize(); CoUninitialize();
return 0; return 0;
} }

View File

@@ -5,11 +5,9 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Forms.Design.Behavior;
using Common.UI; using Common.UI;
using ManagedCommon; using ManagedCommon;
using WorkspacesLauncherUI.Utils; using PowerToys.Interop;
using WorkspacesLauncherUI.ViewModels; using WorkspacesLauncherUI.ViewModels;
namespace WorkspacesLauncherUI namespace WorkspacesLauncherUI
@@ -21,6 +19,9 @@ namespace WorkspacesLauncherUI
{ {
private static Mutex _instanceMutex; private static Mutex _instanceMutex;
// Create an instance of the IPC wrapper.
private static TwoWayPipeMessageIPCManaged ipcmanager;
private StatusWindow _mainWindow; private StatusWindow _mainWindow;
private MainViewModel _mainViewModel; private MainViewModel _mainViewModel;
@@ -29,21 +30,23 @@ namespace WorkspacesLauncherUI
private bool _isDisposed; private bool _isDisposed;
public static Action<string> IPCMessageReceivedCallback { get; set; }
public App() public App()
{ {
} }
private void OnStartup(object sender, StartupEventArgs e) private void OnStartup(object sender, StartupEventArgs e)
{ {
Logger.InitializeLogger("\\Workspaces\\Logs"); Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI");
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
const string appName = "Local\\PowerToys_Workspaces_Launcher_InstanceMutex"; const string appName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex";
bool createdNew; bool createdNew;
_instanceMutex = new Mutex(true, appName, out createdNew); _instanceMutex = new Mutex(true, appName, out createdNew);
if (!createdNew) if (!createdNew)
{ {
Logger.LogWarning("Another instance of Workspaces Launcher is already running. Exiting this instance."); Logger.LogWarning("Another instance of Workspaces Launcher UI is already running. Exiting this instance.");
_instanceMutex = null; _instanceMutex = null;
Shutdown(0); Shutdown(0);
return; return;
@@ -56,6 +59,15 @@ namespace WorkspacesLauncherUI
return; return;
} }
ipcmanager = new TwoWayPipeMessageIPCManaged("\\\\.\\pipe\\powertoys_workspaces_ui_", "\\\\.\\pipe\\powertoys_workspaces_launcher_ui_", (string message) =>
{
if (IPCMessageReceivedCallback != null && message.Length > 0)
{
IPCMessageReceivedCallback(message);
}
});
ipcmanager.Start();
ThemeManager = new ThemeManager(this); ThemeManager = new ThemeManager(this);
if (_mainViewModel == null) if (_mainViewModel == null)
@@ -97,6 +109,10 @@ namespace WorkspacesLauncherUI
if (disposing) if (disposing)
{ {
ThemeManager?.Dispose(); ThemeManager?.Dispose();
ipcmanager?.End();
ipcmanager?.Dispose();
_instanceMutex?.Dispose(); _instanceMutex?.Dispose();
} }

View File

@@ -1,33 +1,16 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Workspaces.Data; using Workspaces.Data;
using WorkspacesLauncherUI.Utils;
using static WorkspacesLauncherUI.Data.AppLaunchData; using static WorkspacesLauncherUI.Data.AppLaunchData;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
using static WorkspacesLauncherUI.Data.AppLaunchInfosData; using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data namespace WorkspacesLauncherUI.Data
{ {
internal sealed class AppLaunchData : WorkspacesEditorData<AppLaunchDataWrapper> public class AppLaunchData : WorkspacesUIData<AppLaunchDataWrapper>
{ {
public static string File
{
get
{
return FolderUtils.DataFolder() + "\\launch-workspaces.json";
}
}
public struct AppLaunchDataWrapper public struct AppLaunchDataWrapper
{ {
[JsonPropertyName("apps")] [JsonPropertyName("apps")]

View File

@@ -1,32 +1,23 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Workspaces.Data; using Workspaces.Data;
using static WorkspacesLauncherUI.Data.AppLaunchInfoData; using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
namespace WorkspacesLauncherUI.Data namespace WorkspacesLauncherUI.Data
{ {
public class AppLaunchInfoData : WorkspacesEditorData<AppLaunchInfoWrapper> public class AppLaunchInfoData : WorkspacesUIData<AppLaunchInfoWrapper>
{ {
public struct AppLaunchInfoWrapper public struct AppLaunchInfoWrapper
{ {
[JsonPropertyName("name")] [JsonPropertyName("application")]
public string Name { get; set; } public ApplicationWrapper Application { get; set; }
[JsonPropertyName("path")]
public string Path { get; set; }
[JsonPropertyName("state")] [JsonPropertyName("state")]
public string State { get; set; } public LaunchingState State { get; set; }
} }
} }
} }

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -13,7 +12,7 @@ using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
namespace WorkspacesLauncherUI.Data namespace WorkspacesLauncherUI.Data
{ {
public class AppLaunchInfosData : WorkspacesEditorData<AppLaunchInfoListWrapper> public class AppLaunchInfosData : WorkspacesUIData<AppLaunchInfoListWrapper>
{ {
public struct AppLaunchInfoListWrapper public struct AppLaunchInfoListWrapper
{ {

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
public struct ApplicationWrapper
{
public string Application { get; set; }
public string ApplicationPath { get; set; }
public string Title { get; set; }
public string PackageFullName { get; set; }
public string AppUserModelId { get; set; }
public string CommandLineArguments { get; set; }
public bool IsElevated { get; set; }
public bool CanLaunchElevated { get; set; }
public bool Minimized { get; set; }
public bool Maximized { get; set; }
public PositionWrapper Position { get; set; }
public int Monitor { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
// sync with WorkspacesLib : LaunchingStateEnum.h
public enum LaunchingState
{
Waiting = 0,
Launched,
LaunchedAndMoved,
Failed,
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace WorkspacesLauncherUI.Data
{
public struct PositionWrapper
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public static bool operator ==(PositionWrapper left, PositionWrapper right)
{
return left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height;
}
public static bool operator !=(PositionWrapper left, PositionWrapper right)
{
return left.X != right.X || left.Y != right.Y || left.Width != right.Width || left.Height != right.Height;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
PositionWrapper pos = (PositionWrapper)obj;
return X == pos.X && Y == pos.Y && Width == pos.Width && Height == pos.Height;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
}

View File

@@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils;
namespace Workspaces.Data namespace Workspaces.Data
{ {
public class WorkspacesEditorData<T> public class WorkspacesUIData<T>
{ {
protected JsonSerializerOptions JsonOptions protected JsonSerializerOptions JsonOptions
{ {
@@ -22,10 +22,8 @@ namespace Workspaces.Data
} }
} }
public T Read(string file) public T Deserialize(string data)
{ {
IOUtils ioUtils = new IOUtils();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, JsonOptions); return JsonSerializer.Deserialize<T>(data, JsonOptions);
} }

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Automation.Peers; using System.Windows.Automation.Peers;
using System.Windows.Controls; using System.Windows.Controls;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -13,9 +13,9 @@ using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using ManagedCommon; using ManagedCommon;
using Windows.Management.Deployment; using Windows.Management.Deployment;
using WorkspacesLauncherUI.Data;
namespace WorkspacesLauncherUI.Models namespace WorkspacesLauncherUI.Models
{ {
@@ -28,9 +28,9 @@ namespace WorkspacesLauncherUI.Models
PropertyChanged?.Invoke(this, e); PropertyChanged?.Invoke(this, e);
} }
public string AppPath { get; set; } public ApplicationWrapper Application { get; set; }
public bool Loading => LaunchState == "waiting"; public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched;
private Icon _icon; private Icon _icon;
@@ -51,12 +51,12 @@ namespace WorkspacesLauncherUI.Models
} }
else else
{ {
_icon = Icon.ExtractAssociatedIcon(AppPath); _icon = Icon.ExtractAssociatedIcon(Application.ApplicationPath);
} }
} }
catch (Exception) catch (Exception)
{ {
Logger.LogWarning($"Icon not found on app path: {AppPath}. Using default icon"); Logger.LogWarning($"Icon not found on app path: {Application.ApplicationPath}. Using default icon");
IsNotFound = true; IsNotFound = true;
_icon = new Icon(@"images\DefaultIcon.ico"); _icon = new Icon(@"images\DefaultIcon.ico");
} }
@@ -66,16 +66,22 @@ namespace WorkspacesLauncherUI.Models
} }
} }
public string Name { get; set; } public string Name
{
get
{
return Application.Application;
}
}
public string LaunchState { get; set; } public LaunchingState LaunchState { get; set; }
public string StateGlyph public string StateGlyph
{ {
get => LaunchState switch get => LaunchState switch
{ {
"launched" => "\U0000F78C", LaunchingState.LaunchedAndMoved => "\U0000F78C",
"failed" => "\U0000EF2C", LaunchingState.Failed => "\U0000EF2C",
_ => "\U0000EF2C", _ => "\U0000EF2C",
}; };
} }
@@ -84,8 +90,8 @@ namespace WorkspacesLauncherUI.Models
{ {
get => LaunchState switch get => LaunchState switch
{ {
"launched" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)), LaunchingState.LaunchedAndMoved => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
"failed" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)), LaunchingState.Failed => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
_ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)), _ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
}; };
} }
@@ -139,13 +145,13 @@ namespace WorkspacesLauncherUI.Models
{ {
if (_isPackagedApp == null) if (_isPackagedApp == null)
{ {
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase)) if (!Application.ApplicationPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
{ {
_isPackagedApp = false; _isPackagedApp = false;
} }
else else
{ {
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty); string appPath = Application.ApplicationPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match match = packagedAppPathRegex.Match(appPath); Match match = packagedAppPathRegex.Match(appPath);
_isPackagedApp = match.Success; _isPackagedApp = match.Success;
@@ -200,7 +206,7 @@ namespace WorkspacesLauncherUI.Models
} }
catch (Exception e) catch (Exception e)
{ {
Logger.LogError($"Exception while drawing icon for app with path: {AppPath}. Exception message: {e.Message}"); Logger.LogError($"Exception while drawing icon for app with path: {Application.ApplicationPath}. Exception message: {e.Message}");
} }
} }

View File

@@ -1,28 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace WorkspacesLauncherUI.Utils
{
public class FolderUtils
{
public static string Desktop()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public static string Temp()
{
return Path.GetTempPath();
}
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}
}

View File

@@ -1,54 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace WorkspacesLauncherUI.Utils
{
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public IOUtils()
{
}
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
var attempts = 0;
while (attempts < 10)
{
try
{
using (Stream inputStream = _fileSystem.File.Open(fileName, FileMode.Open))
using (StreamReader reader = new StreamReader(inputStream))
{
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}
}

View File

@@ -7,9 +7,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.IO.Abstractions;
using ManagedCommon; using ManagedCommon;
using WorkspacesLauncherUI.Data; using WorkspacesLauncherUI.Data;
using WorkspacesLauncherUI.Models; using WorkspacesLauncherUI.Models;
@@ -20,8 +17,6 @@ namespace WorkspacesLauncherUI.ViewModels
{ {
public ObservableCollection<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>(); public ObservableCollection<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>();
private IFileSystemWatcher _watcher;
private System.Timers.Timer selfDestroyTimer;
private StatusWindow _snapshotWindow; private StatusWindow _snapshotWindow;
private int launcherProcessID; private int launcherProcessID;
private bool _exiting; private bool _exiting;
@@ -36,60 +31,43 @@ namespace WorkspacesLauncherUI.ViewModels
public MainViewModel() public MainViewModel()
{ {
_exiting = false; _exiting = false;
LoadAppLaunchInfos();
string fileName = Path.GetFileName(AppLaunchData.File);
_watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher("Workspaces", fileName, () => AppLaunchInfoStateChanged());
}
private void AppLaunchInfoStateChanged() // receive IPC Message
App.IPCMessageReceivedCallback = (string msg) =>
{ {
LoadAppLaunchInfos(); try
{
AppLaunchData parser = new AppLaunchData();
AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Deserialize(msg);
HandleAppLaunchingState(appLaunchData);
}
catch (Exception ex)
{
Logger.LogError(ex.Message);
}
};
} }
private void LoadAppLaunchInfos() private void HandleAppLaunchingState(AppLaunchData.AppLaunchDataWrapper appLaunchData)
{ {
if (_exiting) if (_exiting)
{ {
return; return;
} }
AppLaunchData parser = new AppLaunchData();
if (!File.Exists(AppLaunchData.File))
{
Logger.LogWarning($"AppLaunchInfosData storage file not found: {AppLaunchData.File}");
return;
}
AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Read(AppLaunchData.File);
launcherProcessID = appLaunchData.LauncherProcessID; launcherProcessID = appLaunchData.LauncherProcessID;
List<AppLaunching> appLaunchingList = new List<AppLaunching>(); List<AppLaunching> appLaunchingList = new List<AppLaunching>();
bool allLaunched = true;
foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList) foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList)
{ {
appLaunchingList.Add(new AppLaunching() appLaunchingList.Add(new AppLaunching()
{ {
Name = app.Name, Application = app.Application,
AppPath = app.Path,
LaunchState = app.State, LaunchState = app.State,
}); });
if (app.State != "launched" && app.State != "failed")
{
allLaunched = false;
}
} }
AppsListed = new ObservableCollection<AppLaunching>(appLaunchingList); AppsListed = new ObservableCollection<AppLaunching>(appLaunchingList);
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppsListed))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppsListed)));
if (allLaunched)
{
selfDestroyTimer = new System.Timers.Timer();
selfDestroyTimer.Interval = 1000;
selfDestroyTimer.Elapsed += SelfDestroy;
selfDestroyTimer.Start();
}
} }
private void SelfDestroy(object source, System.Timers.ElapsedEventArgs e) private void SelfDestroy(object source, System.Timers.ElapsedEventArgs e)
@@ -113,7 +91,6 @@ namespace WorkspacesLauncherUI.ViewModels
internal void CancelLaunch() internal void CancelLaunch()
{ {
_exiting = true; _exiting = true;
_watcher.Dispose();
Process proc = Process.GetProcessById(launcherProcessID); Process proc = Process.GetProcessById(launcherProcessID);
proc.Kill(); proc.Kill();
} }

View File

@@ -0,0 +1,42 @@
#include "pch.h"
#include "IPCHelper.h"
#include <common/logger/logger.h>
IPCHelper::IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function<void(const std::wstring&)> messageCallback) :
callback(messageCallback)
{
HANDLE hToken = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
{
Logger::error("Failed to get process token");
return;
}
std::unique_lock lock{ ipcMutex };
ipc = make_unique<TwoWayPipeMessageIPC>(currentPipeName, receiverPipeName, std::bind(&IPCHelper::receive, this, std::placeholders::_1));
ipc->start(hToken);
}
IPCHelper::~IPCHelper()
{
std::unique_lock lock{ ipcMutex };
if (ipc)
{
ipc->end();
ipc = nullptr;
}
}
void IPCHelper::send(const std::wstring& message) const
{
ipc->send(message);
}
void IPCHelper::receive(const std::wstring& msg)
{
if (callback)
{
callback(msg);
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <mutex>
#include <common/interop/two_way_pipe_message_ipc.h>
namespace IPCHelperStrings
{
static std::wstring LauncherUIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_ui_");
static std::wstring UIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_ui_");
static std::wstring LauncherArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_arranger_");
static std::wstring WindowArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_window_arranger_");
}
class IPCHelper
{
public:
IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function<void(const std::wstring&)> messageCallback);
~IPCHelper();
void send(const std::wstring& message) const;
private:
void receive(const std::wstring& msg);
std::unique_ptr<TwoWayPipeMessageIPC> ipc;
std::mutex ipcMutex;
std::function<void(const std::wstring&)> callback;
};

View File

@@ -0,0 +1,106 @@
#include "pch.h"
#include "JsonUtils.h"
#include <filesystem>
#include <common/logger/logger.h>
namespace JsonUtils
{
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName)
{
if (std::filesystem::exists(fileName))
{
try
{
auto tempWorkspacesJson = json::from_file(fileName);
if (tempWorkspacesJson.has_value())
{
auto tempWorkspace = WorkspacesData::WorkspacesProjectJSON::FromJson(tempWorkspacesJson.value());
if (tempWorkspace.has_value())
{
return Ok(tempWorkspace.value());
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
return Error(WorkspacesFileError::FileReadingError);
}
}
return Ok(WorkspacesData::WorkspacesProject{});
}
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName)
{
try
{
auto savedWorkspacesJson = json::from_file(fileName);
if (savedWorkspacesJson.has_value())
{
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
if (savedWorkspaces.has_value())
{
return Ok(savedWorkspaces.value());
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
else
{
Logger::critical("Incorrect Workspaces file");
return Error(WorkspacesFileError::IncorrectFileError);
}
}
catch (std::exception ex)
{
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
return Error(WorkspacesFileError::FileReadingError);
}
}
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
return false;
}
return true;
}
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
return false;
}
return true;
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h>
namespace JsonUtils
{
enum class WorkspacesFileError
{
FileReadingError,
IncorrectFileError,
};
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName);
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName);
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects);
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project);
}

View File

@@ -0,0 +1,10 @@
#pragma once
// sync with WorkspacesLauncherUI : Data : LaunchingState.cs
enum class LaunchingState
{
Waiting = 0,
Launched,
LaunchedAndMoved,
Failed
};

View File

@@ -0,0 +1,66 @@
#include "pch.h"
#include "LaunchingStatus.h"
#include <common/logger/logger.h>
LaunchingStatus::LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> updateCallback) :
m_updateCallback(updateCallback)
{
std::unique_lock lock(m_mutex);
for (const auto& app : project.apps)
{
m_appsState.insert({ app, { app, nullptr, LaunchingState::Waiting } });
}
}
const WorkspacesData::LaunchingAppStateMap& LaunchingStatus::Get() noexcept
{
std::shared_lock lock(m_mutex);
return m_appsState;
}
bool LaunchingStatus::AllLaunchedAndMoved() noexcept
{
std::shared_lock lock(m_mutex);
for (const auto& [app, data] : m_appsState)
{
if (data.state != LaunchingState::Failed && data.state != LaunchingState::LaunchedAndMoved)
{
Logger::debug(data.state);
return false;
}
}
return true;
}
bool LaunchingStatus::AllLaunched() noexcept
{
std::shared_lock lock(m_mutex);
for (const auto& [app, data] : m_appsState)
{
if (data.state == LaunchingState::Waiting)
{
return false;
}
}
return true;
}
void LaunchingStatus::Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state)
{
std::unique_lock lock(m_mutex);
if (!m_appsState.contains(app))
{
Logger::error(L"Error updating state: app {} is not tracked in the project", app.name);
return;
}
m_appsState[app].state = state;
if (m_updateCallback)
{
m_updateCallback(m_appsState);
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <shared_mutex>
#include <WorkspacesLib/WorkspacesData.h>
class LaunchingStatus
{
public:
LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> updateCallback);
~LaunchingStatus() = default;
bool AllLaunchedAndMoved() noexcept;
bool AllLaunched() noexcept;
const WorkspacesData::LaunchingAppStateMap& Get() noexcept;
void Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state);
private:
WorkspacesData::LaunchingAppStateMap m_appsState;
std::function<void(const WorkspacesData::LaunchingAppStateMap&)> m_updateCallback;
std::shared_mutex m_mutex;
};

View File

@@ -0,0 +1,53 @@
#pragma once
#include <variant>
template<typename T>
class Ok
{
public:
explicit constexpr Ok(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename T>
class Error
{
public:
explicit constexpr Error(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename OkT, typename ErrT>
class Result
{
public:
using VariantT = std::variant<Ok<OkT>, Error<ErrT>>;
constexpr Result(Ok<OkT> value) :
variant(std::move(value))
{}
constexpr Result(Error<ErrT> value) :
variant(std::move(value))
{}
constexpr bool isOk() const { return std::holds_alternative<Ok<OkT>>(variant); }
constexpr bool isError() const { return std::holds_alternative<Error<ErrT>>(variant); }
constexpr OkT value() const { return std::get<Ok<OkT>>(variant).value; }
constexpr ErrT error() const { return std::get<Error<ErrT>>(variant).value; }
constexpr OkT&& getValue() { return std::get<Ok<OkT>>(variant).get(); }
constexpr ErrT&& getError() { return std::get<Error<ErrT>>(variant).get(); }
VariantT variant;
};

View File

@@ -22,12 +22,6 @@ namespace WorkspacesData
return settingsFolderPath + L"\\temp-workspaces.json"; return settingsFolderPath + L"\\temp-workspaces.json";
} }
std::wstring LaunchWorkspacesFile()
{
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return settingsFolderPath + L"\\launch-workspaces.json";
}
RECT WorkspacesProject::Application::Position::toRect() const noexcept RECT WorkspacesProject::Application::Position::toRect() const noexcept
{ {
return RECT{ .left = x, .top = y, .right = x + width, .bottom = y + height }; return RECT{ .left = x, .top = y, .right = x + width, .bottom = y + height };
@@ -420,19 +414,40 @@ namespace WorkspacesData
{ {
namespace NonLocalizable namespace NonLocalizable
{ {
const static wchar_t* NameID = L"name"; const static wchar_t* ApplicationID = L"application";
const static wchar_t* PathID = L"path";
const static wchar_t* StateID = L"state"; const static wchar_t* StateID = L"state";
} }
json::JsonObject ToJson(const AppLaunchInfo& data) json::JsonObject ToJson(const LaunchingAppState& data)
{ {
json::JsonObject json{}; json::JsonObject json{};
json.SetNamedValue(NonLocalizable::NameID, json::value(data.name)); json.SetNamedValue(NonLocalizable::ApplicationID, WorkspacesProjectJSON::ApplicationJSON::ToJson(data.application));
json.SetNamedValue(NonLocalizable::PathID, json::value(data.path)); json.SetNamedValue(NonLocalizable::StateID, json::value(static_cast<int>(data.state)));
json.SetNamedValue(NonLocalizable::StateID, json::value(data.state));
return json; return json;
} }
std::optional<LaunchingAppState> FromJson(const json::JsonObject& json)
{
LaunchingAppState result{};
try
{
auto app = WorkspacesProjectJSON::ApplicationJSON::FromJson(json.GetNamedObject(NonLocalizable::ApplicationID));
if (!app.has_value())
{
return std::nullopt;
}
result.application = app.value();
result.state = static_cast<LaunchingState>(json.GetNamedNumber(NonLocalizable::StateID));
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
} }
namespace AppLaunchInfoListJSON namespace AppLaunchInfoListJSON
@@ -442,18 +457,46 @@ namespace WorkspacesData
const static wchar_t* AppLaunchInfoID = L"appLaunchInfos"; const static wchar_t* AppLaunchInfoID = L"appLaunchInfos";
} }
json::JsonObject ToJson(const std::vector<AppLaunchInfo>& data) json::JsonObject ToJson(const LaunchingAppStateMap& data)
{ {
json::JsonObject json{}; json::JsonObject json{};
json::JsonArray appLaunchInfoArray{}; json::JsonArray appLaunchInfoArray{};
for (const auto& appLaunchInfo : data) for (const auto& appLaunchInfo : data)
{ {
appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo)); appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo.second));
} }
json.SetNamedValue(NonLocalizable::AppLaunchInfoID, appLaunchInfoArray); json.SetNamedValue(NonLocalizable::AppLaunchInfoID, appLaunchInfoArray);
return json; return json;
} }
std::optional<LaunchingAppStateMap> FromJson(const json::JsonObject& json)
{
LaunchingAppStateMap result{};
try
{
auto array = json.GetNamedArray(NonLocalizable::AppLaunchInfoID);
for (uint32_t i = 0; i < array.Size(); ++i)
{
auto obj = AppLaunchInfoJSON::FromJson(array.GetObjectAt(i));
if (obj.has_value())
{
result.insert({ obj.value().application, obj.value() });
}
else
{
return std::nullopt;
}
}
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
} }
namespace AppLaunchDataJSON namespace AppLaunchDataJSON
@@ -467,7 +510,7 @@ namespace WorkspacesData
json::JsonObject ToJson(const AppLaunchData& data) json::JsonObject ToJson(const AppLaunchData& data)
{ {
json::JsonObject json{}; json::JsonObject json{};
json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appLaunchInfoList)); json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appsStateList));
json.SetNamedValue(NonLocalizable::ProcessID, json::value(data.launcherProcessID)); json.SetNamedValue(NonLocalizable::ProcessID, json::value(data.launcherProcessID));
return json; return json;
} }

View File

@@ -2,11 +2,12 @@
#include <common/utils/json.h> #include <common/utils/json.h>
#include <WorkspacesLib/LaunchingStateEnum.h>
namespace WorkspacesData namespace WorkspacesData
{ {
std::wstring WorkspacesFile(); std::wstring WorkspacesFile();
std::wstring TempWorkspacesFile(); std::wstring TempWorkspacesFile();
std::wstring LaunchWorkspacesFile();
struct WorkspacesProject struct WorkspacesProject
{ {
@@ -21,10 +22,7 @@ namespace WorkspacesData
RECT toRect() const noexcept; RECT toRect() const noexcept;
inline bool operator==(const Position& other) const noexcept auto operator<=>(const Position&) const = default;
{
return x == other.x && y == other.y && width == other.width && height == other.height;
}
}; };
std::wstring name; std::wstring name;
@@ -39,6 +37,8 @@ namespace WorkspacesData
bool isMaximized{}; bool isMaximized{};
Position position{}; Position position{};
unsigned int monitor{}; unsigned int monitor{};
auto operator<=>(const Application&) const = default;
}; };
struct Monitor struct Monitor
@@ -80,34 +80,22 @@ namespace WorkspacesData
std::vector<WorkspacesProject> projects; std::vector<WorkspacesProject> projects;
}; };
struct AppLaunchInfo struct LaunchingAppState
{ {
std::wstring name; WorkspacesData::WorkspacesProject::Application application;
std::wstring path; HWND window{};
std::wstring state; LaunchingState state { LaunchingState::Waiting };
}; };
namespace AppLaunchInfoJSON using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;
{ using LaunchingAppStateList = std::vector<std::pair<WorkspacesData::WorkspacesProject::Application, LaunchingState>>;
json::JsonObject ToJson(const AppLaunchInfo& data);
}
namespace AppLaunchInfoListJSON
{
json::JsonObject ToJson(const std::vector<AppLaunchInfo>& data);
}
struct AppLaunchData struct AppLaunchData
{ {
std::vector<AppLaunchInfo> appLaunchInfoList; LaunchingAppStateMap appsStateList;
int launcherProcessID = 0; int launcherProcessID = 0;
}; };
namespace AppLaunchDataJSON
{
json::JsonObject ToJson(const AppLaunchData& data);
}
namespace WorkspacesProjectJSON namespace WorkspacesProjectJSON
{ {
namespace ApplicationJSON namespace ApplicationJSON
@@ -143,4 +131,22 @@ namespace WorkspacesData
json::JsonObject ToJson(const std::vector<WorkspacesProject>& data); json::JsonObject ToJson(const std::vector<WorkspacesProject>& data);
std::optional<std::vector<WorkspacesProject>> FromJson(const json::JsonObject& json); std::optional<std::vector<WorkspacesProject>> FromJson(const json::JsonObject& json);
} }
namespace AppLaunchInfoJSON
{
json::JsonObject ToJson(const LaunchingAppState& data);
std::optional<LaunchingAppState> FromJson(const json::JsonObject& json);
}
namespace AppLaunchInfoListJSON
{
json::JsonObject ToJson(const LaunchingAppStateMap& data);
std::optional<LaunchingAppStateMap> FromJson(const json::JsonObject& json);
}
namespace AppLaunchDataJSON
{
json::JsonObject ToJson(const AppLaunchData& data);
}
}; };

View File

@@ -33,19 +33,32 @@
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="AppUtils.h" /> <ClInclude Include="AppUtils.h" />
<ClInclude Include="IPCHelper.h" />
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="LaunchingStateEnum.h" />
<ClInclude Include="LaunchingStatus.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WorkspacesData.h" /> <ClInclude Include="WorkspacesData.h" />
<ClInclude Include="trace.h" /> <ClInclude Include="trace.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="AppUtils.cpp" /> <ClCompile Include="AppUtils.cpp" />
<ClCompile Include="IPCHelper.cpp" />
<ClCompile Include="JsonUtils.cpp" />
<ClCompile Include="LaunchingStatus.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WorkspacesData.cpp" /> <ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" /> <ClCompile Include="trace.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj">
<Project>{f055103b-f80b-4d0c-bf48-057c55620033}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj"> <ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project> <Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference> </ProjectReference>

View File

@@ -23,6 +23,24 @@
<ClInclude Include="AppUtils.h"> <ClInclude Include="AppUtils.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="Result.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IPCHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStateEnum.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStatus.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
@@ -37,6 +55,18 @@
<ClCompile Include="AppUtils.cpp"> <ClCompile Include="AppUtils.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="JsonUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="IPCHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LaunchingStatus.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -0,0 +1,469 @@
#include "pch.h"
#include <common/interop/two_way_pipe_message_ipc_impl.h>
#include <iterator>
constexpr DWORD BUFSIZE = 1024;
TwoWayPipeMessageIPC::TwoWayPipeMessageIPC(
std::wstring _input_pipe_name,
std::wstring _output_pipe_name,
callback_function p_func) :
impl(new TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl(
_input_pipe_name,
_output_pipe_name,
p_func))
{
}
TwoWayPipeMessageIPC::~TwoWayPipeMessageIPC()
{
delete impl;
}
void TwoWayPipeMessageIPC::send(std::wstring msg)
{
impl->send(msg);
}
void TwoWayPipeMessageIPC::start(HANDLE _restricted_pipe_token)
{
impl->start(_restricted_pipe_token);
}
void TwoWayPipeMessageIPC::end()
{
impl->end();
}
TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::TwoWayPipeMessageIPCImpl(
std::wstring _input_pipe_name,
std::wstring _output_pipe_name,
callback_function p_func)
{
input_pipe_name = _input_pipe_name;
output_pipe_name = _output_pipe_name;
dispatch_inc_message_function = p_func;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send(std::wstring msg)
{
output_queue.queue_message(msg);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start(HANDLE _restricted_pipe_token)
{
output_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_output_queue_thread, this);
input_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_input_queue_thread, this);
input_pipe_thread = std::thread(&TwoWayPipeMessageIPCImpl::start_named_pipe_server, this, _restricted_pipe_token);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::end()
{
closed = true;
input_queue.interrupt();
input_queue_thread.join();
output_queue.interrupt();
output_queue_thread.join();
pipe_connect_handle_mutex.lock();
if (current_connect_pipe_handle != NULL)
{
//Cancels the Pipe currently waiting for a connection.
CancelIoEx(current_connect_pipe_handle, NULL);
}
pipe_connect_handle_mutex.unlock();
input_pipe_thread.join();
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send_pipe_message(std::wstring message)
{
// Adapted from https://learn.microsoft.com/windows/win32/ipc/named-pipe-client
HANDLE output_pipe_handle;
const wchar_t* message_send = message.c_str();
BOOL fSuccess = FALSE;
DWORD cbToWrite, cbWritten, dwMode;
const wchar_t* lpszPipename = output_pipe_name.c_str();
// Try to open a named pipe; wait for it, if necessary.
while (1)
{
output_pipe_handle = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// Break if the pipe handle is valid.
if (output_pipe_handle != INVALID_HANDLE_VALUE)
break;
// Exit if an error other than ERROR_PIPE_BUSY occurs.
DWORD curr_error = 0;
if ((curr_error = GetLastError()) != ERROR_PIPE_BUSY)
{
return;
}
// All pipe instances are busy, so wait for 20 seconds.
if (!WaitNamedPipe(lpszPipename, 20000))
{
return;
}
}
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
output_pipe_handle, // pipe handle
&dwMode, // new pipe mode
NULL, // don't set maximum bytes
NULL); // don't set maximum time
if (!fSuccess)
{
return;
}
// Send a message to the pipe server.
cbToWrite = (lstrlen(message_send)) * sizeof(WCHAR); // no need to send final '\0'. Pipe is in message mode.
fSuccess = WriteFile(
output_pipe_handle, // pipe handle
message_send, // message
cbToWrite, // message length
&cbWritten, // bytes written
NULL); // not overlapped
if (!fSuccess)
{
return;
}
CloseHandle(output_pipe_handle);
return;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_output_queue_thread()
{
while (!closed)
{
std::wstring message = output_queue.pop_message();
if (message.length() == 0)
{
break;
}
send_pipe_message(message);
}
}
BOOL TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::GetLogonSID(HANDLE hToken, PSID* ppsid)
{
// From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85)
BOOL bSuccess = FALSE;
DWORD dwIndex;
DWORD dwLength = 0;
PTOKEN_GROUPS ptg = NULL;
// Verify the parameter passed in is not NULL.
if (NULL == ppsid)
goto Cleanup;
// Get required buffer size and allocate the TOKEN_GROUPS buffer.
if (!GetTokenInformation(
hToken, // handle to the access token
TokenGroups, // get information about the token's groups
static_cast<LPVOID>(ptg), // pointer to TOKEN_GROUPS buffer
0, // size of buffer
&dwLength // receives required buffer size
))
{
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
goto Cleanup;
ptg = static_cast<PTOKEN_GROUPS>(HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
dwLength));
if (ptg == NULL)
goto Cleanup;
}
// Get the token group information from the access token.
if (!GetTokenInformation(
hToken, // handle to the access token
TokenGroups, // get information about the token's groups
static_cast<LPVOID>(ptg), // pointer to TOKEN_GROUPS buffer
dwLength, // size of buffer
&dwLength // receives required buffer size
))
{
goto Cleanup;
}
// Loop through the groups to find the logon SID.
for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++)
if ((ptg->Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID)
{
// Found the logon SID; make a copy of it.
dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid);
*ppsid = static_cast<PSID>(HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
dwLength));
if (*ppsid == NULL)
goto Cleanup;
if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid))
{
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(*ppsid));
goto Cleanup;
}
break;
}
bSuccess = TRUE;
Cleanup:
// Free the buffer for the token groups.
if (ptg != NULL)
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(ptg));
return bSuccess;
}
VOID TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::FreeLogonSID(PSID* ppsid)
{
// From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85)
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(*ppsid));
}
int TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::change_pipe_security_allow_restricted_token(HANDLE handle, HANDLE token)
{
PACL old_dacl, new_dacl;
PSECURITY_DESCRIPTOR sd;
EXPLICIT_ACCESS ea;
PSID user_restricted;
int error;
if (!GetLogonSID(token, &user_restricted))
{
error = 5; // No access error.
goto Ldone;
}
if (GetSecurityInfo(handle,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
NULL,
NULL,
&old_dacl,
NULL,
&sd))
{
error = GetLastError();
goto Lclean_sid;
}
memset(&ea, 0, sizeof(EXPLICIT_ACCESS));
ea.grfAccessPermissions |= GENERIC_READ | FILE_WRITE_ATTRIBUTES;
ea.grfAccessPermissions |= GENERIC_WRITE | FILE_READ_ATTRIBUTES;
ea.grfAccessPermissions |= SYNCHRONIZE;
ea.grfAccessMode = SET_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
ea.Trustee.ptstrName = static_cast<LPTSTR>(user_restricted);
if (SetEntriesInAcl(1, &ea, old_dacl, &new_dacl))
{
error = GetLastError();
goto Lclean_sd;
}
if (SetSecurityInfo(handle,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
NULL,
NULL,
new_dacl,
NULL))
{
error = GetLastError();
goto Lclean_dacl;
}
error = 0;
Lclean_dacl:
LocalFree(static_cast<HLOCAL>(new_dacl));
Lclean_sd:
LocalFree(static_cast<HLOCAL>(sd));
Lclean_sid:
FreeLogonSID(&user_restricted);
Ldone:
return error;
}
HANDLE TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::create_medium_integrity_token()
{
HANDLE restricted_token_handle;
SAFER_LEVEL_HANDLE level_handle = NULL;
DWORD sid_size = SECURITY_MAX_SID_SIZE;
BYTE medium_sid[SECURITY_MAX_SID_SIZE];
if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &level_handle, NULL))
{
return NULL;
}
if (!SaferComputeTokenFromLevel(level_handle, NULL, &restricted_token_handle, 0, NULL))
{
SaferCloseLevel(level_handle);
return NULL;
}
SaferCloseLevel(level_handle);
if (!CreateWellKnownSid(WinMediumLabelSid, nullptr, medium_sid, &sid_size))
{
CloseHandle(restricted_token_handle);
return NULL;
}
TOKEN_MANDATORY_LABEL integrity_level = { 0 };
integrity_level.Label.Attributes = SE_GROUP_INTEGRITY;
integrity_level.Label.Sid = reinterpret_cast<PSID>(medium_sid);
if (!SetTokenInformation(restricted_token_handle, TokenIntegrityLevel, &integrity_level, sizeof(integrity_level)))
{
CloseHandle(restricted_token_handle);
return NULL;
}
return restricted_token_handle;
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::handle_pipe_connection(HANDLE input_pipe_handle)
{
if (!input_pipe_handle)
{
return;
}
constexpr DWORD readBlockBytes = BUFSIZE;
std::wstring message;
size_t iBlock = 0;
message.reserve(BUFSIZE);
bool ok;
do
{
constexpr size_t charsPerBlock = readBlockBytes / sizeof(message[0]);
message.resize(message.size() + charsPerBlock);
DWORD bytesRead = 0;
ok = ReadFile(
input_pipe_handle,
// read the message directly into the string block by block simultaneously resizing it
message.data() + iBlock * charsPerBlock,
readBlockBytes,
&bytesRead,
nullptr);
if (!ok && GetLastError() != ERROR_MORE_DATA)
{
break;
}
iBlock++;
} while (!ok);
// trim the message's buffer
const auto nullCharPos = message.find_last_not_of(L'\0');
if (nullCharPos != std::wstring::npos)
{
message.resize(nullCharPos + 1);
}
input_queue.queue_message(std::move(message));
// Flush the pipe to allow the client to read the pipe's contents
// before disconnecting. Then disconnect the pipe, and close the
// handle to this pipe instance.
FlushFileBuffers(input_pipe_handle);
DisconnectNamedPipe(input_pipe_handle);
CloseHandle(input_pipe_handle);
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start_named_pipe_server(HANDLE token)
{
// Adapted from https://learn.microsoft.com/windows/win32/ipc/multithreaded-pipe-server
const wchar_t* pipe_name = input_pipe_name.c_str();
BOOL connected = FALSE;
HANDLE connect_pipe_handle = INVALID_HANDLE_VALUE;
while (!closed)
{
{
std::unique_lock lock(pipe_connect_handle_mutex);
connect_pipe_handle = CreateNamedPipe(
pipe_name,
PIPE_ACCESS_DUPLEX |
WRITE_DAC,
PIPE_TYPE_MESSAGE |
PIPE_READMODE_MESSAGE |
PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFSIZE,
BUFSIZE,
0,
NULL);
if (connect_pipe_handle == INVALID_HANDLE_VALUE)
{
return;
}
if (token != NULL)
{
change_pipe_security_allow_restricted_token(connect_pipe_handle, token);
}
current_connect_pipe_handle = connect_pipe_handle;
}
connected = ConnectNamedPipe(connect_pipe_handle, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
{
std::unique_lock lock(pipe_connect_handle_mutex);
current_connect_pipe_handle = NULL;
}
if (connected)
{
std::thread(&TwoWayPipeMessageIPCImpl::handle_pipe_connection, this, connect_pipe_handle).detach();
}
else
{
// Client could not connect.
CloseHandle(connect_pipe_handle);
}
}
}
void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_input_queue_thread()
{
while (!closed)
{
outgoing_message = L"";
std::wstring message = input_queue.pop_message();
if (message.length() == 0)
{
break;
}
// Check if callback method exists first before trying to call it.
// otherwise just store the response message in a variable.
if (dispatch_inc_message_function != nullptr)
{
dispatch_inc_message_function(message);
}
outgoing_message = message;
}
}

View File

@@ -3,12 +3,12 @@
#include <vector> #include <vector>
#include <string> #include <string>
std::vector<std::string> split(std::string s, const std::string& delimiter) std::vector<std::wstring> split(std::wstring s, const std::wstring& delimiter)
{ {
std::vector<std::string> tokens; std::vector<std::wstring> tokens;
size_t pos = 0; size_t pos = 0;
std::string token; std::wstring token;
while ((pos = s.find(delimiter)) != std::string::npos) while ((pos = s.find(delimiter)) != std::wstring::npos)
{ {
token = s.substr(0, pos); token = s.substr(0, pos);
tokens.push_back(token); tokens.push_back(token);

View File

@@ -20,6 +20,7 @@
// Non-localizable // Non-localizable
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe"; const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
const std::wstring workspacesWindowArrangerPath = L"PowerToys.WorkspacesWindowArranger.exe";
const std::wstring workspacesSnapshotToolPath = L"PowerToys.WorkspacesSnapshotTool.exe"; const std::wstring workspacesSnapshotToolPath = L"PowerToys.WorkspacesSnapshotTool.exe";
const std::wstring workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe"; const std::wstring workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe";

View File

@@ -1,57 +0,0 @@
#pragma once
#include <vector>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/logger/logger.h>
namespace WorkspacesJsonUtils
{
inline std::vector<WorkspacesData::WorkspacesProject> Read(const std::wstring& fileName)
{
std::vector<WorkspacesData::WorkspacesProject> projects{};
try
{
auto savedProjectsJson = json::from_file(fileName);
if (savedProjectsJson.has_value())
{
auto savedProjects = WorkspacesData::WorkspacesListJSON::FromJson(savedProjectsJson.value());
if (savedProjects.has_value())
{
projects = savedProjects.value();
}
}
}
catch (std::exception ex)
{
Logger::error("Error reading workspaces file. {}", ex.what());
}
return projects;
}
inline void Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
}
}
inline void Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project)
{
try
{
json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project));
}
catch (std::exception ex)
{
Logger::error("Error writing workspaces file. {}", ex.what());
}
}
}

View File

@@ -133,7 +133,6 @@
<ClCompile Include="SnapshotUtils.cpp" /> <ClCompile Include="SnapshotUtils.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="resource.base.h" /> <ClInclude Include="resource.base.h" />
<ClInclude Include="SnapshotUtils.h" /> <ClInclude Include="SnapshotUtils.h" />

View File

@@ -18,9 +18,6 @@
<ClInclude Include="pch.h"> <ClInclude Include="pch.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SnapshotUtils.h"> <ClInclude Include="SnapshotUtils.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>

View File

@@ -5,21 +5,21 @@
#include <workspaces-common/GuidUtils.h> #include <workspaces-common/GuidUtils.h>
#include <workspaces-common/MonitorUtils.h> #include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/WorkspacesData.h> #include <WorkspacesLib/WorkspacesData.h>
#include <JsonUtils.h>
#include <SnapshotUtils.h> #include <SnapshotUtils.h>
#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>
const std::wstring moduleName = L"Workspaces\\ProjectsSnapshotTool"; const std::wstring moduleName = L"Workspaces\\WorkspacesSnapshotTool";
const std::wstring internalPath = L""; const std::wstring internalPath = L"";
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cmdShow) int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cmdShow)
{ {
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesLauncherLoggerName); LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesSnapshotToolLoggerName);
InitUnhandledExceptionHandler(); InitUnhandledExceptionHandler();
if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
@@ -46,14 +46,6 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
return -1; return -1;
} }
std::wstring fileName = WorkspacesData::WorkspacesFile();
std::string cmdLineStr(cmdLine);
if (!cmdLineStr.empty())
{
std::wstring fileNameParam(cmdLineStr.begin(), cmdLineStr.end());
fileName = fileNameParam;
}
// 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 };
@@ -75,7 +67,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
return monitorNumber; return monitorNumber;
}); });
WorkspacesJsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project); JsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project);
Logger::trace(L"WorkspacesProject {}:{} created", project.name, project.id); Logger::trace(L"WorkspacesProject {}:{} created", project.name, project.id);
CoUninitialize(); CoUninitialize();

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,257 @@
#include "pch.h"
#include "WindowArranger.h"
#include <common/logger/logger.h>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <workspaces-common/MonitorUtils.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <workspaces-common/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
namespace FancyZones
{
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
DPIAware::Convert(monitor, rect);
auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset);
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
}
inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (isMinimized)
{
placement.showCmd = SW_MINIMIZE;
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
// make sure window is moved to the correct monitor before maximize.
if (isMaximized)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
}
WindowArranger::WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper) :
m_project(project),
m_windowsBefore(WindowEnumerator::Enumerate(WindowFilter::Filter)),
m_monitors(MonitorUtils::IdentifyMonitors()),
m_installedApps(Utils::Apps::GetAppsList()),
//m_windowCreationHandler(std::bind(&WindowArranger::onWindowCreated, this, std::placeholders::_1)),
m_ipcHelper(ipcHelper)
{
for (auto& app : project.apps)
{
m_launchingApps.insert({ app, { app, nullptr } });
}
m_ipcHelper.send(L"ready");
for (int attempt = 0; attempt < 50 && !allWindowsFound(); attempt++)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::vector<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> windowsDiff{};
std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(m_windowsBefore.begin(), m_windowsBefore.end(), window) == m_windowsBefore.end(); });
for (HWND window : windowsDiff)
{
processWindow(window);
}
}
bool allFound = allWindowsFound();
Logger::info(L"Finished moving new windows, all windows found: {}", allFound);
if (!allFound)
{
std::vector<HWND> allWindows = WindowEnumerator::Enumerate(WindowFilter::Filter);
for (HWND window : allWindows)
{
processWindow(window);
}
}
}
//void WindowArranger::onWindowCreated(HWND window)
//{
// if (!WindowFilter::Filter(window))
// {
// return;
// }
//
// processWindow(window);
//}
void WindowArranger::processWindow(HWND window)
{
// check if this window is already handled
auto windowIter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val) { return val.second.window == window; });
if (windowIter != m_launchingApps.end())
{
return;
}
RECT rect = WindowUtils::GetWindowRect(window);
if (rect.right - rect.left <= 0 || rect.bottom - rect.top <= 0)
{
return;
}
std::wstring title = WindowUtils::GetWindowTitle(window);
if (title.empty())
{
return;
}
std::wstring processPath = get_process_path(window);
if (processPath.empty())
{
return;
}
auto data = Utils::Apps::GetApp(processPath, m_installedApps);
if (!data.has_value() || data->name.empty())
{
return;
}
auto iter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val)
{ return val.second.state == LaunchingState::Waiting && val.first.name == data.value().name; });
if (iter == m_launchingApps.end())
{
Logger::info(L"A window of {} is not in the project", processPath);
return;
}
Logger::debug(L"Move {}", title);
iter->second.window = window;
if (moveWindow(window, iter->first))
{
iter->second.state = LaunchingState::LaunchedAndMoved;
}
else
{
iter->second.state = LaunchingState::Failed;
}
m_ipcHelper.send(WorkspacesData::AppLaunchInfoJSON::ToJson({iter->first, nullptr, iter->second.state}).ToString().c_str());
}
bool WindowArranger::moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app)
{
auto snapMonitorIter = std::find_if(m_project.monitors.begin(), m_project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (snapMonitorIter == m_project.monitors.end())
{
Logger::error(L"No monitor saved for launching the app");
return false;
}
bool launchMinimized = app.isMinimized;
bool launchMaximized = app.isMaximized;
HMONITOR currentMonitor{};
UINT currentDpi = DPIAware::DEFAULT_DPI;
auto currentMonitorIter = std::find_if(m_monitors.begin(), m_monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (currentMonitorIter != m_monitors.end())
{
currentMonitor = currentMonitorIter->monitor;
currentDpi = currentMonitorIter->dpi;
}
else
{
currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);
DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi);
launchMinimized = true;
launchMaximized = false;
}
RECT rect = app.position.toRect();
float mult = static_cast<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
return true;
}
else
{
Logger::error(L"Failed placing {}", app.name);
return false;
}
}
bool WindowArranger::allWindowsFound() const
{
return std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const std::pair<WorkspacesData::WorkspacesProject::Application, WorkspacesData::LaunchingAppState>& val) {
return val.second.window == nullptr;
}) == m_launchingApps.end();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <WindowCreationHandler.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
class WindowArranger
{
public:
WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper);
~WindowArranger() = default;
private:
const WorkspacesData::WorkspacesProject m_project;
const std::vector<HWND> m_windowsBefore;
const std::vector<WorkspacesData::WorkspacesProject::Monitor> m_monitors;
const Utils::Apps::AppList m_installedApps;
//const WindowCreationHandler m_windowCreationHandler;
const IPCHelper& m_ipcHelper;
WorkspacesData::LaunchingAppStateMap m_launchingApps{};
//void onWindowCreated(HWND window);
void processWindow(HWND window);
bool moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app);
bool allWindowsFound() const;
};

View File

@@ -0,0 +1,60 @@
#include "pch.h"
#include "WindowCreationHandler.h"
WindowCreationHandler::WindowCreationHandler(std::function<void(HWND)> windowCreatedCallback) :
m_windowCreatedCallback(windowCreatedCallback)
{
s_instance = this;
InitHooks();
}
WindowCreationHandler::~WindowCreationHandler()
{
m_staticWinEventHooks.erase(std::remove_if(begin(m_staticWinEventHooks),
end(m_staticWinEventHooks),
[](const HWINEVENTHOOK hook) {
return UnhookWinEvent(hook);
}),
end(m_staticWinEventHooks));
}
void WindowCreationHandler::InitHooks()
{
std::array<DWORD, 3> events_to_subscribe = {
EVENT_OBJECT_UNCLOAKED,
EVENT_OBJECT_SHOW,
EVENT_OBJECT_CREATE
};
for (const auto event : events_to_subscribe)
{
auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (hook)
{
m_staticWinEventHooks.emplace_back(hook);
}
else
{
Logger::error(L"Failed to initialize win event hooks");
}
}
}
void WindowCreationHandler::HandleWinHookEvent(DWORD event, HWND window) noexcept
{
switch (event)
{
//case EVENT_OBJECT_UNCLOAKED:
//case EVENT_OBJECT_SHOW:
case EVENT_OBJECT_CREATE:
{
if (m_windowCreatedCallback)
{
m_windowCreatedCallback(window);
}
}
break;
default:
break;
}
}

View File

@@ -0,0 +1,30 @@
#pragma once
class WindowCreationHandler
{
public:
WindowCreationHandler(std::function<void(HWND)> windowCreatedCallback);
~WindowCreationHandler();
private:
static inline WindowCreationHandler* s_instance = nullptr;
std::vector<HWINEVENTHOOK> m_staticWinEventHooks;
std::function<void(HWND)> m_windowCreatedCallback;
void InitHooks();
void HandleWinHookEvent(DWORD event, HWND window) noexcept;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,
DWORD event,
HWND window,
LONG object,
LONG child,
DWORD eventThread,
DWORD eventTime)
{
if (s_instance)
{
s_instance->HandleWinHookEvent(event, window);
}
}
};

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Project configurations -->
<!-- Props that should be disabled while building on CI server -->
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesWindowArrangerResource.base.rc WorkspacesWindowArrangerResource.rc" />
</Target>
<!-- C++ source compile-specific things for all configurations -->
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
</Link>
<Lib>
<TreatLibWarningAsErrors>true</TreatLibWarningAsErrors>
</Lib>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for Debug/Release configurations -->
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{37D07516-4185-43A4-924F-3C7A5D95ECF6}</ProjectGuid>
<RootNamespace>WorkspacesWindowArranger</RootNamespace>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<SpectreMitigation>Spectre</SpectreMitigation>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>PowerToys.$(MSBuildProjectName)</TargetName>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="WindowArranger.cpp" />
<ClCompile Include="WindowCreationHandler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="WindowArranger.h" />
<ClInclude Include="WindowCreationHandler.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\WorkspacesLib\WorkspacesLib.vcxproj">
<Project>{b31fcc55-b5a4-4ea7-b414-2dceae6af332}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc" />
<None Include="WorkspacesWindowArrangerResource.base.rc" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowCreationHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowArranger.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowCreationHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowArranger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="WorkspacesWindowArrangerResource.base.rc">
<Filter>Resource Files</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<Filter>Resource Files</Filter>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,115 @@
#include "pch.h"
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/utils.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
#include <common/utils/window.h>
#include <WindowArranger.h>
const std::wstring moduleName = L"Workspaces\\WorkspacesWindowArranger";
const std::wstring internalPath = L"";
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cmdShow)
{
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesWindowArrangerLoggerName);
InitUnhandledExceptionHandler();
if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
{
Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
return 0;
}
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
std::wstring commandLine{ GetCommandLineW() };
if (commandLine.empty())
{
Logger::warn("Empty command line arguments");
return 1;
}
auto args = split(commandLine, L" ");
std::wstring id{};
if (args.size() == 1)
{
id = args[0];
}
else if (args.size() == 2)
{
id = args[1];
}
if (id.empty())
{
Logger::warn("Incorrect command line arguments: no workspace id");
return 1;
}
// read workspaces
std::vector<WorkspacesData::WorkspacesProject> workspaces;
WorkspacesData::WorkspacesProject projectToLaunch{};
// check the temp file in case the project is just created and not saved to the workspaces.json yet
if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile()))
{
auto file = WorkspacesData::TempWorkspacesFile();
auto res = JsonUtils::ReadSingleWorkspace(file);
if (res.isOk() && res.value().id == id)
{
projectToLaunch = res.getValue();
}
else
{
Logger::error(L"Error reading temp file");
return 1;
}
}
if (projectToLaunch.id.empty())
{
auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{
workspaces = res.getValue();
}
else
{
return 1;
}
for (const auto& proj : workspaces)
{
if (proj.id == id)
{
projectToLaunch = proj;
break;
}
}
}
if (projectToLaunch.id.empty())
{
Logger::critical(L"Workspace {} not found", id);
return 1;
}
// IPC
IPCHelper ipc(IPCHelperStrings::WindowArrangerPipeName, IPCHelperStrings::LauncherArrangerPipeName, nullptr);
// arrange windows
Logger::info(L"Arrange windows from Workspace {} : {}", projectToLaunch.name, projectToLaunch.id);
WindowArranger windowArranger(projectToLaunch, ipc);
//run_message_loop();
Logger::debug(L"Arranger finished");
CoUninitialize();
return 0;
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,6 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <common/logger/logger.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by WorkspacesWindowArrangerResource.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Workspaces Window Arranger"
#define INTERNAL_NAME "PowerToys.WorkspacesWindowArranger"
#define ORIGINAL_FILENAME "PowerToys.WorkspacesWindowArranger.exe"
// Non-localizable
//////////////////////////////

View File

@@ -21,6 +21,7 @@ namespace WindowUtils
const wchar_t WorkspacesSnapshotTool[] = L"POWERTOYS.WORKSPACESSNAPSHOTTOOL"; const wchar_t WorkspacesSnapshotTool[] = L"POWERTOYS.WORKSPACESSNAPSHOTTOOL";
const wchar_t WorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR"; const wchar_t WorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR";
const wchar_t WorkspacesLauncher[] = L"POWERTOYS.WORKSPACESLAUNCHER"; const wchar_t WorkspacesLauncher[] = L"POWERTOYS.WORKSPACESLAUNCHER";
const wchar_t WorkspacesWindowArranger[] = L"POWERTOYS.WORKSPACESWINDOWARRANGER";
} }
inline bool IsRoot(HWND window) noexcept inline bool IsRoot(HWND window) noexcept
@@ -80,6 +81,7 @@ namespace WindowUtils
NonLocalizable::HelpWindow, NonLocalizable::HelpWindow,
NonLocalizable::WorkspacesEditor, NonLocalizable::WorkspacesEditor,
NonLocalizable::WorkspacesLauncher, NonLocalizable::WorkspacesLauncher,
NonLocalizable::WorkspacesWindowArranger,
NonLocalizable::WorkspacesSnapshotTool, NonLocalizable::WorkspacesSnapshotTool,
}; };
return (check_excluded_app(window, processPathUpper, defaultExcludedApps)); return (check_excluded_app(window, processPathUpper, defaultExcludedApps));

View File

@@ -46,5 +46,6 @@ std::vector<std::wstring> processes =
L"PowerToys.WorkspacesSnapshotTool.exe", L"PowerToys.WorkspacesSnapshotTool.exe",
L"PowerToys.WorkspacesLauncher.exe", L"PowerToys.WorkspacesLauncher.exe",
L"PowerToys.WorkspacesLauncherUI.exe", L"PowerToys.WorkspacesLauncherUI.exe",
L"PowerToys.WorkspacesWindowArranger.exe",
L"PowerToys.WorkspacesEditor.exe", L"PowerToys.WorkspacesEditor.exe",
}; };