[Workspaces] Handle admin windows repositioning. (#34965)
This commit is contained in:
Родитель
499dc9bb7a
Коммит
1e18e83af6
|
@ -1861,6 +1861,7 @@ workarounds
|
|||
WORKSPACESEDITOR
|
||||
WORKSPACESLAUNCHER
|
||||
WORKSPACESSNAPSHOTTOOL
|
||||
WORKSPACESWINDOWARRANGER
|
||||
wox
|
||||
wparam
|
||||
wpf
|
||||
|
|
|
@ -194,6 +194,7 @@
|
|||
|
||||
"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
"PowerToys.WorkspacesLauncher.exe",
|
||||
"PowerToys.WorkspacesWindowArranger.exe",
|
||||
"PowerToys.WorkspacesEditor.exe",
|
||||
"PowerToys.WorkspacesEditor.dll",
|
||||
"PowerToys.WorkspacesLauncherUI.exe",
|
||||
|
|
|
@ -619,6 +619,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\mod
|
|||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesWindowArranger", "src\modules\Workspaces\WorkspacesWindowArranger\WorkspacesWindowArranger.vcxproj", "{37D07516-4185-43A4-924F-3C7A5D95ECF6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -2946,6 +2960,7 @@ Global
|
|||
{3D63307B-9D27-44FD-B033-B26F39245B85} = {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}
|
||||
{37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
|
|
@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
|||
}
|
||||
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.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
|
@ -1259,6 +1259,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
|||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"PowerToys.exe",
|
||||
};
|
||||
|
||||
|
|
|
@ -72,6 +72,8 @@ struct LogSettings
|
|||
inline const static std::string newLoggerName = "NewPlus";
|
||||
inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher";
|
||||
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::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt";
|
||||
inline const static int retention = 30;
|
||||
|
|
|
@ -1,150 +1,27 @@
|
|||
#include "pch.h"
|
||||
#include "AppLauncher.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <winrt/Windows.Management.Deployment.h>
|
||||
#include <winrt/Windows.ApplicationModel.Core.h>
|
||||
|
||||
#include <shellapi.h>
|
||||
#include <ShellScalingApi.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <workspaces-common/MonitorEnumerator.h>
|
||||
#include <workspaces-common/WindowEnumerator.h>
|
||||
#include <workspaces-common/WindowFilter.h>
|
||||
#include <common/utils/winapi_error.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 <WindowProperties/WorkspacesWindowPropertyUtils.h>
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Windows::Foundation;
|
||||
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)
|
||||
{
|
||||
// Packaged apps have version in the path, it will be outdated after update.
|
||||
|
@ -160,322 +37,173 @@ namespace
|
|||
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)
|
||||
Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
|
||||
{
|
||||
return std::find_if(launchedApps.begin(), launchedApps.end(), [&](const LaunchingApp& val) {
|
||||
return val.window == nullptr;
|
||||
}) == launchedApps.end();
|
||||
};
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
|
||||
bool AddOpenedWindows(LaunchingApps& launchedApps, const std::vector<HWND>& windows, const Utils::Apps::AppList& installedApps)
|
||||
{
|
||||
bool statusChanged = false;
|
||||
for (HWND window : windows)
|
||||
SHELLEXECUTEINFO sei = { 0 };
|
||||
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
||||
sei.hwnd = nullptr;
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
|
||||
sei.lpVerb = elevated ? L"runas" : L"open";
|
||||
sei.lpFile = appPath.c_str();
|
||||
sei.lpParameters = commandLineArgs.c_str();
|
||||
sei.lpDirectory = dir.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
if (!ShellExecuteEx(&sei))
|
||||
{
|
||||
auto installedAppData = Utils::Apps::GetApp(window, installedApps);
|
||||
if (!installedAppData.has_value())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::wstring error = get_last_error_or_default(GetLastError());
|
||||
Logger::error(L"Failed to launch process. {}", error);
|
||||
return Error(error);
|
||||
}
|
||||
|
||||
auto insertionIter = launchedApps.end();
|
||||
for (auto iter = launchedApps.begin(); iter != launchedApps.end(); ++iter)
|
||||
return Ok(sei);
|
||||
}
|
||||
|
||||
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
|
||||
{
|
||||
try
|
||||
{
|
||||
PackageManager packageManager;
|
||||
for (const auto& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
if (iter->window == nullptr && installedAppData.value().name == iter->application.name)
|
||||
if (package.Id().FullName() == packageFullName)
|
||||
{
|
||||
insertionIter = iter;
|
||||
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
|
||||
auto appEntries = getAppListEntriesOperation.get();
|
||||
|
||||
if (appEntries.Size() > 0)
|
||||
{
|
||||
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
|
||||
bool launchResult = launchOperation.get();
|
||||
return launchResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"No app entries found for the package.");
|
||||
launchErrors.push_back({ packageFullName, L"No app entries found for the package." });
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
{
|
||||
insertionIter->window = window;
|
||||
insertionIter->state = L"launched";
|
||||
statusChanged = true;
|
||||
}
|
||||
|
||||
if (AllWindowsFound(launchedApps))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return statusChanged;
|
||||
}
|
||||
}
|
||||
catch (const hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Packaged app launching error: {}", ex.message());
|
||||
launchErrors.push_back({ packageFullName, ex.message().c_str() });
|
||||
}
|
||||
|
||||
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors)
|
||||
{
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
|
||||
SHELLEXECUTEINFO sei = { 0 };
|
||||
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
||||
sei.hwnd = nullptr;
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
|
||||
sei.lpVerb = elevated ? L"runas" : L"open";
|
||||
sei.lpFile = appPath.c_str();
|
||||
sei.lpParameters = commandLineArgs.c_str();
|
||||
sei.lpDirectory = dir.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
if (!ShellExecuteEx(&sei))
|
||||
{
|
||||
auto error = GetLastError();
|
||||
Logger::error(L"Failed to launch process. {}", get_last_error_or_default(error));
|
||||
launchErrors.push_back({ std::filesystem::path(appPath).filename(), get_last_error_or_default(error) });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
|
||||
{
|
||||
try
|
||||
bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors)
|
||||
{
|
||||
PackageManager packageManager;
|
||||
for (const auto& package : packageManager.FindPackagesForUser({}))
|
||||
{
|
||||
if (package.Id().FullName() == packageFullName)
|
||||
{
|
||||
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
|
||||
auto appEntries = getAppListEntriesOperation.get();
|
||||
bool launched{ false };
|
||||
|
||||
if (appEntries.Size() > 0)
|
||||
// packaged apps: check protocol in registry
|
||||
// usage example: Settings with cmd args
|
||||
if (!app.packageFullName.empty())
|
||||
{
|
||||
auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName);
|
||||
if (!names.empty())
|
||||
{
|
||||
Logger::trace(L"Launching packaged by protocol with command line args {}", app.name);
|
||||
|
||||
std::wstring uriProtocolName = names[0];
|
||||
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
|
||||
|
||||
auto res = LaunchApp(command, L"", app.isElevated);
|
||||
if (res.isOk())
|
||||
{
|
||||
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
|
||||
bool launchResult = launchOperation.get();
|
||||
return launchResult;
|
||||
launched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"No app entries found for the package.");
|
||||
launchErrors.push_back({ packageFullName, L"No app entries found for the package." });
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const hresult_error& ex)
|
||||
{
|
||||
Logger::error(L"Packaged app launching error: {}", ex.message());
|
||||
launchErrors.push_back({ packageFullName, ex.message().c_str() });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors)
|
||||
{
|
||||
bool launched{ false };
|
||||
|
||||
// packaged apps: check protocol in registry
|
||||
// usage example: Settings with cmd args
|
||||
if (!app.packageFullName.empty())
|
||||
{
|
||||
auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName);
|
||||
if (!names.empty())
|
||||
{
|
||||
Logger::trace(L"Launching packaged by protocol with command line args {}", app.name);
|
||||
|
||||
std::wstring uriProtocolName = names[0];
|
||||
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
|
||||
|
||||
launched = LaunchApp(command, L"", app.isElevated, launchErrors);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"Uri protocol names not found for {}", app.packageFullName);
|
||||
}
|
||||
}
|
||||
|
||||
// packaged apps: try launching first by AppUserModel.ID
|
||||
// usage example: elevated Terminal
|
||||
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
|
||||
launched = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated, launchErrors);
|
||||
}
|
||||
|
||||
// packaged apps: try launching by package full name
|
||||
// doesn't work for elevated apps or apps with command line args
|
||||
if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated)
|
||||
{
|
||||
Logger::trace(L"Launching packaged app {}", app.name);
|
||||
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
|
||||
}
|
||||
|
||||
if (!launched)
|
||||
{
|
||||
Logger::trace(L"Launching {} at {}", app.name, app.path);
|
||||
|
||||
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
|
||||
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
Logger::error(L"File not found at {}", app.path);
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
|
||||
return false;
|
||||
}
|
||||
|
||||
launched = LaunchApp(app.path, app.commandLineArgs, app.isElevated, launchErrors);
|
||||
}
|
||||
|
||||
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
|
||||
return launched;
|
||||
}
|
||||
|
||||
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors)
|
||||
{
|
||||
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 launchedApps = Prepare(project.apps, installedApps);
|
||||
|
||||
uiHelper.UpdateLaunchStatus(launchedApps);
|
||||
|
||||
// Launch apps
|
||||
for (auto& app : launchedApps)
|
||||
{
|
||||
if (!app.window)
|
||||
{
|
||||
if (!Launch(app.application, launchErrors))
|
||||
else
|
||||
{
|
||||
Logger::error(L"Failed to launch {}", app.application.name);
|
||||
app.state = L"failed";
|
||||
uiHelper.UpdateLaunchStatus(launchedApps);
|
||||
launchedSuccessfully = false;
|
||||
Logger::info(L"Uri protocol names not found for {}", app.packageFullName);
|
||||
}
|
||||
}
|
||||
|
||||
// packaged apps: try launching first by AppUserModel.ID
|
||||
// usage example: elevated Terminal
|
||||
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
|
||||
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
|
||||
// doesn't work for elevated apps or apps with command line args
|
||||
if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated)
|
||||
{
|
||||
Logger::trace(L"Launching packaged app {}", app.name);
|
||||
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
|
||||
}
|
||||
|
||||
if (!launched)
|
||||
{
|
||||
Logger::trace(L"Launching {} at {}", app.name, app.path);
|
||||
|
||||
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
|
||||
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
Logger::error(L"File not found at {}", app.path);
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
return launched;
|
||||
}
|
||||
|
||||
// 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++)
|
||||
bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors)
|
||||
{
|
||||
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))
|
||||
bool launchedSuccessfully{ true };
|
||||
|
||||
auto installedApps = Utils::Apps::GetAppsList();
|
||||
UpdatePackagedApps(project.apps, installedApps);
|
||||
|
||||
// Launch apps
|
||||
for (auto& app : project.apps)
|
||||
{
|
||||
uiHelper.UpdateLaunchStatus(launchedApps);
|
||||
if (!Launch(app, launchErrors))
|
||||
{
|
||||
Logger::error(L"Failed to launch {}", app.name);
|
||||
launchingStatus.Update(app, LaunchingState::Failed);
|
||||
launchedSuccessfully = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
launchingStatus.Update(app, LaunchingState::Launched);
|
||||
}
|
||||
}
|
||||
|
||||
// check if all windows were found
|
||||
if (AllWindowsFound(launchedApps))
|
||||
{
|
||||
Logger::trace(L"All windows found.");
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Not all windows found, retry.");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
return launchedSuccessfully;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <WorkspacesLib/LaunchingStatus.h>
|
||||
#include <WorkspacesLib/Result.h>
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
|
||||
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
|
||||
namespace AppLauncher
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -7,12 +7,22 @@
|
|||
#include <common/utils/OnThreadExecutor.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
|
||||
#include <AppLauncher.h>
|
||||
|
||||
LauncherUIHelper::LauncherUIHelper() :
|
||||
m_processId{},
|
||||
m_ipcHelper(IPCHelperStrings::LauncherUIPipeName, IPCHelperStrings::UIPipeName, nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
LauncherUIHelper::~LauncherUIHelper()
|
||||
{
|
||||
OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] {
|
||||
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)
|
||||
{
|
||||
bool res = TerminateProcess(uiProcess, 0);
|
||||
|
@ -25,54 +35,39 @@ LauncherUIHelper::~LauncherUIHelper()
|
|||
{
|
||||
Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
|
||||
std::filesystem::remove(WorkspacesData::LaunchWorkspacesFile());
|
||||
} }).wait();
|
||||
}
|
||||
|
||||
void LauncherUIHelper::LaunchUI()
|
||||
{
|
||||
Logger::trace(L"Starting WorkspacesLauncherUI");
|
||||
|
||||
STARTUPINFO info = { sizeof(info) };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
TCHAR buffer[MAX_PATH] = { 0 };
|
||||
GetModuleFileName(NULL, buffer, MAX_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);
|
||||
if (succeeded)
|
||||
|
||||
auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesLauncherUI.exe", L"", false);
|
||||
if (res.isOk())
|
||||
{
|
||||
if (pi.hProcess)
|
||||
{
|
||||
uiProcessId = pi.dwProcessId;
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if (pi.hThread)
|
||||
{
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
auto value = res.value();
|
||||
m_processId = GetProcessId(value.hProcess);
|
||||
CloseHandle(value.hProcess);
|
||||
Logger::info(L"WorkspacesLauncherUI started with pid {}", m_processId);
|
||||
}
|
||||
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();
|
||||
appData.appLaunchInfoList.reserve(launchedApps.size());
|
||||
WorkspacesData::AppLaunchData appData;
|
||||
appData.launcherProcessID = GetCurrentProcessId();
|
||||
for (auto& app : launchedApps)
|
||||
for (auto& [app, data] : launchedApps)
|
||||
{
|
||||
WorkspacesData::AppLaunchInfo appLaunchInfo = WorkspacesData::AppLaunchInfo();
|
||||
appLaunchInfo.name = app.application.name;
|
||||
appLaunchInfo.path = app.application.path;
|
||||
appLaunchInfo.state = app.state;
|
||||
|
||||
appData.appLaunchInfoList.push_back(appLaunchInfo);
|
||||
appData.appsStateList.insert({ app, { app, nullptr, data.state } });
|
||||
}
|
||||
|
||||
json::to_file(WorkspacesData::LaunchWorkspacesFile(), WorkspacesData::AppLaunchDataJSON::ToJson(appData));
|
||||
m_ipcHelper.send(WorkspacesData::AppLaunchDataJSON::ToJson(appData).ToString().c_str());
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <LaunchingApp.h>
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
#include <WorkspacesLib/IPCHelper.h>
|
||||
|
||||
class LauncherUIHelper
|
||||
{
|
||||
public:
|
||||
LauncherUIHelper() = default;
|
||||
LauncherUIHelper();
|
||||
~LauncherUIHelper();
|
||||
|
||||
void LaunchUI();
|
||||
void UpdateLaunchStatus(LaunchingApps launchedApps);
|
||||
void UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const;
|
||||
|
||||
private:
|
||||
DWORD uiProcessId;
|
||||
DWORD m_processId;
|
||||
IPCHelper m_ipcHelper;
|
||||
};
|
||||
|
|
|
@ -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>;
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -127,21 +127,23 @@
|
|||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="AppLauncher.cpp" />
|
||||
<ClCompile Include="Launcher.cpp" />
|
||||
<ClCompile Include="LauncherUIHelper.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="RegistryUtils.cpp" />
|
||||
<ClCompile Include="WindowArrangerHelper.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="AppLauncher.h" />
|
||||
<ClInclude Include="Launcher.h" />
|
||||
<ClInclude Include="LauncherUIHelper.h" />
|
||||
<ClInclude Include="LaunchingApp.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="RegistryUtils.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="WindowArrangerHelper.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
|
|
@ -27,13 +27,13 @@
|
|||
<ClInclude Include="RegistryUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="utils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LauncherUIHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LaunchingApp.h">
|
||||
<ClInclude Include="WindowArrangerHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Launcher.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
|
@ -53,6 +53,12 @@
|
|||
<ClCompile Include="LauncherUIHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WindowArrangerHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Launcher.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
#include "pch.h"
|
||||
#include "pch.h"
|
||||
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
#include <WorkspacesLib/trace.h>
|
||||
#include <WorkspacesLib/JsonUtils.h>
|
||||
#include <WorkspacesLib/utils.h>
|
||||
|
||||
#include <AppLauncher.h>
|
||||
#include <utils.h>
|
||||
#include <Launcher.h>
|
||||
|
||||
#include <Generated Files/resource.h>
|
||||
|
||||
#include <workspaces-common/InvokePoint.h>
|
||||
#include <workspaces-common/MonitorUtils.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/gpo.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);
|
||||
|
||||
std::string cmdLineStr(cmdline);
|
||||
auto cmdArgs = split(cmdLineStr, " ");
|
||||
if (cmdArgs.size() < 1)
|
||||
std::wstring cmdLineStr{ GetCommandLineW() };
|
||||
auto cmdArgs = split(cmdLineStr, L" ");
|
||||
if (cmdArgs.size() < 2)
|
||||
{
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wstring id(cmdArgs[0].begin(), cmdArgs[0].end());
|
||||
std::wstring id(cmdArgs[1].begin(), cmdArgs[1].end());
|
||||
if (id.empty())
|
||||
{
|
||||
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;
|
||||
if (cmdArgs.size() > 1)
|
||||
if (cmdArgs.size() > 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[1]));
|
||||
invokePoint = static_cast<InvokePoint>(std::stoi(cmdArgs[2]));
|
||||
}
|
||||
catch (std::exception)
|
||||
{
|
||||
|
@ -95,79 +91,51 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
|||
if (invokePoint == InvokePoint::LaunchAndEdit)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wstring formattedMessage{};
|
||||
switch (res.error())
|
||||
{
|
||||
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
|
||||
{
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
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;
|
||||
case JsonUtils::WorkspacesFileError::FileReadingError:
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectToLaunch.id.empty())
|
||||
{
|
||||
try
|
||||
auto file = WorkspacesData::WorkspacesFile();
|
||||
auto res = JsonUtils::ReadWorkspaces(file);
|
||||
if (res.isOk())
|
||||
{
|
||||
auto savedWorkspacesJson = json::from_file(WorkspacesData::WorkspacesFile());
|
||||
if (savedWorkspacesJson.has_value())
|
||||
{
|
||||
auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value());
|
||||
if (savedWorkspaces.has_value())
|
||||
{
|
||||
workspaces = savedWorkspaces.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::critical("Incorrect Workspaces file");
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile());
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
workspaces = res.getValue();
|
||||
}
|
||||
catch (std::exception ex)
|
||||
else
|
||||
{
|
||||
Logger::critical("Exception on reading Workspaces file: {}", ex.what());
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::WorkspacesFile());
|
||||
std::wstring formattedMessage{};
|
||||
switch (res.error())
|
||||
{
|
||||
case JsonUtils::WorkspacesFileError::FileReadingError:
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
|
@ -175,7 +143,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
|||
if (workspaces.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);
|
||||
return 1;
|
||||
}
|
||||
|
@ -198,50 +166,9 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
|||
return 1;
|
||||
}
|
||||
|
||||
// launch apps
|
||||
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);
|
||||
Launcher launcher(projectToLaunch, workspaces, invokePoint);
|
||||
|
||||
Logger::trace("Finished");
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms.Design.Behavior;
|
||||
|
||||
using Common.UI;
|
||||
using ManagedCommon;
|
||||
using WorkspacesLauncherUI.Utils;
|
||||
using PowerToys.Interop;
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
|
@ -21,6 +19,9 @@ namespace WorkspacesLauncherUI
|
|||
{
|
||||
private static Mutex _instanceMutex;
|
||||
|
||||
// Create an instance of the IPC wrapper.
|
||||
private static TwoWayPipeMessageIPCManaged ipcmanager;
|
||||
|
||||
private StatusWindow _mainWindow;
|
||||
|
||||
private MainViewModel _mainViewModel;
|
||||
|
@ -29,21 +30,23 @@ namespace WorkspacesLauncherUI
|
|||
|
||||
private bool _isDisposed;
|
||||
|
||||
public static Action<string> IPCMessageReceivedCallback { get; set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
}
|
||||
|
||||
private void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
Logger.InitializeLogger("\\Workspaces\\Logs");
|
||||
Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI");
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
|
||||
const string appName = "Local\\PowerToys_Workspaces_Launcher_InstanceMutex";
|
||||
const string appName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex";
|
||||
bool createdNew;
|
||||
_instanceMutex = new Mutex(true, appName, out 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;
|
||||
Shutdown(0);
|
||||
return;
|
||||
|
@ -56,6 +59,15 @@ namespace WorkspacesLauncherUI
|
|||
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);
|
||||
|
||||
if (_mainViewModel == null)
|
||||
|
@ -97,6 +109,10 @@ namespace WorkspacesLauncherUI
|
|||
if (disposing)
|
||||
{
|
||||
ThemeManager?.Dispose();
|
||||
|
||||
ipcmanager?.End();
|
||||
ipcmanager?.Dispose();
|
||||
|
||||
_instanceMutex?.Dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,33 +1,16 @@
|
|||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Workspaces.Data;
|
||||
using WorkspacesLauncherUI.Utils;
|
||||
|
||||
using static WorkspacesLauncherUI.Data.AppLaunchData;
|
||||
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
|
||||
using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
|
||||
|
||||
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
|
||||
{
|
||||
[JsonPropertyName("apps")]
|
||||
|
|
|
@ -1,32 +1,23 @@
|
|||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Workspaces.Data;
|
||||
|
||||
using static WorkspacesLauncherUI.Data.AppLaunchInfoData;
|
||||
|
||||
namespace WorkspacesLauncherUI.Data
|
||||
{
|
||||
public class AppLaunchInfoData : WorkspacesEditorData<AppLaunchInfoWrapper>
|
||||
public class AppLaunchInfoData : WorkspacesUIData<AppLaunchInfoWrapper>
|
||||
{
|
||||
public struct AppLaunchInfoWrapper
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; }
|
||||
[JsonPropertyName("application")]
|
||||
public ApplicationWrapper Application { get; set; }
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; }
|
||||
public LaunchingState State { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
@ -13,7 +12,7 @@ using static WorkspacesLauncherUI.Data.AppLaunchInfosData;
|
|||
|
||||
namespace WorkspacesLauncherUI.Data
|
||||
{
|
||||
public class AppLaunchInfosData : WorkspacesEditorData<AppLaunchInfoListWrapper>
|
||||
public class AppLaunchInfosData : WorkspacesUIData<AppLaunchInfoListWrapper>
|
||||
{
|
||||
public struct AppLaunchInfoListWrapper
|
||||
{
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils;
|
|||
|
||||
namespace Workspaces.Data
|
||||
{
|
||||
public class WorkspacesEditorData<T>
|
||||
public class WorkspacesUIData<T>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// 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.Windows;
|
||||
using System.Windows.Automation.Peers;
|
||||
using System.Windows.Controls;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
|
@ -13,9 +13,9 @@ using System.Text.Json.Serialization;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ManagedCommon;
|
||||
using Windows.Management.Deployment;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Models
|
||||
{
|
||||
|
@ -28,9 +28,9 @@ namespace WorkspacesLauncherUI.Models
|
|||
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;
|
||||
|
||||
|
@ -51,12 +51,12 @@ namespace WorkspacesLauncherUI.Models
|
|||
}
|
||||
else
|
||||
{
|
||||
_icon = Icon.ExtractAssociatedIcon(AppPath);
|
||||
_icon = Icon.ExtractAssociatedIcon(Application.ApplicationPath);
|
||||
}
|
||||
}
|
||||
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;
|
||||
_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
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
"launched" => "\U0000F78C",
|
||||
"failed" => "\U0000EF2C",
|
||||
LaunchingState.LaunchedAndMoved => "\U0000F78C",
|
||||
LaunchingState.Failed => "\U0000EF2C",
|
||||
_ => "\U0000EF2C",
|
||||
};
|
||||
}
|
||||
|
@ -84,8 +90,8 @@ namespace WorkspacesLauncherUI.Models
|
|||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
"launched" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
|
||||
"failed" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
LaunchingState.LaunchedAndMoved => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 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)),
|
||||
};
|
||||
}
|
||||
|
@ -139,13 +145,13 @@ namespace WorkspacesLauncherUI.Models
|
|||
{
|
||||
if (_isPackagedApp == null)
|
||||
{
|
||||
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!Application.ApplicationPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_isPackagedApp = false;
|
||||
}
|
||||
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);
|
||||
Match match = packagedAppPathRegex.Match(appPath);
|
||||
_isPackagedApp = match.Success;
|
||||
|
@ -200,7 +206,7 @@ namespace WorkspacesLauncherUI.Models
|
|||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,9 +7,6 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
|
||||
using ManagedCommon;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
|
@ -20,8 +17,6 @@ namespace WorkspacesLauncherUI.ViewModels
|
|||
{
|
||||
public ObservableCollection<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>();
|
||||
|
||||
private IFileSystemWatcher _watcher;
|
||||
private System.Timers.Timer selfDestroyTimer;
|
||||
private StatusWindow _snapshotWindow;
|
||||
private int launcherProcessID;
|
||||
private bool _exiting;
|
||||
|
@ -36,60 +31,43 @@ namespace WorkspacesLauncherUI.ViewModels
|
|||
public MainViewModel()
|
||||
{
|
||||
_exiting = false;
|
||||
LoadAppLaunchInfos();
|
||||
string fileName = Path.GetFileName(AppLaunchData.File);
|
||||
_watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher("Workspaces", fileName, () => AppLaunchInfoStateChanged());
|
||||
|
||||
// receive IPC Message
|
||||
App.IPCMessageReceivedCallback = (string msg) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
AppLaunchData parser = new AppLaunchData();
|
||||
AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Deserialize(msg);
|
||||
HandleAppLaunchingState(appLaunchData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex.Message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void AppLaunchInfoStateChanged()
|
||||
{
|
||||
LoadAppLaunchInfos();
|
||||
}
|
||||
|
||||
private void LoadAppLaunchInfos()
|
||||
private void HandleAppLaunchingState(AppLaunchData.AppLaunchDataWrapper appLaunchData)
|
||||
{
|
||||
if (_exiting)
|
||||
{
|
||||
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;
|
||||
|
||||
List<AppLaunching> appLaunchingList = new List<AppLaunching>();
|
||||
bool allLaunched = true;
|
||||
foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList)
|
||||
{
|
||||
appLaunchingList.Add(new AppLaunching()
|
||||
{
|
||||
Name = app.Name,
|
||||
AppPath = app.Path,
|
||||
Application = app.Application,
|
||||
LaunchState = app.State,
|
||||
});
|
||||
if (app.State != "launched" && app.State != "failed")
|
||||
{
|
||||
allLaunched = false;
|
||||
}
|
||||
}
|
||||
|
||||
AppsListed = new ObservableCollection<AppLaunching>(appLaunchingList);
|
||||
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)
|
||||
|
@ -113,7 +91,6 @@ namespace WorkspacesLauncherUI.ViewModels
|
|||
internal void CancelLaunch()
|
||||
{
|
||||
_exiting = true;
|
||||
_watcher.Dispose();
|
||||
Process proc = Process.GetProcessById(launcherProcessID);
|
||||
proc.Kill();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
// sync with WorkspacesLauncherUI : Data : LaunchingState.cs
|
||||
enum class LaunchingState
|
||||
{
|
||||
Waiting = 0,
|
||||
Launched,
|
||||
LaunchedAndMoved,
|
||||
Failed
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -21,12 +21,6 @@ namespace WorkspacesData
|
|||
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
|
||||
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
|
||||
{
|
||||
|
@ -420,19 +414,40 @@ namespace WorkspacesData
|
|||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const static wchar_t* NameID = L"name";
|
||||
const static wchar_t* PathID = L"path";
|
||||
const static wchar_t* ApplicationID = L"application";
|
||||
const static wchar_t* StateID = L"state";
|
||||
}
|
||||
|
||||
json::JsonObject ToJson(const AppLaunchInfo& data)
|
||||
json::JsonObject ToJson(const LaunchingAppState& data)
|
||||
{
|
||||
json::JsonObject json{};
|
||||
json.SetNamedValue(NonLocalizable::NameID, json::value(data.name));
|
||||
json.SetNamedValue(NonLocalizable::PathID, json::value(data.path));
|
||||
json.SetNamedValue(NonLocalizable::StateID, json::value(data.state));
|
||||
json.SetNamedValue(NonLocalizable::ApplicationID, WorkspacesProjectJSON::ApplicationJSON::ToJson(data.application));
|
||||
json.SetNamedValue(NonLocalizable::StateID, json::value(static_cast<int>(data.state)));
|
||||
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
|
||||
|
@ -442,18 +457,46 @@ namespace WorkspacesData
|
|||
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::JsonArray appLaunchInfoArray{};
|
||||
for (const auto& appLaunchInfo : data)
|
||||
{
|
||||
appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo));
|
||||
appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo.second));
|
||||
}
|
||||
|
||||
json.SetNamedValue(NonLocalizable::AppLaunchInfoID, appLaunchInfoArray);
|
||||
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
|
||||
|
@ -467,7 +510,7 @@ namespace WorkspacesData
|
|||
json::JsonObject ToJson(const AppLaunchData& data)
|
||||
{
|
||||
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));
|
||||
return json;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
#include <common/utils/json.h>
|
||||
|
||||
#include <WorkspacesLib/LaunchingStateEnum.h>
|
||||
|
||||
namespace WorkspacesData
|
||||
{
|
||||
std::wstring WorkspacesFile();
|
||||
std::wstring TempWorkspacesFile();
|
||||
std::wstring LaunchWorkspacesFile();
|
||||
|
||||
struct WorkspacesProject
|
||||
{
|
||||
|
@ -21,10 +22,7 @@ namespace WorkspacesData
|
|||
|
||||
RECT toRect() const noexcept;
|
||||
|
||||
inline bool operator==(const Position& other) const noexcept
|
||||
{
|
||||
return x == other.x && y == other.y && width == other.width && height == other.height;
|
||||
}
|
||||
auto operator<=>(const Position&) const = default;
|
||||
};
|
||||
|
||||
std::wstring name;
|
||||
|
@ -39,6 +37,8 @@ namespace WorkspacesData
|
|||
bool isMaximized{};
|
||||
Position position{};
|
||||
unsigned int monitor{};
|
||||
|
||||
auto operator<=>(const Application&) const = default;
|
||||
};
|
||||
|
||||
struct Monitor
|
||||
|
@ -80,34 +80,22 @@ namespace WorkspacesData
|
|||
std::vector<WorkspacesProject> projects;
|
||||
};
|
||||
|
||||
struct AppLaunchInfo
|
||||
struct LaunchingAppState
|
||||
{
|
||||
std::wstring name;
|
||||
std::wstring path;
|
||||
std::wstring state;
|
||||
WorkspacesData::WorkspacesProject::Application application;
|
||||
HWND window{};
|
||||
LaunchingState state { LaunchingState::Waiting };
|
||||
};
|
||||
|
||||
namespace AppLaunchInfoJSON
|
||||
{
|
||||
json::JsonObject ToJson(const AppLaunchInfo& data);
|
||||
}
|
||||
|
||||
namespace AppLaunchInfoListJSON
|
||||
{
|
||||
json::JsonObject ToJson(const std::vector<AppLaunchInfo>& data);
|
||||
}
|
||||
using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;
|
||||
using LaunchingAppStateList = std::vector<std::pair<WorkspacesData::WorkspacesProject::Application, LaunchingState>>;
|
||||
|
||||
struct AppLaunchData
|
||||
{
|
||||
std::vector<AppLaunchInfo> appLaunchInfoList;
|
||||
LaunchingAppStateMap appsStateList;
|
||||
int launcherProcessID = 0;
|
||||
};
|
||||
|
||||
namespace AppLaunchDataJSON
|
||||
{
|
||||
json::JsonObject ToJson(const AppLaunchData& data);
|
||||
}
|
||||
|
||||
namespace WorkspacesProjectJSON
|
||||
{
|
||||
namespace ApplicationJSON
|
||||
|
@ -143,4 +131,22 @@ namespace WorkspacesData
|
|||
json::JsonObject ToJson(const std::vector<WorkspacesProject>& data);
|
||||
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);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -33,19 +33,32 @@
|
|||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<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="Result.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="WorkspacesData.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="AppUtils.cpp" />
|
||||
<ClCompile Include="IPCHelper.cpp" />
|
||||
<ClCompile Include="JsonUtils.cpp" />
|
||||
<ClCompile Include="LaunchingStatus.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
|
||||
<ClCompile Include="WorkspacesData.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj">
|
||||
<Project>{f055103b-f80b-4d0c-bf48-057c55620033}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
|
|
|
@ -23,6 +23,24 @@
|
|||
<ClInclude Include="AppUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</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>
|
||||
<ClCompile Include="pch.cpp">
|
||||
|
@ -37,6 +55,18 @@
|
|||
<ClCompile Include="AppUtils.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</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>
|
||||
<None Include="packages.config" />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@
|
|||
#include <vector>
|
||||
#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;
|
||||
std::string token;
|
||||
while ((pos = s.find(delimiter)) != std::string::npos)
|
||||
std::wstring token;
|
||||
while ((pos = s.find(delimiter)) != std::wstring::npos)
|
||||
{
|
||||
token = s.substr(0, pos);
|
||||
tokens.push_back(token);
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
// Non-localizable
|
||||
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 workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe";
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -133,7 +133,6 @@
|
|||
<ClCompile Include="SnapshotUtils.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="JsonUtils.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.base.h" />
|
||||
<ClInclude Include="SnapshotUtils.h" />
|
||||
|
|
|
@ -18,9 +18,6 @@
|
|||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="JsonUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SnapshotUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
#include <workspaces-common/GuidUtils.h>
|
||||
#include <workspaces-common/MonitorUtils.h>
|
||||
|
||||
#include <WorkspacesLib/JsonUtils.h>
|
||||
#include <WorkspacesLib/WorkspacesData.h>
|
||||
|
||||
#include <JsonUtils.h>
|
||||
#include <SnapshotUtils.h>
|
||||
|
||||
#include <common/utils/gpo.h>
|
||||
#include <common/utils/logger_helper.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"";
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::wstring fileName = WorkspacesData::WorkspacesFile();
|
||||
std::string cmdLineStr(cmdLine);
|
||||
if (!cmdLineStr.empty())
|
||||
{
|
||||
std::wstring fileNameParam(cmdLineStr.begin(), cmdLineStr.end());
|
||||
fileName = fileNameParam;
|
||||
}
|
||||
|
||||
// create new project
|
||||
time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
WorkspacesData::WorkspacesProject project{ .id = CreateGuidString(), .creationTime = creationTime };
|
||||
|
@ -75,7 +67,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm
|
|||
return monitorNumber;
|
||||
});
|
||||
|
||||
WorkspacesJsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project);
|
||||
JsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project);
|
||||
Logger::trace(L"WorkspacesProject {}:{} created", project.name, project.id);
|
||||
|
||||
CoUninitialize();
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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>
|
|
@ -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>
|
Двоичные данные
src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc
Normal file
Двоичные данные
src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc
Normal 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;
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
#include "pch.h"
|
|
@ -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>
|
|
@ -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
|
||||
//////////////////////////////
|
|
@ -21,6 +21,7 @@ namespace WindowUtils
|
|||
const wchar_t WorkspacesSnapshotTool[] = L"POWERTOYS.WORKSPACESSNAPSHOTTOOL";
|
||||
const wchar_t WorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR";
|
||||
const wchar_t WorkspacesLauncher[] = L"POWERTOYS.WORKSPACESLAUNCHER";
|
||||
const wchar_t WorkspacesWindowArranger[] = L"POWERTOYS.WORKSPACESWINDOWARRANGER";
|
||||
}
|
||||
|
||||
inline bool IsRoot(HWND window) noexcept
|
||||
|
@ -79,7 +80,8 @@ namespace WindowUtils
|
|||
NonLocalizable::SearchUI,
|
||||
NonLocalizable::HelpWindow,
|
||||
NonLocalizable::WorkspacesEditor,
|
||||
NonLocalizable::WorkspacesLauncher,
|
||||
NonLocalizable::WorkspacesLauncher,
|
||||
NonLocalizable::WorkspacesWindowArranger,
|
||||
NonLocalizable::WorkspacesSnapshotTool,
|
||||
};
|
||||
return (check_excluded_app(window, processPathUpper, defaultExcludedApps));
|
||||
|
|
|
@ -46,5 +46,6 @@ std::vector<std::wstring> processes =
|
|||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче