Merge branch 'dev/feature/projects' of https://github.com/microsoft/PowerToys into dev/feature/projects

This commit is contained in:
donlaci
2024-05-28 18:26:36 +02:00
16 changed files with 349 additions and 608 deletions

View File

@@ -1210,6 +1210,8 @@ PROJECTSSNAPSHOTTOOL
PROPBAG
PROPERTYKEY
propkey
propsys
PROPVARIANT
propvarutil
prvpane
psapi

View File

@@ -31,12 +31,14 @@ namespace ProjectsEditor.Data
public int Height { get; set; }
}
public long Hwnd { get; set; }
public string Application { get; set; }
public string ApplicationPath { get; set; }
public string Title { get; set; }
public string PackageFullName { get; set; }
public string CommandLineArguments { get; set; }
public bool Minimized { get; set; }

View File

@@ -34,12 +34,14 @@ namespace ProjectsEditor.Models
public int Height { get; set; }
}
public IntPtr Hwnd { get; set; }
public string AppName { get; set; }
public string AppPath { get; set; }
public string AppTitle { get; set; }
public string PackageFullName { get; set; }
public string CommandLineArguments { get; set; }
public bool Minimized { get; set; }
@@ -134,20 +136,6 @@ namespace ProjectsEditor.Models
}
}
[JsonIgnore]
public string AppName
{
get
{
if (File.Exists(AppPath))
{
return Path.GetFileNameWithoutExtension(AppPath);
}
return AppPath.Split('\\').LastOrDefault();
}
}
public WindowPosition Position { get; set; }
private WindowPosition? _scaledPosition;
@@ -194,18 +182,6 @@ namespace ProjectsEditor.Models
PropertyChanged?.Invoke(this, e);
}
internal bool IsMyAppPath(string path)
{
if (!IsPackagedApp)
{
return path.Equals(AppPath, StringComparison.Ordinal);
}
else
{
return path.Contains(PackagedName + "_", StringComparison.InvariantCultureIgnoreCase) && path.Contains(PackagedPublisherID, StringComparison.InvariantCultureIgnoreCase);
}
}
private bool? _isPackagedApp;
public string PackagedId { get; set; }

View File

@@ -204,10 +204,11 @@ namespace ProjectsEditor.Models
{
Applications.Add(new Application()
{
Hwnd = item.Hwnd,
AppName = item.AppName,
AppPath = item.AppPath,
AppTitle = item.AppTitle,
CommandLineArguments = item.CommandLineArguments,
PackageFullName = item.PackageFullName,
Minimized = item.Minimized,
Maximized = item.Maximized,
IsSelected = item.IsSelected,

View File

@@ -109,9 +109,10 @@ namespace ProjectsEditor.Utils
{
newProject.Applications.Add(new Models.Application()
{
Hwnd = new IntPtr(app.Hwnd),
AppPath = app.Application,
AppName = app.Application,
AppPath = app.ApplicationPath,
AppTitle = app.Title,
PackageFullName = app.PackageFullName,
Parent = newProject,
CommandLineArguments = app.CommandLineArguments,
Maximized = app.Maximized,
@@ -163,9 +164,10 @@ namespace ProjectsEditor.Utils
{
wrapper.Applications.Add(new ProjectsData.ApplicationWrapper
{
Hwnd = app.Hwnd,
Application = app.AppPath,
Application = app.AppName,
ApplicationPath = app.AppPath,
Title = app.AppTitle,
PackageFullName = app.PackageFullName,
CommandLineArguments = app.CommandLineArguments,
Maximized = app.Maximized,
Minimized = app.Minimized,

View File

@@ -9,14 +9,11 @@ using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using ManagedCommon;
using ProjectsEditor.Models;
using ProjectsEditor.Utils;
using Windows.ApplicationModel.Core;
using Windows.Management.Deployment;
using static ProjectsEditor.Data.ProjectsData;
namespace ProjectsEditor.ViewModels
@@ -234,215 +231,11 @@ namespace ProjectsEditor.ViewModels
public async void LaunchProject(Project project, bool exitAfterLaunch = false)
{
Project actualSetup = await GetActualSetup();
// Launch apps
// TODO: move launching to ProjectsLauncher
List<Application> newlyStartedApps = new List<Application>();
foreach (Application app in project.Applications.Where(x => x.IsSelected))
{
string launchParam = app.AppPath;
if (string.IsNullOrEmpty(launchParam))
{
Logger.LogWarning($"Project launch. App path is empty. App name: {app.AppName}");
continue;
}
// todo: app path might be different for packaged apps.
if (actualSetup.Applications.Exists(x => x.AppPath.Equals(app.AppPath, StringComparison.Ordinal)))
{
// just move the existing window to the desired position
Logger.LogInfo($"Project launch. App exists. Moving the first app with same app path to the desired position. App name: {app.AppName}");
Application existingApp = actualSetup.Applications.Where(x => x.AppPath.Equals(app.AppPath, StringComparison.Ordinal)).First();
NativeMethods.ShowWindow(existingApp.Hwnd, app.Minimized ? NativeMethods.SW_MINIMIZE : NativeMethods.SW_NORMAL);
if (!app.Minimized)
{
MoveWindowWithScaleAdjustment(project, existingApp.Hwnd, app, true);
NativeMethods.SetForegroundWindow(existingApp.Hwnd);
}
continue;
}
if (launchParam.EndsWith("systemsettings.exe", StringComparison.InvariantCultureIgnoreCase))
{
try
{
string args = string.IsNullOrWhiteSpace(app.CommandLineArguments) ? "home" : app.CommandLineArguments;
bool result = await Windows.System.Launcher.LaunchUriAsync(new Uri($"ms-settings:{args}"));
newlyStartedApps.Add(app);
}
catch (Exception e)
{
// todo exception handling
Logger.LogError($"Exception while launching the project {project.Id}. Tried to launch the settings app via LaunchUriAsync. Exception message: {e.Message}");
}
}
// todo: check the user's packaged apps folder
else if (app.IsPackagedApp)
{
bool started = false;
int retryCountLaunch = 50;
while (!started && retryCountLaunch > 0)
{
try
{
if (string.IsNullOrEmpty(app.CommandLineArguments))
{
// the official app launching method.No parameters are expected
var packApp = await GetAppByPackageFamilyNameAsync(app.PackagedId);
if (packApp != null)
{
await packApp.LaunchAsync();
}
newlyStartedApps.Add(app);
started = true;
}
else
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo("cmd.exe");
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.StartInfo.Arguments = $"cmd /c start shell:appsfolder\\{app.Aumid} {app.CommandLineArguments}";
p.EnableRaisingEvents = true;
p.ErrorDataReceived += (sender, e) =>
{
started = false;
};
p.Start();
p.WaitForExit();
});
newlyStartedApps.Add(app);
started = true;
}
}
catch (Exception e)
{
// todo exception handling
Logger.LogError($"Exception while launching the project {project.Id}. Tried to launch a packaged app {app.AppName}. Exception message{e.Message}");
Thread.Sleep(100);
}
retryCountLaunch--;
}
}
else
{
try
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(launchParam);
p.StartInfo.Arguments = app.CommandLineArguments;
p.Start();
});
newlyStartedApps.Add(app);
}
catch (Exception)
{
// try again as admin
try
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(launchParam);
p.StartInfo.Arguments = app.CommandLineArguments;
p.StartInfo.UseShellExecute = true;
p.StartInfo.Verb = "runas"; // administrator privileges, some apps start only that way
p.Start();
});
newlyStartedApps.Add(app);
}
catch (Exception e)
{
// todo exception handling
Logger.LogError($"Exception while launching the project {project.Id}. Tried to launch an exe via Process.Start: {app.AppName}. Exception message{e.Message}");
}
}
}
}
// the official launching method needs this task.
static async Task<AppListEntry> GetAppByPackageFamilyNameAsync(string packageFamilyName)
{
var pkgManager = new PackageManager();
var pkg = pkgManager.FindPackagesForUser(string.Empty, packageFamilyName).FirstOrDefault();
if (pkg == null)
{
Logger.LogWarning($"Could not load any app for {packageFamilyName}.");
return null;
}
var apps = await pkg.GetAppListEntriesAsync();
var firstApp = apps.Any() ? apps[0] : null;
return firstApp;
}
// next step: move newly created apps to their desired position
// the OS needs some time to show the apps, so do it in multiple steps until all windows all in place
// retry every 100ms for 5 sec totally
int retryCount = 50;
while (newlyStartedApps.Count > 0 && retryCount > 0)
{
List<Application> finishedApps = new List<Application>();
actualSetup = await GetActualSetup();
foreach (Application app in newlyStartedApps)
{
IEnumerable<Application> candidates = actualSetup.Applications.Where(x => app.IsMyAppPath(x.AppPath));
if (candidates.Any())
{
if (app.AppPath.EndsWith("PowerToys.Settings.exe", StringComparison.Ordinal))
{
// give it time to not get confused (the app tries to position itself)
Thread.Sleep(1000);
}
else
{
// the other apps worked fine and reacted correctly to the MoveWindow event, but to be safe, give them also a little time
Thread.Sleep(100);
}
// just move the existing window to the desired position
Application existingApp = candidates.First();
NativeMethods.ShowWindow(existingApp.Hwnd, app.Minimized ? NativeMethods.SW_MINIMIZE : NativeMethods.SW_NORMAL);
if (!app.Minimized)
{
MoveWindowWithScaleAdjustment(project, existingApp.Hwnd, app, true);
NativeMethods.SetForegroundWindow(existingApp.Hwnd);
}
finishedApps.Add(app);
}
}
foreach (Application app in finishedApps)
{
newlyStartedApps.Remove(app);
}
Thread.Sleep(100);
retryCount--;
}
// update last launched time
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
project.LastLaunchedTime = (long)ts.TotalSeconds;
_projectsEditorIO.SerializeProjects(Projects.ToList());
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
/*await Task.Run(() => RunLauncher(project.Id));
await Task.Run(() => RunLauncher(project.Id));
if (_projectsEditorIO.ParseProjects(this).Result == true)
{
OnPropertyChanged(new PropertyChangedEventArgs("ProjectsView"));
}*/
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
}
if (exitAfterLaunch)
{
@@ -464,32 +257,6 @@ namespace ProjectsEditor.ViewModels
}
}
private void MoveWindowWithScaleAdjustment(Project project, nint hwnd, Application app, bool repaint)
{
NativeMethods.SetForegroundWindow(hwnd);
NativeMethods.MoveWindow(hwnd, app.ScaledPosition.X, app.ScaledPosition.Y, app.ScaledPosition.Width, app.ScaledPosition.Height, repaint);
}
private async Task<Project> GetActualSetup()
{
string filename = Path.GetTempFileName();
if (File.Exists(filename))
{
File.Delete(filename);
}
await Task.Run(() => RunSnapshotTool(filename));
Project actualProject;
_projectsEditorIO.ParseProject(filename, out actualProject);
if (File.Exists(filename))
{
File.Delete(filename);
}
return actualProject;
}
private void RunSnapshotTool(string filename = null)
{
Process p = new Process();

View File

@@ -1,20 +1,19 @@
#include "pch.h"
#include "AppLauncher.h"
#include <TlHelp32.h>
#include <future>
#include <iostream>
#include <string>
#include <vector>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <ShellScalingApi.h>
#include <shellapi.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <iostream>
#include "../projects-common/MonitorEnumerator.h"
#include "../projects-common/WindowEnumerator.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Management::Deployment;
namespace FancyZones
{
@@ -326,328 +325,95 @@ namespace FancyZones
}
}
namespace KBM
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs)
{
using namespace winrt;
using namespace Windows::UI::Notifications;
using namespace Windows::Data::Xml::Dom;
SHELLEXECUTEINFO shExecInfo;
shExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
shExecInfo.fMask = NULL;
shExecInfo.hwnd = NULL;
shExecInfo.lpVerb = NULL;
shExecInfo.lpFile = appPath.c_str();
shExecInfo.lpParameters = commandLineArgs.c_str();
shExecInfo.lpDirectory = NULL;
shExecInfo.nShow = SW_MAXIMIZE;
shExecInfo.hInstApp = NULL;
// Use to find a process by its name
std::wstring GetFileNameFromPath(const std::wstring& fullPath)
BOOL result = ShellExecuteEx(&shExecInfo);
return result;
}
bool LaunchPackagedApp(const std::wstring& packageFullName)
{
try
{
size_t found = fullPath.find_last_of(L"\\");
if (found != std::wstring::npos)
// Create a PackageManager object to get the package information.
PackageManager packageManager;
// Find the package by its full name.
for (const auto& package : packageManager.FindPackagesForUser({}))
{
return fullPath.substr(found + 1);
}
return fullPath;
}
DWORD GetProcessIdByName(const std::wstring& processName)
{
DWORD pid = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
if (package.Id().FullName() == packageFullName)
{
do
// Get the AppListEntry for the package.
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
auto appEntries = getAppListEntriesOperation.get();
// Launch the first app in the list.
if (appEntries.Size() > 0)
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
pid = processEntry.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return pid;
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
{
std::vector<DWORD> processIds;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
processIds.push_back(processEntry.th32ProcessID);
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return processIds;
}
struct handle_data
{
unsigned long process_id;
HWND window_handle;
};
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallbackAllowNonVisible(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id == process_id)
{
data.window_handle = handle;
return FALSE;
}
return TRUE;
}
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id != process_id || !(GetWindow(handle, GW_OWNER) == static_cast<HWND>(0) && IsWindowVisible(handle)))
{
return TRUE;
}
data.window_handle = handle;
return FALSE;
}
// used for reactivating a window for a program we already started.
HWND FindMainWindow(unsigned long process_id, const bool allowNonVisible)
{
handle_data data;
data.process_id = process_id;
data.window_handle = 0;
if (allowNonVisible)
{
EnumWindows(EnumWindowsCallbackAllowNonVisible, reinterpret_cast<LPARAM>(&data));
}
else
{
EnumWindows(EnumWindowsCallback, reinterpret_cast<LPARAM>(&data));
}
return data.window_handle;
}
bool ShowProgram(DWORD pid, std::wstring programName, bool isNewProcess, bool minimizeIfVisible, const RECT& windowPosition, int retryCount)
{
// a good place to look for this...
// https://github.com/ritchielawrence/cmdow
// try by main window.
auto allowNonVisible = true;
HWND hwnd = FindMainWindow(pid, allowNonVisible);
if (hwnd == NULL)
{
if (retryCount < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ShowProgram(pid, programName, isNewProcess, minimizeIfVisible, windowPosition, retryCount + 1);
return false;
});
}
}
else
{
if (hwnd == GetForegroundWindow())
{
// only hide if this was a call from a already open program, don't make small if we just opened it.
if (!isNewProcess && minimizeIfVisible)
{
return ShowWindow(hwnd, SW_MINIMIZE);
}
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return false;
}
else
{
// Check if the window is minimized
if (IsIconic(hwnd))
{
// Show the window since SetForegroundWindow fails on minimized windows
if (!ShowWindow(hwnd, SW_RESTORE))
{
std::wcout << L"ShowWindow failed" << std::endl;
}
}
//INPUT inputs[1] = { {.type = INPUT_MOUSE } };
//SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (!SetForegroundWindow(hwnd))
{
return false;
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
bool launchResult = launchOperation.get();
return launchResult;
}
else
{
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return true;
}
}
}
if (isNewProcess)
{
return true;
}
if (false)
{
// try by console.
hwnd = FindWindow(nullptr, nullptr);
if (AttachConsole(pid))
{
// Get the console window handle
hwnd = GetConsoleWindow();
auto showByConsoleSuccess = false;
if (hwnd != NULL)
{
ShowWindow(hwnd, SW_RESTORE);
if (SetForegroundWindow(hwnd))
{
showByConsoleSuccess = true;
}
}
// Detach from the console
FreeConsole();
if (showByConsoleSuccess)
{
return true;
}
}
}
// try to just show them all (if they have a title)!.
hwnd = FindWindow(nullptr, nullptr);
if (hwnd)
{
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
int length = GetWindowTextLength(hwnd);
if (length > 0)
{
ShowWindow(hwnd, SW_RESTORE);
// hwnd is the window handle with targetPid
if (SetForegroundWindow(hwnd))
{
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return true;
}
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
}
return false;
}
bool HideProgram(DWORD pid, std::wstring programName, int retryCount)
{
HWND hwnd = FindMainWindow(pid, false);
if (hwnd == NULL)
{
if (retryCount < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
HideProgram(pid, programName, retryCount + 1);
std::wcout << L"No app entries found for the package." << std::endl;
return false;
});
}
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
if (IsWindowVisible(hwnd))
{
ShowWindow(hwnd, SW_HIDE);
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
}
catch (const hresult_error& ex)
{
std::wcerr << L"Error: " << ex.message().c_str() << std::endl;
}
return false;
}
bool Launch(const Project::Application& app)
{
bool launched = false;
if (!app.packageFullName.empty() && app.commandLineArgs.empty())
{
std::wcout << L"Launching packaged " << app.name << std::endl;
launched = LaunchPackagedApp(app.packageFullName);
}
else
{
// TODO: verify app path is up to date.
// Packaged apps have version in the path, it will be outdated after update.
std::wcout << L"Launching " << app.name << " at " << app.path << std::endl;
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
std::wcout << L" File not found at " << app.path << std::endl;
return false;
}
return true;
launched = LaunchApp(app.path, app.commandLineArgs);
}
}
void Launch(const std::wstring& appPath, bool startMinimized, const std::wstring& commandLineArgs, const RECT& windowPosition) noexcept
{
WCHAR fullExpandedFilePath[MAX_PATH];
ExpandEnvironmentStrings(appPath.c_str(), fullExpandedFilePath, MAX_PATH);
auto fileNamePart = KBM::GetFileNameFromPath(fullExpandedFilePath);
if (launched)
{
std::wcout << L"Launched " << app.name << std::endl;
}
else
{
std::wcout << L"Failed to launch " << app.name << std::endl;
}
DWORD dwAttrib = GetFileAttributesW(fullExpandedFilePath);
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
return;
}
DWORD processId = 0;
if (Common::Utils::Elevation::run_non_elevated(fullExpandedFilePath, commandLineArgs, &processId, nullptr, !startMinimized))
{
std::wcout << "Launched " << fileNamePart << std::endl;
}
else
{
std::wcout << "Failed to launch " << fileNamePart << std::endl;
}
if (processId == 0)
{
return;
}
auto targetPid = KBM::GetProcessIdByName(fileNamePart);
if (!startMinimized)
{
KBM::ShowProgram(targetPid, fileNamePart, false, false, windowPosition, 0);
}
else
{
KBM::HideProgram(targetPid, fileNamePart, 0);
}
return;
return launched;
}

View File

@@ -1,5 +1,5 @@
#pragma once
#include <Windows.h>
#include "../projects-common/Data.h"
void Launch(const std::wstring& appPath, bool startMinimized, const std::wstring& commandLineArgs, const RECT& rect) noexcept;
bool Launch(const Project::Application& app);

View File

@@ -51,7 +51,7 @@ int main(int argc, char* argv[])
// launch apps
for (const auto& app : projectToLaunch.apps)
{
Launch(app.appPath, app.isMinimized, app.commandLineArgs, app.position.toRect());
Launch(app);
}
// update last-launched time

View File

@@ -0,0 +1,169 @@
#include "pch.h"
#include "PackagedAppUtils.h"
#include <windows.h>
#include <winreg.h>
#include <iostream>
#include <filesystem>
#include <atlbase.h>
#include <wil/registry.h>
#include <ShlObj.h>
#include <propvarutil.h>
namespace Utils
{
namespace Apps
{
namespace NonLocalizable
{
const wchar_t* PackageFullNameProp = L"System.AppUserModel.PackageFullName";
const wchar_t* PackageInstallPathProp = L"System.AppUserModel.PackageInstallPath";
const wchar_t* InstallPathProp = L"System.Link.TargetParsingPath";
const wchar_t* FileExplorerName = L"File Explorer";
const wchar_t* FileExplorerPath = L"C:\\WINDOWS\\EXPLORER.EXE";
}
AppList IterateAppsFolder()
{
AppList result{};
// get apps folder
CComPtr<IShellItem> folder;
HRESULT hr = SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder));
if (FAILED(hr))
{
return result;
}
CComPtr<IEnumShellItems> enumItems;
hr = folder->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&enumItems));
if (FAILED(hr))
{
return result;
}
IShellItem* items;
while (enumItems->Next(1, &items, nullptr) == S_OK)
{
CComPtr<IShellItem> item = items;
CComHeapPtr<wchar_t> name;
if (FAILED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &name)))
{
continue;
}
std::wcout << name.m_pData << std::endl;
AppData data
{
.name = std::wstring(name.m_pData),
};
// properties
CComPtr<IPropertyStore> store;
if (FAILED(item->BindToHandler(NULL, BHID_PropertyStore, IID_PPV_ARGS(&store))))
{
continue;
}
DWORD count = 0;
store->GetCount(&count);
for (DWORD i = 0; i < count; i++)
{
PROPERTYKEY pk;
if (FAILED(store->GetAt(i, &pk)))
{
continue;
}
CComHeapPtr<wchar_t> pkName;
PSGetNameFromPropertyKey(pk, &pkName);
std::wstring prop(pkName.m_pData);
if (prop == NonLocalizable::PackageFullNameProp ||
prop == NonLocalizable::PackageInstallPathProp ||
prop == NonLocalizable::InstallPathProp)
{
PROPVARIANT pv;
PropVariantInit(&pv);
if (SUCCEEDED(store->GetValue(pk, &pv)))
{
CComHeapPtr<wchar_t> propVariantString;
propVariantString.Allocate(512);
PropVariantToString(pv, propVariantString, 512);
PropVariantClear(&pv);
if (prop == NonLocalizable::PackageFullNameProp)
{
data.packageFullName = propVariantString.m_pData;
}
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
{
data.installPath = propVariantString.m_pData;
}
}
if (!data.packageFullName.empty() && !data.installPath.empty())
{
break;
}
}
}
if (!data.name.empty())
{
result.push_back(data);
}
}
return result;
}
AppList Utils::Apps::GetAppsList()
{
return IterateAppsFolder();
}
std::optional<AppData> GetApp(const std::wstring& appPath, const AppList& apps)
{
for (const auto& appData : apps)
{
std::wstring appPathUpper(appPath);
std::transform(appPathUpper.begin(), appPathUpper.end(), appPathUpper.begin(), towupper);
// edge case, "Windows Software Development Kit" has the same app path as "File Explorer"
if (appPathUpper == NonLocalizable::FileExplorerPath)
{
return AppData
{
.name = NonLocalizable::FileExplorerName,
.installPath = appPath,
};
}
std::wstring installPathUpper(appData.installPath);
std::transform(installPathUpper.begin(), installPathUpper.end(), installPathUpper.begin(), towupper);
if (appPathUpper.contains(installPathUpper))
{
return appData;
}
// edge case, some apps (e.g., Gitkraken) have different .exe files in the subfolders.
// apps list contains only one path, so in this case app is not found
if (std::filesystem::path(appPath).filename() == std::filesystem::path(appData.installPath).filename())
{
return appData;
}
}
// TODO: not all installed apps found
return AppData {
.installPath = appPath
};
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
namespace Utils
{
namespace Apps
{
struct AppData
{
std::wstring name;
std::wstring installPath;
std::wstring packageFullName;
};
using AppList = std::vector<AppData>; // path; data
AppList GetAppsList();
std::optional<AppData> GetApp(const std::wstring& appPath, const AppList& apps);
}
}

View File

@@ -70,7 +70,7 @@
<PreprocessorDefinitions>_CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /permissive- /bigobj</AdditionalOptions>
<LanguageStandard>stdcpplatest</LanguageStandard>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
@@ -81,7 +81,7 @@
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">shcore.lib;Shell32.lib;propsys.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Platform)'=='Win32'">
@@ -101,7 +101,7 @@
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">shcore.lib;Shell32.lib;propsys.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
@@ -117,6 +117,7 @@
<ItemGroup>
<ClInclude Include="MonitorUtils.h" />
<ClInclude Include="OnThreadExecutor.h" />
<ClInclude Include="PackagedAppUtils.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="VirtualDesktop.h" />
<ClInclude Include="WindowFilter.h" />
@@ -126,6 +127,7 @@
<ClCompile Include="main.cpp" />
<ClCompile Include="MonitorUtils.cpp" />
<ClCompile Include="OnThreadExecutor.cpp" />
<ClCompile Include="PackagedAppUtils.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
@@ -138,7 +140,8 @@
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -146,5 +149,6 @@
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -33,6 +33,9 @@
<ClInclude Include="OnThreadExecutor.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PackagedAppUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -53,9 +56,15 @@
<ClCompile Include="OnThreadExecutor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PackagedAppUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,7 @@
#include "../projects-common/WindowEnumerator.h"
#include "MonitorUtils.h"
#include "PackagedAppUtils.h"
#include "WindowFilter.h"
int main(int argc, char* argv[])
@@ -77,6 +78,9 @@ int main(int argc, char* argv[])
// get list of windows
auto windows = WindowEnumerator::Enumerate(WindowFilter::Filter);
// get installed apps list
auto apps = Utils::Apps::GetAppsList();
for (const auto& window : windows)
{
// filter by window rect size
@@ -95,7 +99,13 @@ int main(int argc, char* argv[])
// filter by app path
std::wstring processPath = Common::Utils::ProcessPath::get_process_path_waiting_uwp(window);
if (WindowUtils::IsExcludedByDefault(window, processPath, title))
if (processPath.empty() || WindowUtils::IsExcludedByDefault(window, processPath, title))
{
continue;
}
auto data = Utils::Apps::GetApp(processPath, apps);
if (!data.has_value())
{
continue;
}
@@ -112,9 +122,10 @@ int main(int argc, char* argv[])
}
Project::Application app {
.hwnd = window,
.appPath = processPath,
.appTitle = title,
.name = data.value().name,
.title = title,
.path = data.value().installPath,
.packageFullName = data.value().packageFullName,
.commandLineArgs = L"",
.isMinimized = WindowUtils::IsMinimized(window),
.isMaximized = WindowUtils::IsMaximized(window),

View File

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

View File

@@ -23,9 +23,10 @@ struct Project
}
};
HWND hwnd{};
std::wstring appPath;
std::wstring appTitle;
std::wstring name;
std::wstring title;
std::wstring path;
std::wstring packageFullName;
std::wstring commandLineArgs;
bool isMinimized{};
bool isMaximized{};
@@ -120,8 +121,9 @@ namespace JsonUtils
namespace NonLocalizable
{
const static wchar_t* AppPathID = L"application";
const static wchar_t* HwndID = L"hwnd";
const static wchar_t* AppNameID = L"application";
const static wchar_t* AppPathID = L"application-path";
const static wchar_t* AppPackageFullNameID = L"package-full-name";
const static wchar_t* AppTitleID = L"title";
const static wchar_t* CommandLineArgsID = L"command-line-arguments";
const static wchar_t* MinimizedID = L"minimized";
@@ -133,9 +135,10 @@ namespace JsonUtils
inline json::JsonObject ToJson(const Project::Application& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::HwndID, json::value(static_cast<double>(reinterpret_cast<long long>(data.hwnd))));
json.SetNamedValue(NonLocalizable::AppPathID, json::value(data.appPath));
json.SetNamedValue(NonLocalizable::AppTitleID, json::value(data.appTitle));
json.SetNamedValue(NonLocalizable::AppNameID, json::value(data.name));
json.SetNamedValue(NonLocalizable::AppPathID, json::value(data.path));
json.SetNamedValue(NonLocalizable::AppTitleID, json::value(data.title));
json.SetNamedValue(NonLocalizable::AppPackageFullNameID, json::value(data.packageFullName));
json.SetNamedValue(NonLocalizable::CommandLineArgsID, json::value(data.commandLineArgs));
json.SetNamedValue(NonLocalizable::MinimizedID, json::value(data.isMinimized));
json.SetNamedValue(NonLocalizable::MaximizedID, json::value(data.isMaximized));
@@ -150,9 +153,18 @@ namespace JsonUtils
Project::Application result;
try
{
result.hwnd = reinterpret_cast<HWND>(static_cast<long long>(json.GetNamedNumber(NonLocalizable::HwndID)));
result.appPath = json.GetNamedString(NonLocalizable::AppPathID);
result.appTitle = json.GetNamedString(NonLocalizable::AppTitleID);
if (json.HasKey(NonLocalizable::AppNameID))
{
result.name = json.GetNamedString(NonLocalizable::AppNameID);
}
result.path = json.GetNamedString(NonLocalizable::AppPathID);
result.title = json.GetNamedString(NonLocalizable::AppTitleID);
if (json.HasKey(NonLocalizable::AppPackageFullNameID))
{
result.packageFullName = json.GetNamedString(NonLocalizable::AppPackageFullNameID);
}
result.commandLineArgs = json.GetNamedString(NonLocalizable::CommandLineArgsID);
result.isMaximized = json.GetNamedBoolean(NonLocalizable::MaximizedID);
result.isMinimized = json.GetNamedBoolean(NonLocalizable::MinimizedID);