diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 370847ac38..fef5508375 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1861,6 +1861,7 @@ workarounds WORKSPACESEDITOR WORKSPACESLAUNCHER WORKSPACESSNAPSHOTTOOL +WORKSPACESWINDOWARRANGER wox wparam wpf diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index ed5bd325a7..57a22ffd57 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -194,6 +194,7 @@ "PowerToys.WorkspacesSnapshotTool.exe", "PowerToys.WorkspacesLauncher.exe", + "PowerToys.WorkspacesWindowArranger.exe", "PowerToys.WorkspacesEditor.exe", "PowerToys.WorkspacesEditor.dll", "PowerToys.WorkspacesLauncherUI.exe", diff --git a/PowerToys.sln b/PowerToys.sln index 6f214eb5c0..5f831135cb 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -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} diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 216a05aaab..8845208200 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array 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", }; diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 814547ef0b..e20bc999d9 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -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; diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp index 73e89a40d8..f9aed31fe6 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp @@ -1,150 +1,27 @@ #include "pch.h" #include "AppLauncher.h" +#include + +#include + #include #include -#include -#include - -#include - -#include -#include -#include +#include #include -#include -#include - -#include -#include #include -#include using namespace winrt; using namespace Windows::Foundation; using namespace Windows::Management::Deployment; -namespace FancyZones +namespace AppLauncher { - inline bool allMonitorsHaveSameDpiScaling() + void UpdatePackagedApps(std::vector& 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& 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 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& 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 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(placement.rcNormalPosition.left); - float y = static_cast(placement.rcNormalPosition.top); - float width = static_cast(placement.rcNormalPosition.right - placement.rcNormalPosition.left); - float height = static_cast(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top); - - DPIAware::InverseConvert(monitor, x, y); - DPIAware::InverseConvert(monitor, width, height); - - WorkspacesData::WorkspacesProject::Application::Position windowPosition{ - .x = static_cast(std::round(x)), - .y = static_cast(std::round(y)), - .width = static_cast(std::round(width)), - .height = static_cast(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 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& monitors, ErrorList& launchErrors) -{ - bool launchedSuccessfully{ true }; - - LauncherUIHelper uiHelper; - uiHelper.LaunchUI(); - - // Get the set of windows before launching the app - std::vector 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 windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter); - std::vector 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(snapMonitorIter->dpi) / currentDpi; - rect.left = static_cast(std::round(rect.left * mult)); - rect.right = static_cast(std::round(rect.right * mult)); - rect.top = static_cast(std::round(rect.top * mult)); - rect.bottom = static_cast(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; -} +} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h index 03f6a6d06d..0827afc4c9 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h +++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h @@ -1,7 +1,16 @@ #pragma once +#include + +#include +#include #include -using ErrorList = std::vector>; +namespace AppLauncher +{ + using ErrorList = std::vector>; -bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector& monitors, ErrorList& launchErrors); \ No newline at end of file + Result LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated); + + bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors); +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp b/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp new file mode 100644 index 0000000000..b660d0d939 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp @@ -0,0 +1,123 @@ +#include "pch.h" +#include "Launcher.h" + +#include + +#include + +#include + +#include + +Launcher::Launcher(const WorkspacesData::WorkspacesProject& project, + std::vector& 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()), + m_windowArrangerHelper(std::make_unique(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 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 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"); + } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/Launcher.h b/src/modules/Workspaces/WorkspacesLauncher/Launcher.h new file mode 100644 index 0000000000..36f17329d2 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/Launcher.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +class Launcher +{ +public: + Launcher(const WorkspacesData::WorkspacesProject& project, std::vector& workspaces, InvokePoint invokePoint); + ~Launcher(); + + void Launch(); + +private: + WorkspacesData::WorkspacesProject m_project; + std::vector& m_workspaces; + const InvokePoint m_invokePoint; + const std::chrono::steady_clock::time_point m_start; + std::unique_ptr m_uiHelper; + std::unique_ptr m_windowArrangerHelper; + LaunchingStatus m_launchingStatus; + bool m_launchedSuccessfully{}; + std::vector> m_launchErrors{}; + + void handleWindowArrangerMessage(const std::wstring& msg); +}; diff --git a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp index e1d06ac0f9..b35c6d3657 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp @@ -7,12 +7,22 @@ #include #include +#include + +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()); } diff --git a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h index 53fe78f931..20704f13a2 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h +++ b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h @@ -1,16 +1,18 @@ #pragma once -#include +#include +#include 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; }; diff --git a/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h b/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h deleted file mode 100644 index c2daae31f2..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -struct LaunchingApp -{ - WorkspacesData::WorkspacesProject::Application application; - HWND window; - std::wstring state; -}; - -using LaunchingApps = std::vector; \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp new file mode 100644 index 0000000000..600038ea61 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp @@ -0,0 +1,71 @@ +#include "pch.h" +#include "WindowArrangerHelper.h" + +#include + +#include +#include + +#include + +#include + +WindowArrangerHelper::WindowArrangerHelper(std::function 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 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()); + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h new file mode 100644 index 0000000000..22d7e3ea1c --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include + +class WindowArrangerHelper +{ +public: + WindowArrangerHelper(std::function ipcCallback); + ~WindowArrangerHelper(); + + void Launch(const std::wstring& projectId, bool elevated, std::function keepWaitingCallback); + +private: + DWORD m_processId; + IPCHelper m_ipcHelper; + OnThreadExecutor m_threadExecutor; +}; diff --git a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj index 7d69c20533..8ff482677c 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj +++ b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj @@ -127,21 +127,23 @@ + Create + + - - + diff --git a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters index 326bec179f..191fdb87af 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters @@ -27,13 +27,13 @@ Header Files - - Header Files - Header Files - + + Header Files + + Header Files @@ -53,6 +53,12 @@ Source Files + + Source Files + + + Source Files + diff --git a/src/modules/Workspaces/WorkspacesLauncher/main.cpp b/src/modules/Workspaces/WorkspacesLauncher/main.cpp index 98d6319231..4e3efc8381 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/main.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/main.cpp @@ -1,16 +1,12 @@ -#include "pch.h" +#include "pch.h" -#include -#include +#include +#include -#include -#include +#include #include -#include -#include - #include #include #include @@ -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(std::stoi(cmdArgs[1])); + invokePoint = static_cast(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> 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 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; } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs b/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs index 0709a59c09..b4117e746d 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs @@ -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 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(); } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs index 6415e18713..dc19b86647 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs @@ -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 + public class AppLaunchData : WorkspacesUIData { - public static string File - { - get - { - return FolderUtils.DataFolder() + "\\launch-workspaces.json"; - } - } - public struct AppLaunchDataWrapper { [JsonPropertyName("apps")] diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs index 2d339f0603..aa64510ba9 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs @@ -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 + public class AppLaunchInfoData : WorkspacesUIData { 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; } } } } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs index ecb37fd48c..cb00cb4478 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs @@ -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 + public class AppLaunchInfosData : WorkspacesUIData { public struct AppLaunchInfoListWrapper { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs new file mode 100644 index 0000000000..51bb1a24c6 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs @@ -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; } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs new file mode 100644 index 0000000000..9ad8c958d2 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs @@ -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, + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs new file mode 100644 index 0000000000..6b39c7d918 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs @@ -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(); + } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs similarity index 82% rename from src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs rename to src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs index c2599b92d5..5e9b88a728 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs @@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils; namespace Workspaces.Data { - public class WorkspacesEditorData + public class WorkspacesUIData { 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(data, JsonOptions); } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs b/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs index 9f7ea0beda..c7e87e39fc 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs @@ -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; diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs index 82fc47860b..bd0f50b467 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs @@ -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(@"(?[^_]*)_\d+.\d+.\d+.\d+_x64__(?[^\\]*)", 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}"); } } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs deleted file mode 100644 index 3501c2045c..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs +++ /dev/null @@ -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"; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs deleted file mode 100644 index 39b6ea47bb..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs index b7d63dafe8..ed6cdd8f31 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs @@ -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 AppsListed { get; set; } = new ObservableCollection(); - 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 appLaunchingList = new List(); - 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(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(); } diff --git a/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp b/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp new file mode 100644 index 0000000000..6c888f434d --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp @@ -0,0 +1,42 @@ +#include "pch.h" +#include "IPCHelper.h" + +#include + +IPCHelper::IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function 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(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); + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/IPCHelper.h b/src/modules/Workspaces/WorkspacesLib/IPCHelper.h new file mode 100644 index 0000000000..4ccf9cb5e3 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/IPCHelper.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +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 messageCallback); + ~IPCHelper(); + + void send(const std::wstring& message) const; + +private: + void receive(const std::wstring& msg); + + std::unique_ptr ipc; + std::mutex ipcMutex; + std::function callback; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp b/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp new file mode 100644 index 0000000000..039000213f --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp @@ -0,0 +1,106 @@ +#include "pch.h" +#include "JsonUtils.h" + +#include + +#include + +namespace JsonUtils +{ + Result 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, 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& 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; + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/JsonUtils.h b/src/modules/Workspaces/WorkspacesLib/JsonUtils.h new file mode 100644 index 0000000000..012b9bf702 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/JsonUtils.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace JsonUtils +{ + enum class WorkspacesFileError + { + FileReadingError, + IncorrectFileError, + }; + + Result ReadSingleWorkspace(const std::wstring& fileName); + Result, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName); + + bool Write(const std::wstring& fileName, const std::vector& projects); + bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project); +} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h b/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h new file mode 100644 index 0000000000..2fbaf2fe97 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h @@ -0,0 +1,10 @@ +#pragma once + +// sync with WorkspacesLauncherUI : Data : LaunchingState.cs +enum class LaunchingState +{ + Waiting = 0, + Launched, + LaunchedAndMoved, + Failed +}; \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp new file mode 100644 index 0000000000..5c438f5cf0 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp @@ -0,0 +1,66 @@ +#include "pch.h" +#include "LaunchingStatus.h" + +#include + +LaunchingStatus::LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function 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); + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h new file mode 100644 index 0000000000..eec0b1b0f6 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +class LaunchingStatus +{ +public: + LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function 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 m_updateCallback; + std::shared_mutex m_mutex; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/Result.h b/src/modules/Workspaces/WorkspacesLib/Result.h new file mode 100644 index 0000000000..56a5101b42 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/Result.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +template +class Ok +{ +public: + explicit constexpr Ok(T value) : + value(std::move(value)) {} + + constexpr T&& get() { return std::move(value); } + + T value; +}; + +template +class Error +{ +public: + explicit constexpr Error(T value) : + value(std::move(value)) {} + + constexpr T&& get() { return std::move(value); } + + T value; +}; + +template +class Result +{ +public: + using VariantT = std::variant, Error>; + + constexpr Result(Ok value) : + variant(std::move(value)) + {} + + constexpr Result(Error value) : + variant(std::move(value)) + {} + + constexpr bool isOk() const { return std::holds_alternative>(variant); } + constexpr bool isError() const { return std::holds_alternative>(variant); } + + constexpr OkT value() const { return std::get>(variant).value; } + constexpr ErrT error() const { return std::get>(variant).value; } + + constexpr OkT&& getValue() { return std::get>(variant).get(); } + constexpr ErrT&& getError() { return std::get>(variant).get(); } + + VariantT variant; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp index b9748aa993..d71619f90b 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp @@ -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(data.state))); return json; } + + std::optional 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(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& 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 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; } diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h index 908552530f..40252850a3 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h @@ -2,11 +2,12 @@ #include +#include + 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 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& data); - } + using LaunchingAppStateMap = std::map; + using LaunchingAppStateList = std::vector>; struct AppLaunchData { - std::vector 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& data); std::optional> FromJson(const json::JsonObject& json); } + + namespace AppLaunchInfoJSON + { + json::JsonObject ToJson(const LaunchingAppState& data); + std::optional FromJson(const json::JsonObject& json); + } + + namespace AppLaunchInfoListJSON + { + json::JsonObject ToJson(const LaunchingAppStateMap& data); + std::optional FromJson(const json::JsonObject& json); + } + + namespace AppLaunchDataJSON + { + json::JsonObject ToJson(const AppLaunchData& data); + } + }; diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj index 4ef6d283ec..f3aa1424b0 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj @@ -33,19 +33,32 @@ + + + + + + + + + Create + + + {f055103b-f80b-4d0c-bf48-057c55620033} + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters index f30fd53623..c910f65015 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters @@ -23,6 +23,24 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + @@ -37,6 +55,18 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp b/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp new file mode 100644 index 0000000000..40b2f1dbe5 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp @@ -0,0 +1,469 @@ +#include "pch.h" + +#include + +#include + +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(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(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(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(HeapAlloc(GetProcessHeap(), + HEAP_ZERO_MEMORY, + dwLength)); + if (*ppsid == NULL) + goto Cleanup; + if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid)) + { + HeapFree(GetProcessHeap(), 0, static_cast(*ppsid)); + goto Cleanup; + } + break; + } + + bSuccess = TRUE; + +Cleanup: + + // Free the buffer for the token groups. + + if (ptg != NULL) + HeapFree(GetProcessHeap(), 0, static_cast(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(*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(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(new_dacl)); +Lclean_sd: + LocalFree(static_cast(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(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; + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/utils.h b/src/modules/Workspaces/WorkspacesLib/utils.h similarity index 54% rename from src/modules/Workspaces/WorkspacesLauncher/utils.h rename to src/modules/Workspaces/WorkspacesLib/utils.h index 571db60d4d..72492dda1a 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/utils.h +++ b/src/modules/Workspaces/WorkspacesLib/utils.h @@ -3,12 +3,12 @@ #include #include -std::vector split(std::string s, const std::string& delimiter) +std::vector split(std::wstring s, const std::wstring& delimiter) { - std::vector tokens; + std::vector 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); diff --git a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp index c0d2089b3e..21ccd19261 100644 --- a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp +++ b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp @@ -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"; diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h b/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h deleted file mode 100644 index be895e5950..0000000000 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace WorkspacesJsonUtils -{ - inline std::vector Read(const std::wstring& fileName) - { - std::vector 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& 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()); - } - } -} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj index ff4491e636..b817abd043 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj @@ -133,7 +133,6 @@ - diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters index 5f837c5409..2402f15f61 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters @@ -18,9 +18,6 @@ Header Files - - Header Files - Header Files diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp b/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp index e0803fde7f..b736ce238e 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp @@ -5,21 +5,21 @@ #include #include +#include #include -#include #include #include #include #include -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(); diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props b/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props new file mode 100644 index 0000000000..b0c622690f --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx b/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp new file mode 100644 index 0000000000..ba56dc1843 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp @@ -0,0 +1,257 @@ +#include "pch.h" +#include "WindowArranger.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +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 windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter); + std::vector 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 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(snapMonitorIter->dpi) / currentDpi; + rect.left = static_cast(std::round(rect.left * mult)); + rect.right = static_cast(std::round(rect.right * mult)); + rect.top = static_cast(std::round(rect.top * mult)); + rect.bottom = static_cast(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& val) { + return val.second.window == nullptr; + }) == m_launchingApps.end(); +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h new file mode 100644 index 0000000000..e18b52829b --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class WindowArranger +{ +public: + WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper); + ~WindowArranger() = default; + +private: + const WorkspacesData::WorkspacesProject m_project; + const std::vector m_windowsBefore; + const std::vector 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; +}; diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp new file mode 100644 index 0000000000..afcdd1c7bf --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp @@ -0,0 +1,60 @@ +#include "pch.h" +#include "WindowCreationHandler.h" + +WindowCreationHandler::WindowCreationHandler(std::function 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 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; + } +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h new file mode 100644 index 0000000000..6e82a964a0 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h @@ -0,0 +1,30 @@ +#pragma once + +class WindowCreationHandler +{ +public: + WindowCreationHandler(std::function windowCreatedCallback); + ~WindowCreationHandler(); + +private: + static inline WindowCreationHandler* s_instance = nullptr; + std::vector m_staticWinEventHooks; + std::function 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); + } + } +}; diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj new file mode 100644 index 0000000000..2451be2470 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj @@ -0,0 +1,179 @@ + + + + + + + + + + + + Level3 + false + true + stdcpplatest + /await %(AdditionalOptions) + _UNICODE;UNICODE;%(PreprocessorDefinitions) + + + Windows + + + true + + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + true + MultiThreadedDebug + + + true + + + + + NDEBUG;%(PreprocessorDefinitions) + MaxSpeed + false + MultiThreaded + true + true + + + true + true + true + + + + + 16.0 + Win32Proj + {37D07516-4185-43A4-924F-3C7A5D95ECF6} + WorkspacesWindowArranger + + + + Application + v143 + Unicode + Spectre + + + + true + true + + + false + true + false + + + + + + + + + + + + PowerToys.$(MSBuildProjectName) + ..\..\..\..\$(Platform)\$(Configuration)\ + + + true + + + false + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies) + + + + + + Create + + + + + + + + + + + + + + + + {caba8dfb-823b-4bf2-93ac-3f31984150d9} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {b31fcc55-b5a4-4ea7-b414-2dceae6af332} + + + + + + + + + Designer + + + + + + + + + + + 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}. + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters new file mode 100644 index 0000000000..78a21dc4d1 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters @@ -0,0 +1,64 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + Resource Files + + + + + Resource Files + + + + + Resource Files + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc new file mode 100644 index 0000000000..acbc7659d5 Binary files /dev/null and b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc differ diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp new file mode 100644 index 0000000000..c946848a19 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp @@ -0,0 +1,115 @@ +#include "pch.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +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 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; +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/packages.config b/src/modules/Workspaces/WorkspacesWindowArranger/packages.config new file mode 100644 index 0000000000..ff4b059648 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/pch.h b/src/modules/Workspaces/WorkspacesWindowArranger/pch.h new file mode 100644 index 0000000000..99c9d8a242 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/pch.h @@ -0,0 +1,6 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.h b/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.h new file mode 100644 index 0000000000..8a3207d1ad --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.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 +////////////////////////////// diff --git a/src/modules/Workspaces/workspaces-common/WindowUtils.h b/src/modules/Workspaces/workspaces-common/WindowUtils.h index aff0c957a3..8424591dfa 100644 --- a/src/modules/Workspaces/workspaces-common/WindowUtils.h +++ b/src/modules/Workspaces/workspaces-common/WindowUtils.h @@ -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)); diff --git a/tools/BugReportTool/BugReportTool/ProcessesList.cpp b/tools/BugReportTool/BugReportTool/ProcessesList.cpp index 02591aa1d6..4e8fb3e422 100644 --- a/tools/BugReportTool/BugReportTool/ProcessesList.cpp +++ b/tools/BugReportTool/BugReportTool/ProcessesList.cpp @@ -46,5 +46,6 @@ std::vector processes = L"PowerToys.WorkspacesSnapshotTool.exe", L"PowerToys.WorkspacesLauncher.exe", L"PowerToys.WorkspacesLauncherUI.exe", + L"PowerToys.WorkspacesWindowArranger.exe", L"PowerToys.WorkspacesEditor.exe", };