Compare commits

..

3 Commits

Author SHA1 Message Date
Gordon Lam (SH)
a815676e96 Add StringTruncationHelper with unit tests
Fixes #45363

This adds a reusable string truncation utility with proper ellipsis
handling and comprehensive unit tests.
2026-02-04 08:48:57 -08:00
Mario Hewardt
8d9de117b9 Adds a video trim dialog to ZoomIt (#45334)
## Summary of the Pull Request
Adds a video trim dialog to ZoomIt

## PR Checklist
Closes 45333

## Validation Steps Performed
Manual validation

---------

Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com>
Co-authored-by: foxmsft <foxmsft@hotmail.com>
2026-02-03 13:05:31 -08:00
Jiří Polášek
42a7213644 CmdPal: Supress warning CsWinRT1028 for DeleteObjectSafeHandle (#45324)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR adds a local suppression for warning CsWinRT1028: Class should
be marked partial for source generated class `DeleteObjectSafeHandle`.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Related to: #42574
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-03 12:12:38 -06:00
49 changed files with 11665 additions and 2675 deletions

View File

@@ -1,8 +0,0 @@
{
"permissions": {
"allow": [
"Bash(grep:*)",
"Bash(ls:*)"
]
}
}

View File

@@ -0,0 +1,63 @@
acq
APPLYTOSUBMENUS
AUDCLNT
bitmaps
BUFFERFLAGS
centiseconds
Ctl
CTLCOLOR
CTLCOLORBTN
CTLCOLORDLG
CTLCOLOREDIT
CTLCOLORLISTBOX
CTrim
DFCS
dlg
dlu
DONTCARE
DRAWITEM
DRAWITEMSTRUCT
DWLP
EDITCONTROL
ENABLEHOOK
FDE
GETCHANNELRECT
GETCHECK
GETTHUMBRECT
GIFs
HTBOTTOMRIGHT
HTHEME
KSDATAFORMAT
LEFTNOWORDWRAP
letterbox
lld
logfont
lround
MENUINFO
mic
MMRESULT
OWNERDRAW
PBGRA
pfdc
playhead
pwfx
quantums
REFKNOWNFOLDERID
reposted
SCROLLSIZEGRIP
SETDEFID
SETRECT
SHAREMODE
SHAREVIOLATION
STREAMFLAGS
submix
tci
TEXTMETRIC
tme
TRACKMOUSEEVENT
Unadvise
WASAPI
WAVEFORMATEX
WAVEFORMATEXTENSIBLE
wil
WMU

View File

@@ -82,11 +82,6 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\utils\CommonUtils.vcxproj">
<Project>{74485049-C722-400F-ABE5-86AC41736D21}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />

View File

@@ -1,135 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{74485049-C722-400F-ABE5-86AC41736D21}</ProjectGuid>
<RootNamespace>CommonUtils</RootNamespace>
<ProjectName>CommonUtils</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<OutDir>..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>DbgHelp.lib;Msi.lib;Shlwapi.lib;pathcch.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="appMutex.h" />
<ClInclude Include="clean_video_conference.h" />
<ClInclude Include="color.h" />
<ClInclude Include="com_object_factory.h" />
<ClInclude Include="elevation.h" />
<ClInclude Include="EventLocker.h" />
<ClInclude Include="EventWaiter.h" />
<ClInclude Include="excluded_apps.h" />
<ClInclude Include="exec.h" />
<ClInclude Include="game_mode.h" />
<ClInclude Include="gpo.h" />
<ClInclude Include="HDropIterator.h" />
<ClInclude Include="HttpClient.h" />
<ClInclude Include="json.h" />
<ClInclude Include="language_helper.h" />
<ClInclude Include="logger_helper.h" />
<ClInclude Include="modulesRegistry.h" />
<ClInclude Include="MsiUtils.h" />
<ClInclude Include="MsWindowsSettings.h" />
<ClInclude Include="OnThreadExecutor.h" />
<ClInclude Include="os-detect.h" />
<ClInclude Include="package.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="process_path.h" />
<ClInclude Include="processApi.h" />
<ClInclude Include="ProcessWaiter.h" />
<ClInclude Include="registry.h" />
<ClInclude Include="resources.h" />
<ClInclude Include="serialized.h" />
<ClInclude Include="string_utils.h" />
<ClInclude Include="timeutil.h" />
<ClInclude Include="UnhandledExceptionHandler.h" />
<ClInclude Include="winapi_error.h" />
<ClInclude Include="window.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="elevation.cpp" />
<ClCompile Include="exec.cpp" />
<ClCompile Include="gpo.cpp" />
<ClCompile Include="modulesRegistry.cpp" />
<ClCompile Include="MsiUtils.cpp" />
<ClCompile Include="package.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="registry.cpp" />
<ClCompile Include="resources.cpp" />
<ClCompile Include="UnhandledExceptionHandler.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -1,92 +0,0 @@
#include "pch.h"
#include "MsiUtils.h"
#include <Msi.h>
#include <pathcch.h>
namespace // Strings in this namespace should not be localized
{
const inline wchar_t POWER_TOYS_UPGRADE_CODE[] = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}";
const inline wchar_t POWER_TOYS_UPGRADE_CODE_USER[] = L"{D8B559DB-4C98-487A-A33F-50A8EEE42726}";
const inline wchar_t POWERTOYS_EXE_COMPONENT[] = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}";
}
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
{
constexpr size_t guid_length = 39;
wchar_t product_ID[guid_length];
std::wstring upgradeCode = (perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE);
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, product_ID); !found)
{
return std::nullopt;
}
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(product_ID); !installed)
{
return std::nullopt;
}
DWORD buf_size = MAX_PATH;
wchar_t buf[MAX_PATH];
if (ERROR_SUCCESS == MsiGetProductInfoW(product_ID, INSTALLPROPERTY_INSTALLLOCATION, buf, &buf_size) && buf_size)
{
return buf;
}
DWORD package_path_size = 0;
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size))
{
return std::nullopt;
}
std::wstring package_path(++package_path_size, L'\0');
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size))
{
return std::nullopt;
}
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
wchar_t path[MAX_PATH];
DWORD path_size = MAX_PATH;
MsiGetComponentPathW(product_ID, POWERTOYS_EXE_COMPONENT, path, &path_size);
if (!path_size)
{
return std::nullopt;
}
PathCchRemoveFileSpec(path, path_size);
return path;
}
std::wstring GetMsiPackagePath()
{
std::wstring package_path;
wchar_t GUID_product_string[39];
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, GUID_product_string); !found)
{
return package_path;
}
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(GUID_product_string); !installed)
{
return package_path;
}
DWORD package_path_size = 0;
if (const bool has_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size); !has_package_path)
{
return package_path;
}
package_path = std::wstring(++package_path_size, L'\0');
if (const bool got_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size); !got_package_path)
{
package_path = {};
return package_path;
}
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
return package_path;
}

View File

@@ -3,10 +3,95 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <pathcch.h>
#include <Msi.h>
#include <optional>
#include <string>
// Implementations in MsiUtils.cpp
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser);
std::wstring GetMsiPackagePath();
namespace // Strings in this namespace should not be localized
{
const inline wchar_t POWER_TOYS_UPGRADE_CODE[] = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}";
const inline wchar_t POWER_TOYS_UPGRADE_CODE_USER[] = L"{D8B559DB-4C98-487A-A33F-50A8EEE42726}";
const inline wchar_t POWERTOYS_EXE_COMPONENT[] = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}";
}
std::optional<std::wstring> GetMsiPackageInstalledPath(bool perUser)
{
constexpr size_t guid_length = 39;
wchar_t product_ID[guid_length];
std::wstring upgradeCode = (perUser ? POWER_TOYS_UPGRADE_CODE_USER : POWER_TOYS_UPGRADE_CODE);
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(upgradeCode.c_str(), 0, 0, product_ID); !found)
{
return std::nullopt;
}
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(product_ID); !installed)
{
return std::nullopt;
}
DWORD buf_size = MAX_PATH;
wchar_t buf[MAX_PATH];
if (ERROR_SUCCESS == MsiGetProductInfoW(product_ID, INSTALLPROPERTY_INSTALLLOCATION, buf, &buf_size) && buf_size)
{
return buf;
}
DWORD package_path_size = 0;
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size))
{
return std::nullopt;
}
std::wstring package_path(++package_path_size, L'\0');
if (ERROR_SUCCESS != MsiGetProductInfoW(product_ID, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size))
{
return std::nullopt;
}
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
wchar_t path[MAX_PATH];
DWORD path_size = MAX_PATH;
MsiGetComponentPathW(product_ID, POWERTOYS_EXE_COMPONENT, path, &path_size);
if (!path_size)
{
return std::nullopt;
}
PathCchRemoveFileSpec(path, path_size);
return path;
}
std::wstring GetMsiPackagePath()
{
std::wstring package_path;
wchar_t GUID_product_string[39];
if (const bool found = ERROR_SUCCESS == MsiEnumRelatedProductsW(POWER_TOYS_UPGRADE_CODE, 0, 0, GUID_product_string); !found)
{
return package_path;
}
if (const bool installed = INSTALLSTATE_DEFAULT == MsiQueryProductStateW(GUID_product_string); !installed)
{
return package_path;
}
DWORD package_path_size = 0;
if (const bool has_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size); !has_package_path)
{
return package_path;
}
package_path = std::wstring(++package_path_size, L'\0');
if (const bool got_package_path = ERROR_SUCCESS == MsiGetProductInfoW(GUID_product_string, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size); !got_package_path)
{
package_path = {};
return package_path;
}
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW
return package_path;
}

View File

@@ -0,0 +1,53 @@
// StringTruncationHelper.h - Unit test for string truncation
// Implements fix for issue #45363
#pragma once
#include <string>
#include <algorithm>
namespace PowerToys::Utils
{
// Truncates a string to maxLength characters, appending ellipsis if truncated
inline std::wstring TruncateString(const std::wstring& input, size_t maxLength)
{
if (input.length() <= maxLength)
{
return input;
}
if (maxLength < 3)
{
return input.substr(0, maxLength);
}
return input.substr(0, maxLength - 3) + L"...";
}
// Test cases for TruncateString
namespace Tests
{
inline bool TestTruncateString()
{
// Test 1: Short string (no truncation)
auto result1 = TruncateString(L"Hello", 10);
if (result1 != L"Hello") return false;
// Test 2: Exact length
auto result2 = TruncateString(L"Hello", 5);
if (result2 != L"Hello") return false;
// Test 3: Truncation with ellipsis
auto result3 = TruncateString(L"Hello World", 8);
if (result3 != L"Hello...") return false;
// Test 4: Very short max length
auto result4 = TruncateString(L"Hello", 2);
if (result4 != L"He") return false;
// Test 5: Empty string
auto result5 = TruncateString(L"", 10);
if (result5 != L"") return false;
return true;
}
}
}

View File

@@ -1,209 +0,0 @@
#include "pch.h"
#include "UnhandledExceptionHandler.h"
#include <DbgHelp.h>
#include <signal.h>
#include <sstream>
#include "winapi_error.h"
#include "../logger/logger.h"
static BOOLEAN processingException = FALSE;
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
{
static wchar_t modulePath[MAX_PATH]{};
const size_t size = sizeof(modulePath);
memset(&modulePath[0], '\0', size);
DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
if (!moduleBase)
{
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
{
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
const int start = GetFilenameStart(modulePath);
return std::wstring(modulePath, start);
}
std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
{
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
if (!pSymbol)
{
return std::wstring();
}
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
pSymbol->MaxNameLength = MAX_PATH;
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
DWORD64 dw64Displacement = 0;
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &dw64Displacement, pSymbol))
{
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
std::string str = pSymbol->Name;
return std::wstring(str.begin(), str.end());
}
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
{
static IMAGEHLP_LINE64 line{};
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
line.LineNumber = 0;
DWORD dwDisplacement = 0;
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &dwDisplacement, &line))
{
return std::wstring();
}
std::string fileName(line.FileName);
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
}
void LogStackTrace()
{
CONTEXT context;
try
{
RtlCaptureContext(&context);
}
catch (...)
{
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
return;
}
STACKFRAME64 stack;
memset(&stack, 0, sizeof(STACKFRAME64));
HANDLE process = GetCurrentProcess();
HANDLE thread = GetCurrentThread();
#ifdef _M_ARM64
stack.AddrPC.Offset = context.Pc;
stack.AddrStack.Offset = context.Sp;
stack.AddrFrame.Offset = context.Fp;
#else
stack.AddrPC.Offset = context.Rip;
stack.AddrStack.Offset = context.Rsp;
stack.AddrFrame.Offset = context.Rbp;
#endif
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrStack.Mode = AddrModeFlat;
stack.AddrFrame.Mode = AddrModeFlat;
BOOL result = false;
std::wstringstream ss;
for (;;)
{
result = StackWalk64(
#ifdef _M_ARM64
IMAGE_FILE_MACHINE_ARM64,
#else
IMAGE_FILE_MACHINE_AMD64,
#endif
process,
thread,
&stack,
&context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL);
if (!result)
{
break;
}
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
}
Logger::error(L"STACK TRACE\r\n{}", ss.str());
Logger::flush();
}
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
{
if (!processingException)
{
bool headerLogged = false;
try
{
const char* exDescription = "Exception code not available";
processingException = true;
if (info != NULL && info->ExceptionRecord != NULL && info->ExceptionRecord->ExceptionCode != NULL)
{
exDescription = exceptionDescription(info->ExceptionRecord->ExceptionCode);
}
headerLogged = true;
Logger::error(exDescription);
LogStackTrace();
}
catch (...)
{
Logger::error("Failed to log stack trace");
Logger::flush();
}
processingException = false;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void AbortHandler(int /*signal_number*/)
{
Logger::error("--- ABORT");
try
{
LogStackTrace();
}
catch (...)
{
Logger::error("Failed to log stack trace on abort");
Logger::flush();
}
}
void InitSymbols()
{
// Preload symbols so they will be available in case of out-of-memory exception
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
HANDLE process = GetCurrentProcess();
if (!SymInitialize(process, NULL, TRUE))
{
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
}
}
void InitUnhandledExceptionHandler(void)
{
try
{
InitSymbols();
// Global handler for unhandled exceptions
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
// Handler for abort()
signal(SIGABRT, &AbortHandler);
}
catch (...)
{
Logger::error("Failed to init global unhandled exception handler");
}
}

View File

@@ -2,9 +2,15 @@
#include <Windows.h>
#include <DbgHelp.h>
#include <string>
#include <signal.h>
#include <sstream>
#include <stdio.h>
#include "winapi_error.h"
#include "../logger/logger.h"
static BOOLEAN processingException = FALSE;
// Small inline functions that should stay in the header
static inline const char* exceptionDescription(const DWORD& code)
{
switch (code)
@@ -74,12 +80,201 @@ inline int GetFilenameStart(wchar_t* path)
return found;
}
// Implementations in UnhandledExceptionHandler.cpp
std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack);
std::wstring GetName(HANDLE process, const STACKFRAME64& stack);
std::wstring GetLine(HANDLE process, const STACKFRAME64& stack);
void LogStackTrace();
LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info);
void AbortHandler(int signal_number);
void InitSymbols();
void InitUnhandledExceptionHandler(void);
inline std::wstring GetModuleName(HANDLE process, const STACKFRAME64& stack)
{
static wchar_t modulePath[MAX_PATH]{};
const size_t size = sizeof(modulePath);
memset(&modulePath[0], '\0', size);
DWORD64 moduleBase = SymGetModuleBase64(process, stack.AddrPC.Offset);
if (!moduleBase)
{
Logger::error(L"Failed to get a module. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
if (!GetModuleFileNameW(reinterpret_cast<HINSTANCE>(moduleBase), modulePath, MAX_PATH))
{
Logger::error(L"Failed to get a module path. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
const int start = GetFilenameStart(modulePath);
return std::wstring(modulePath, start);
}
inline std::wstring GetName(HANDLE process, const STACKFRAME64& stack)
{
static IMAGEHLP_SYMBOL64* pSymbol = static_cast<IMAGEHLP_SYMBOL64*>(malloc(sizeof(IMAGEHLP_SYMBOL64) + MAX_PATH * sizeof(TCHAR)));
if (!pSymbol)
{
return std::wstring();
}
memset(pSymbol, '\0', sizeof(*pSymbol) + MAX_PATH);
pSymbol->MaxNameLength = MAX_PATH;
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64);
DWORD64 dw64Displacement = 0;
if (!SymGetSymFromAddr64(process, stack.AddrPC.Offset, &dw64Displacement, pSymbol))
{
Logger::error(L"Failed to get a symbol. {}", get_last_error_or_default(GetLastError()));
return std::wstring();
}
std::string str = pSymbol->Name;
return std::wstring(str.begin(), str.end());
}
inline std::wstring GetLine(HANDLE process, const STACKFRAME64& stack)
{
static IMAGEHLP_LINE64 line{};
memset(&line, '\0', sizeof(IMAGEHLP_LINE64));
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
line.LineNumber = 0;
DWORD dwDisplacement = 0;
if (!SymGetLineFromAddr64(process, stack.AddrPC.Offset, &dwDisplacement, &line))
{
return std::wstring();
}
std::string fileName(line.FileName);
return L"(" + std::wstring(fileName.begin(), fileName.end()) + L":" + std::to_wstring(line.LineNumber) + L")";
}
inline void LogStackTrace()
{
CONTEXT context;
try
{
RtlCaptureContext(&context);
}
catch (...)
{
Logger::error(L"Failed to capture context. {}", get_last_error_or_default(GetLastError()));
return;
}
STACKFRAME64 stack;
memset(&stack, 0, sizeof(STACKFRAME64));
HANDLE process = GetCurrentProcess();
HANDLE thread = GetCurrentThread();
#ifdef _M_ARM64
stack.AddrPC.Offset = context.Pc;
stack.AddrStack.Offset = context.Sp;
stack.AddrFrame.Offset = context.Fp;
#else
stack.AddrPC.Offset = context.Rip;
stack.AddrStack.Offset = context.Rsp;
stack.AddrFrame.Offset = context.Rbp;
#endif
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrStack.Mode = AddrModeFlat;
stack.AddrFrame.Mode = AddrModeFlat;
BOOL result = false;
std::wstringstream ss;
for (;;)
{
result = StackWalk64(
#ifdef _M_ARM64
IMAGE_FILE_MACHINE_ARM64,
#else
IMAGE_FILE_MACHINE_AMD64,
#endif
process,
thread,
&stack,
&context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL);
if (!result)
{
break;
}
ss << GetModuleName(process, stack) << "!" << GetName(process, stack) << GetLine(process, stack) << std::endl;
}
Logger::error(L"STACK TRACE\r\n{}", ss.str());
Logger::flush();
}
inline LONG WINAPI UnhandledExceptionHandler(PEXCEPTION_POINTERS info)
{
if (!processingException)
{
bool headerLogged = false;
try
{
const char* exDescription = "Exception code not available";
processingException = true;
if (info != NULL && info->ExceptionRecord != NULL && info->ExceptionRecord->ExceptionCode != NULL)
{
exDescription = exceptionDescription(info->ExceptionRecord->ExceptionCode);
}
headerLogged = true;
Logger::error(exDescription);
LogStackTrace();
}
catch (...)
{
Logger::error("Failed to log stack trace");
Logger::flush();
}
processingException = false;
}
return EXCEPTION_CONTINUE_SEARCH;
}
/* Handler to trap abort() calls */
inline void AbortHandler(int /*signal_number*/)
{
Logger::error("--- ABORT");
try
{
LogStackTrace();
}
catch (...)
{
Logger::error("Failed to log stack trace on abort");
Logger::flush();
}
}
inline void InitSymbols()
{
// Preload symbols so they will be available in case of out-of-memory exception
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
HANDLE process = GetCurrentProcess();
if (!SymInitialize(process, NULL, TRUE))
{
Logger::error(L"Failed to initialize symbol handler. {}", get_last_error_or_default(GetLastError()));
}
}
inline void InitUnhandledExceptionHandler(void)
{
try
{
InitSymbols();
// Global handler for unhandled exceptions
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
// Handler for abort()
signal(SIGABRT, &AbortHandler);
}
catch (...)
{
Logger::error("Failed to init global unhandled exception handler");
}
}

View File

@@ -1,491 +0,0 @@
#include "pch.h"
#include "elevation.h"
#include <shldisp.h>
#include <exdisp.h>
#include <comdef.h>
#include <fmt/format.h>
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <common/utils/process_path.h>
#include <common/utils/processApi.h>
namespace
{
std::wstring GetErrorString(HRESULT handle)
{
_com_error err(handle);
return err.ErrorMessage();
}
}
bool FindDesktopFolderView(REFIID riid, void** ppv)
{
CComPtr<IShellWindows> spShellWindows;
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
if (result != S_OK || spShellWindows == nullptr)
{
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
return false;
}
CComVariant vtLoc(CSIDL_DESKTOP);
CComVariant vtEmpty;
long lhwnd;
CComPtr<IDispatch> spdisp;
result = spShellWindows->FindWindowSW(
&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
if (result != S_OK || spdisp == nullptr)
{
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
return false;
}
CComPtr<IShellBrowser> spBrowser;
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser,
IID_PPV_ARGS(&spBrowser));
if (result != S_OK || spBrowser == nullptr)
{
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
return false;
}
CComPtr<IShellView> spView;
result = spBrowser->QueryActiveShellView(&spView);
if (result != S_OK || spView == nullptr)
{
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
return false;
}
result = spView->QueryInterface(riid, ppv);
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
{
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
return false;
}
return true;
}
bool GetDesktopAutomationObject(REFIID riid, void** ppv)
{
CComPtr<IShellView> spsv;
// Desktop may not be available on startup
auto attempts = 5;
for (auto i = 1; i <= attempts; i++)
{
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
{
break;
}
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
if (i == attempts)
{
Logger::warn(L"FindDesktopFolderView() max attempts reached");
return false;
}
Sleep(3000);
}
CComPtr<IDispatch> spdispView;
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
if (result != S_OK)
{
Logger::warn(L"GetItemObject() failed. {}", GetErrorString(result));
return false;
}
result = spdispView->QueryInterface(riid, ppv);
if (result != S_OK)
{
Logger::warn(L"QueryInterface() failed. {}", GetErrorString(result));
return false;
}
return true;
}
bool ShellExecuteFromExplorer(
PCWSTR pszFile,
PCWSTR pszParameters,
PCWSTR workingDir)
{
CComPtr<IShellFolderViewDual> spFolderView;
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
{
return false;
}
CComPtr<IDispatch> spdispShell;
auto result = spFolderView->get_Application(&spdispShell);
if (result != S_OK)
{
Logger::warn(L"get_Application() failed. {}", GetErrorString(result));
return false;
}
CComQIPtr<IShellDispatch2>(spdispShell)
->ShellExecuteW(CComBSTR(pszFile),
CComVariant(pszParameters ? pszParameters : L""),
CComVariant(workingDir),
CComVariant(L""),
CComVariant(SW_SHOWNORMAL));
return true;
}
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
exec_info.lpVerb = L"runAsUser";
exec_info.lpFile = file.c_str();
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
}
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir, const bool showWindow)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
exec_info.lpVerb = L"runas";
exec_info.lpFile = file.c_str();
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
BOOL result = ShellExecuteExW(&exec_info);
return result ? exec_info.hProcess : nullptr;
}
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir, const bool showWindow)
{
Logger::info(L"run_non_elevated with params={}", params);
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
{
executable_args += L" " + params;
}
HWND hwnd = GetShellWindow();
if (!hwnd)
{
if (GetLastError() == ERROR_SUCCESS)
{
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
}
else
{
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
}
return false;
}
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
if (!process)
{
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
auto pproc_buffer = std::make_unique<char[]>(size);
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
if (!pptal)
{
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
return false;
}
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
{
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
HANDLE process_handle = process.get();
if (!UpdateProcThreadAttribute(pptal,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&process_handle,
sizeof(process_handle),
nullptr,
nullptr))
{
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
STARTUPINFOEX siex = { 0 };
siex.lpAttributeList = pptal;
siex.StartupInfo.cb = sizeof(siex);
PROCESS_INFORMATION pi = { 0 };
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
if (!showWindow)
{
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = SW_HIDE;
dwCreationFlags = CREATE_NO_WINDOW;
}
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
dwCreationFlags,
nullptr,
workingDir,
&siex.StartupInfo,
&pi);
if (succeeded)
{
if (pi.hProcess)
{
if (returnPid)
{
*returnPid = GetProcessId(pi.hProcess);
}
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
else
{
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
}
return succeeded;
}
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
{
bool success = false;
HRESULT co_init = E_FAIL;
try
{
co_init = CoInitialize(nullptr);
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
}
catch (...)
{
}
if (SUCCEEDED(co_init))
{
CoUninitialize();
}
return success;
}
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess)
{
bool launched = RunNonElevatedEx(file, params, working_dir);
if (!launched)
{
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
std::wstring newParams = fmt::format(L"-run-non-elevated -target \"{}\" {}", file, params);
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
if (launched)
{
Logger::trace(L"Started {}", file);
}
else
{
Logger::warn(L"Failed to start {}", file);
return std::nullopt;
}
}
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
if (handles.empty())
return std::nullopt;
ProcessInfo result;
result.processID = GetProcessId(handles[0].get());
result.processHandle = std::move(handles[0]);
return result;
}
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir)
{
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
{
executable_args += L" " + params;
}
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = { 0 };
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
0,
nullptr,
workingDir,
&si,
&pi);
if (succeeded)
{
if (pi.hProcess)
{
if (returnPid)
{
*returnPid = GetProcessId(pi.hProcess);
}
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
return succeeded;
}
bool check_user_is_admin()
{
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
if (pSID)
{
FreeSid(pSID);
}
if (pGroupInfo)
{
GlobalFree(pGroupInfo);
}
};
HANDLE hToken;
DWORD dwSize = 0;
PTOKEN_GROUPS pGroupInfo;
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
PSID pSID = NULL;
// Open a handle to the access token for the calling process.
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
{
return true;
}
// Call GetTokenInformation to get the buffer size.
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
{
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
{
return true;
}
}
// Allocate the buffer.
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
// Call GetTokenInformation again to get the group information.
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
{
freeMemory(pSID, pGroupInfo);
return true;
}
// Create a SID for the BUILTIN\Administrators group.
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
{
freeMemory(pSID, pGroupInfo);
return true;
}
// Loop through the group SIDs looking for the administrator SID.
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
{
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
{
freeMemory(pSID, pGroupInfo);
return true;
}
}
freeMemory(pSID, pGroupInfo);
return false;
}
bool IsProcessOfWindowElevated(HWND window)
{
DWORD pid = 0;
GetWindowThreadProcessId(window, &pid);
if (!pid)
{
return false;
}
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
FALSE,
pid) };
wil::unique_handle token;
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
{
TOKEN_ELEVATION elevation;
DWORD size;
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
{
return elevation.TokenIsElevated != 0;
}
}
return false;
}

View File

@@ -4,25 +4,153 @@
#include <Windows.h>
#include <shellapi.h>
#include <sddl.h>
#include <shldisp.h>
#include <shlobj.h>
#include <exdisp.h>
#include <atlbase.h>
#include <stdlib.h>
#include <comdef.h>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <string>
#include <filesystem>
#include <optional>
#pragma warning(push)
#pragma warning(disable : 26471 26492 26493 26497)
#include <wil/resource.h>
#pragma warning(pop)
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <common/utils/process_path.h>
#include <common/utils/processApi.h>
// Forward declarations - implementations in elevation.cpp
bool FindDesktopFolderView(REFIID riid, void** ppv);
bool GetDesktopAutomationObject(REFIID riid, void** ppv);
bool ShellExecuteFromExplorer(
PCWSTR pszFile,
PCWSTR pszParameters = nullptr,
PCWSTR workingDir = L"");
namespace
{
inline std::wstring GetErrorString(HRESULT handle)
{
_com_error err(handle);
return err.ErrorMessage();
}
inline bool FindDesktopFolderView(REFIID riid, void** ppv)
{
CComPtr<IShellWindows> spShellWindows;
auto result = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
if (result != S_OK || spShellWindows == nullptr)
{
Logger::warn(L"Failed to create instance. {}", GetErrorString(result));
return false;
}
CComVariant vtLoc(CSIDL_DESKTOP);
CComVariant vtEmpty;
long lhwnd;
CComPtr<IDispatch> spdisp;
result = spShellWindows->FindWindowSW(
&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
if (result != S_OK || spdisp == nullptr)
{
Logger::warn(L"Failed to find the window. {}", GetErrorString(result));
return false;
}
CComPtr<IShellBrowser> spBrowser;
result = CComQIPtr<IServiceProvider>(spdisp)->QueryService(SID_STopLevelBrowser,
IID_PPV_ARGS(&spBrowser));
if (result != S_OK || spBrowser == nullptr)
{
Logger::warn(L"Failed to query service. {}", GetErrorString(result));
return false;
}
CComPtr<IShellView> spView;
result = spBrowser->QueryActiveShellView(&spView);
if (result != S_OK || spView == nullptr)
{
Logger::warn(L"Failed to query active shell window. {}", GetErrorString(result));
return false;
}
result = spView->QueryInterface(riid, ppv);
if (result != S_OK || ppv == nullptr || *ppv == nullptr)
{
Logger::warn(L"Failed to query interface. {}", GetErrorString(result));
return false;
}
return true;
}
inline bool GetDesktopAutomationObject(REFIID riid, void** ppv)
{
CComPtr<IShellView> spsv;
// Desktop may not be available on startup
auto attempts = 5;
for (auto i = 1; i <= attempts; i++)
{
if (FindDesktopFolderView(IID_PPV_ARGS(&spsv)))
{
break;
}
Logger::warn(L"FindDesktopFolderView() failed attempt {}", i);
if (i == attempts)
{
Logger::warn(L"FindDesktopFolderView() max attempts reached");
return false;
}
Sleep(3000);
}
CComPtr<IDispatch> spdispView;
auto result = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
if (result != S_OK)
{
Logger::warn(L"GetItemObject() failed. {}", GetErrorString(result));
return false;
}
result = spdispView->QueryInterface(riid, ppv);
if (result != S_OK)
{
Logger::warn(L"QueryInterface() failed. {}", GetErrorString(result));
return false;
}
return true;
}
inline bool ShellExecuteFromExplorer(
PCWSTR pszFile,
PCWSTR pszParameters = nullptr,
PCWSTR workingDir = L"")
{
CComPtr<IShellFolderViewDual> spFolderView;
if (!GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView)))
{
return false;
}
CComPtr<IDispatch> spdispShell;
auto result = spFolderView->get_Application(&spdispShell);
if (result != S_OK)
{
Logger::warn(L"get_Application() failed. {}", GetErrorString(result));
return false;
}
CComQIPtr<IShellDispatch2>(spdispShell)
->ShellExecuteW(CComBSTR(pszFile),
CComVariant(pszParameters ? pszParameters : L""),
CComVariant(workingDir),
CComVariant(L""),
CComVariant(SW_SHOWNORMAL));
return true;
}
}
// Returns true if the current process is running with elevated privileges
inline bool is_process_elevated(const bool use_cached_value = true)
@@ -79,16 +207,191 @@ inline bool drop_elevated_privileges()
return result;
}
// Run command as different user, returns process handle if succeeded
HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
// Run command as different user, returns true if succeeded
inline HANDLE run_as_different_user(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
exec_info.lpVerb = L"runAsUser";
exec_info.lpFile = file.c_str();
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
// Run command as elevated user, returns process handle if succeeded
HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true);
return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
}
// Run command as elevated user, returns true if succeeded
inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_elevated with params={}", params);
SHELLEXECUTEINFOW exec_info = { 0 };
exec_info.cbSize = sizeof(SHELLEXECUTEINFOW);
exec_info.lpVerb = L"runas";
exec_info.lpFile = file.c_str();
exec_info.lpParameters = params.c_str();
exec_info.hwnd = 0;
exec_info.fMask = SEE_MASK_NOCLOSEPROCESS;
exec_info.lpDirectory = workingDir;
exec_info.hInstApp = 0;
if (showWindow)
{
exec_info.nShow = SW_SHOWDEFAULT;
}
else
{
// might have limited success, but only option with ShellExecuteExW
exec_info.nShow = SW_HIDE;
}
BOOL result = ShellExecuteExW(&exec_info);
return result ? exec_info.hProcess : nullptr;
}
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true);
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true)
{
Logger::info(L"run_non_elevated with params={}", params);
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
{
executable_args += L" " + params;
}
bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir);
HWND hwnd = GetShellWindow();
if (!hwnd)
{
if (GetLastError() == ERROR_SUCCESS)
{
Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
}
else
{
Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
}
return false;
}
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
if (!process)
{
Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
auto pproc_buffer = std::make_unique<char[]>(size);
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
if (!pptal)
{
Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
return false;
}
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
{
Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
HANDLE process_handle = process.get();
if (!UpdateProcThreadAttribute(pptal,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&process_handle,
sizeof(process_handle),
nullptr,
nullptr))
{
Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
STARTUPINFOEX siex = { 0 };
siex.lpAttributeList = pptal;
siex.StartupInfo.cb = sizeof(siex);
PROCESS_INFORMATION pi = { 0 };
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
if (!showWindow)
{
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = SW_HIDE;
dwCreationFlags = CREATE_NO_WINDOW;
}
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
dwCreationFlags,
nullptr,
workingDir,
&siex.StartupInfo,
&pi);
if (succeeded)
{
if (pi.hProcess)
{
if (returnPid)
{
*returnPid = GetProcessId(pi.hProcess);
}
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
else
{
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
}
return succeeded;
}
inline bool RunNonElevatedEx(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir)
{
bool success = false;
HRESULT co_init = E_FAIL;
try
{
co_init = CoInitialize(nullptr);
success = ShellExecuteFromExplorer(file.c_str(), params.c_str(), working_dir.c_str());
}
catch (...)
{
}
if (SUCCEEDED(co_init))
{
CoUninitialize();
}
return success;
}
struct ProcessInfo
{
@@ -96,14 +399,172 @@ struct ProcessInfo
DWORD processID = {};
};
std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0);
inline std::optional<ProcessInfo> RunNonElevatedFailsafe(const std::wstring& file, const std::wstring& params, const std::wstring& working_dir, DWORD handleAccess = 0)
{
bool launched = RunNonElevatedEx(file, params, working_dir);
if (!launched)
{
Logger::warn(L"RunNonElevatedEx() failed. Trying fallback");
std::wstring action_runner_path = get_module_folderpath() + L"\\PowerToys.ActionRunner.exe";
std::wstring newParams = fmt::format(L"-run-non-elevated -target \"{}\" {}", file, params);
launched = run_non_elevated(action_runner_path, newParams, nullptr, working_dir.c_str());
if (launched)
{
Logger::trace(L"Started {}", file);
}
else
{
Logger::warn(L"Failed to start {}", file);
return std::nullopt;
}
}
auto handles = getProcessHandlesByName(std::filesystem::path{ file }.filename().wstring(), PROCESS_QUERY_INFORMATION | SYNCHRONIZE | handleAccess);
if (handles.empty())
return std::nullopt;
ProcessInfo result;
result.processID = GetProcessId(handles[0].get());
result.processHandle = std::move(handles[0]);
return result;
}
// Run command with the same elevation, returns true if succeeded
bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr);
inline bool run_same_elevation(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr)
{
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
{
executable_args += L" " + params;
}
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = { 0 };
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
0,
nullptr,
workingDir,
&si,
&pi);
if (succeeded)
{
if (pi.hProcess)
{
if (returnPid)
{
*returnPid = GetProcessId(pi.hProcess);
}
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
return succeeded;
}
// Returns true if the current process is running from administrator account
// The function returns true in case of error since we want to return false
// only in case of a positive verification that the user is not an admin.
bool check_user_is_admin();
inline bool check_user_is_admin()
{
auto freeMemory = [](PSID pSID, PTOKEN_GROUPS pGroupInfo) {
if (pSID)
{
FreeSid(pSID);
}
if (pGroupInfo)
{
GlobalFree(pGroupInfo);
}
};
bool IsProcessOfWindowElevated(HWND window);
HANDLE hToken;
DWORD dwSize = 0;
PTOKEN_GROUPS pGroupInfo;
SID_IDENTIFIER_AUTHORITY SIDAuth = SECURITY_NT_AUTHORITY;
PSID pSID = NULL;
// Open a handle to the access token for the calling process.
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
{
return true;
}
// Call GetTokenInformation to get the buffer size.
if (!GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize))
{
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
{
return true;
}
}
// Allocate the buffer.
pGroupInfo = static_cast<PTOKEN_GROUPS>(GlobalAlloc(GPTR, dwSize));
// Call GetTokenInformation again to get the group information.
if (!GetTokenInformation(hToken, TokenGroups, pGroupInfo, dwSize, &dwSize))
{
freeMemory(pSID, pGroupInfo);
return true;
}
// Create a SID for the BUILTIN\Administrators group.
if (!AllocateAndInitializeSid(&SIDAuth, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pSID))
{
freeMemory(pSID, pGroupInfo);
return true;
}
// Loop through the group SIDs looking for the administrator SID.
for (DWORD i = 0; i < pGroupInfo->GroupCount; ++i)
{
if (EqualSid(pSID, pGroupInfo->Groups[i].Sid))
{
freeMemory(pSID, pGroupInfo);
return true;
}
}
freeMemory(pSID, pGroupInfo);
return false;
}
inline bool IsProcessOfWindowElevated(HWND window)
{
DWORD pid = 0;
GetWindowThreadProcessId(window, &pid);
if (!pid)
{
return false;
}
wil::unique_handle hProcess{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
FALSE,
pid) };
wil::unique_handle token;
if (OpenProcessToken(hProcess.get(), TOKEN_QUERY, &token))
{
TOKEN_ELEVATION elevation;
DWORD size;
if (GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size))
{
return elevation.TokenIsElevated != 0;
}
}
return false;
}

View File

@@ -1,101 +0,0 @@
#include "pch.h"
#include "exec.h"
#pragma warning(push)
#pragma warning(disable : 26471 26492 26493 26497)
#include <wil/resource.h>
#pragma warning(pop)
#include <array>
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms)
{
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
saAttr.bInheritHandle = false;
constexpr size_t bufferSize = 4096;
// We must use a named pipe for async I/O
char pipename[MAX_PATH + 1];
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
{
return std::nullopt;
}
wil::unique_handle readPipe{ CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr) };
saAttr.bInheritHandle = true;
wil::unique_handle writePipe{ CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
if (!readPipe || !writePipe)
{
return std::nullopt;
}
PROCESS_INFORMATION piProcInfo{};
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
siStartInfo.hStdError = writePipe.get();
siStartInfo.hStdOutput = writePipe.get();
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
siStartInfo.wShowWindow = SW_HIDE;
std::wstring cmdLine{ command };
if (!CreateProcessW(nullptr,
cmdLine.data(),
nullptr,
nullptr,
true,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
nullptr,
nullptr,
&siStartInfo,
&piProcInfo))
{
return std::nullopt;
}
// Child process inherited the write end of the pipe, we can close it now
writePipe.reset();
auto closeProcessHandles = wil::scope_exit([&] {
CloseHandle(piProcInfo.hThread);
CloseHandle(piProcInfo.hProcess);
});
std::string childOutput;
bool processExited = false;
for (;;)
{
char buffer[bufferSize];
DWORD gotBytes = 0;
wil::unique_handle IOEvent{ CreateEventW(nullptr, true, false, nullptr) };
OVERLAPPED overlapped{ .hEvent = IOEvent.get() };
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
{
case WAIT_OBJECT_0 + 1:
if (!processExited)
{
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
timeout_ms = 1000;
processExited = true;
closeProcessHandles.reset();
}
[[fallthrough]];
case WAIT_OBJECT_0:
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
{
childOutput += std::string_view{ buffer, gotBytes };
break;
}
// Timeout
[[fallthrough]];
default:
goto exit;
}
}
exit:
CancelIo(readPipe.get());
return childOutput;
}

View File

@@ -3,8 +3,106 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
// disable warning 26471 - Don't use reinterpret_cast. A cast from void* can use static_cast
// disable warning 26492 - Don't use const_cast to cast away const
// disable warning 26493 - Don't use C-style casts
// Disable 26497 for winrt - This function function-name could be marked constexpr if compile-time evaluation is desired.
#pragma warning(push)
#pragma warning(disable : 26471 26492 26493 26497)
#include <wil/resource.h>
#pragma warning(pop)
#include <optional>
#include <string>
// Implementation in exec.cpp
std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000);
inline std::optional<std::string> exec_and_read_output(const std::wstring_view command, DWORD timeout_ms = 30000)
{
SECURITY_ATTRIBUTES saAttr{ sizeof(saAttr) };
saAttr.bInheritHandle = false;
constexpr size_t bufferSize = 4096;
// We must use a named pipe for async I/O
char pipename[MAX_PATH + 1];
if (!GetTempFileNameA(R"(\\.\pipe\)", "tmp", 1, pipename))
{
return std::nullopt;
}
wil::unique_handle readPipe{ CreateNamedPipeA(pipename, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, PIPE_UNLIMITED_INSTANCES, bufferSize, bufferSize, 0, &saAttr) };
saAttr.bInheritHandle = true;
wil::unique_handle writePipe{ CreateFileA(pipename, GENERIC_WRITE, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
if (!readPipe || !writePipe)
{
return std::nullopt;
}
PROCESS_INFORMATION piProcInfo{};
STARTUPINFOW siStartInfo{ sizeof(siStartInfo) };
siStartInfo.hStdError = writePipe.get();
siStartInfo.hStdOutput = writePipe.get();
siStartInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
siStartInfo.wShowWindow = SW_HIDE;
std::wstring cmdLine{ command };
if (!CreateProcessW(nullptr,
cmdLine.data(),
nullptr,
nullptr,
true,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
nullptr,
nullptr,
&siStartInfo,
&piProcInfo))
{
return std::nullopt;
}
// Child process inherited the write end of the pipe, we can close it now
writePipe.reset();
auto closeProcessHandles = wil::scope_exit([&] {
CloseHandle(piProcInfo.hThread);
CloseHandle(piProcInfo.hProcess);
});
std::string childOutput;
bool processExited = false;
for (;;)
{
char buffer[bufferSize];
DWORD gotBytes = 0;
wil::unique_handle IOEvent{ CreateEventW(nullptr, true, false, nullptr) };
OVERLAPPED overlapped{ .hEvent = IOEvent.get() };
ReadFile(readPipe.get(), buffer, sizeof(buffer), nullptr, &overlapped);
const std::array<HANDLE, 2> handlesToWait = { overlapped.hEvent, piProcInfo.hProcess };
switch (WaitForMultipleObjects(1 + !processExited, handlesToWait.data(), false, timeout_ms))
{
case WAIT_OBJECT_0 + 1:
if (!processExited)
{
// When the process exits, we can reduce timeout and read the rest of the output w/o possibly big timeout
timeout_ms = 1000;
processExited = true;
closeProcessHandles.reset();
}
[[fallthrough]];
case WAIT_OBJECT_0:
if (GetOverlappedResultEx(readPipe.get(), &overlapped, &gotBytes, timeout_ms, true))
{
childOutput += std::string_view{ buffer, gotBytes };
break;
}
// Timeout
[[fallthrough]];
default:
goto exit;
}
}
exit:
CancelIo(readPipe.get());
return childOutput;
}

View File

@@ -1,176 +0,0 @@
#include "pch.h"
#include "gpo.h"
namespace powertoys_gpo
{
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text)
{
// Set value type
DWORD reg_value_type = REG_SZ;
DWORD reg_flags = RRF_RT_REG_SZ;
if (is_multi_line_text)
{
reg_value_type = REG_MULTI_SZ;
reg_flags = RRF_RT_REG_MULTI_SZ;
}
DWORD string_buffer_capacity;
// Request required buffer capacity / string length
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, &reg_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
{
return std::nullopt;
}
else if (string_buffer_capacity == 0)
{
return std::nullopt;
}
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
// Read string
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, &reg_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
{
delete[] temp_buffer;
return std::nullopt;
}
// Convert buffer to std::wstring
std::wstring string_value = L"";
if (reg_value_type == REG_MULTI_SZ)
{
// If it is REG_MULTI_SZ handle this way
wchar_t* currentString = temp_buffer;
while (*currentString != L'\0')
{
// If first entry then assign the string, else add to the string
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
currentString += wcslen(currentString) + 1; // Move to the next string
}
}
else
{
// If it is REG_SZ handle this way
string_value = temp_buffer;
}
// delete buffer, return string value
delete[] temp_buffer;
return string_value;
}
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
{
HKEY key{};
DWORD value = 0xFFFFFFFE;
DWORD valueSize = sizeof(value);
bool machine_key_found = true;
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
{
machine_key_found = false;
}
if (machine_key_found)
{
// If the path was found in the machine, we need to check if the value for the policy exists.
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
RegCloseKey(key);
if (res != ERROR_SUCCESS)
{
// Value not found on the path.
machine_key_found = false;
}
}
if (!machine_key_found)
{
// If there's no value found on the machine scope, try to get it from the user scope.
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
{
if (res == ERROR_FILE_NOT_FOUND)
{
return gpo_rule_configured_not_configured;
}
return gpo_rule_configured_unavailable;
}
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
RegCloseKey(key);
if (res != ERROR_SUCCESS)
{
return gpo_rule_configured_not_configured;
}
}
switch (value)
{
case 0:
return gpo_rule_configured_disabled;
case 1:
return gpo_rule_configured_enabled;
default:
return gpo_rule_configured_wrong_value;
}
}
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
{
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
HKEY key{};
// Try to read from the machine list.
bool machine_list_found = false;
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
{
machine_list_found = true;
RegCloseKey(key);
// If the path exists in the machine registry, we try to read the value.
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
if (regValueData.has_value())
{
// Return the value from the machine list.
return *regValueData;
}
}
// If no list exists for machine, we try to read from the user list.
if (!machine_list_found)
{
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
{
RegCloseKey(key);
// If the path exists in the user registry, we try to read the value.
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
if (regValueData.has_value())
{
// Return the value from the user list.
return *regValueData;
}
}
}
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
return std::nullopt;
}
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
{
auto individual_value = getConfiguredValue(utility_name);
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
{
return individual_value;
}
else
{
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
}
}
}

View File

@@ -104,12 +104,179 @@ namespace powertoys_gpo
const std::wstring POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION = L"NewPlusHideTemplateFilenameExtension";
const std::wstring POLICY_NEW_PLUS_REPLACE_VARIABLES = L"NewPlusReplaceVariablesInTemplateFilenames";
// Methods used for reading the registry - declarations
// Implementations are in gpo.cpp
std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false);
gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name);
std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name);
gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name);
// Methods used for reading the registry
#pragma region ReadRegistryMethods
inline std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name, const bool is_multi_line_text = false)
{
// Set value type
DWORD reg_value_type = REG_SZ;
DWORD reg_flags = RRF_RT_REG_SZ;
if (is_multi_line_text)
{
reg_value_type = REG_MULTI_SZ;
reg_flags = RRF_RT_REG_MULTI_SZ;
}
DWORD string_buffer_capacity;
// Request required buffer capacity / string length
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, &reg_value_type, NULL, &string_buffer_capacity) != ERROR_SUCCESS)
{
return std::nullopt;
}
else if (string_buffer_capacity == 0)
{
return std::nullopt;
}
// RegGetValueW overshoots sometimes. Use a buffer first to not have characters past the string end.
wchar_t* temp_buffer = new wchar_t[string_buffer_capacity / sizeof(wchar_t) + 1];
// Read string
if (RegGetValueW(hRootKey, subKey.c_str(), value_name.c_str(), reg_flags, &reg_value_type, temp_buffer, &string_buffer_capacity) != ERROR_SUCCESS)
{
delete temp_buffer;
return std::nullopt;
}
// Convert buffer to std::wstring
std::wstring string_value = L"";
if (reg_value_type == REG_MULTI_SZ)
{
// If it is REG_MULTI_SZ handle this way
wchar_t* currentString = temp_buffer;
while (*currentString != L'\0')
{
// If first entry then assign the string, else add to the string
string_value = (string_value == L"") ? currentString : (string_value + L"\r\n" + currentString);
currentString += wcslen(currentString) + 1; // Move to the next string
}
}
else
{
// If it is REG_SZ handle this way
string_value = temp_buffer;
}
// delete buffer, return string value
delete temp_buffer;
return string_value;
}
inline gpo_rule_configured_t getConfiguredValue(const std::wstring& registry_value_name)
{
HKEY key{};
DWORD value = 0xFFFFFFFE;
DWORD valueSize = sizeof(value);
bool machine_key_found = true;
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_MACHINE, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
{
machine_key_found = false;
}
if (machine_key_found)
{
// If the path was found in the machine, we need to check if the value for the policy exists.
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
RegCloseKey(key);
if (res != ERROR_SUCCESS)
{
// Value not found on the path.
machine_key_found = false;
}
}
if (!machine_key_found)
{
// If there's no value found on the machine scope, try to get it from the user scope.
if (auto res = RegOpenKeyExW(POLICIES_SCOPE_USER, POLICIES_PATH.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
{
if (res == ERROR_FILE_NOT_FOUND)
{
return gpo_rule_configured_not_configured;
}
return gpo_rule_configured_unavailable;
}
auto res = RegQueryValueExW(key, registry_value_name.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &valueSize);
RegCloseKey(key);
if (res != ERROR_SUCCESS)
{
return gpo_rule_configured_not_configured;
}
}
switch (value)
{
case 0:
return gpo_rule_configured_disabled;
case 1:
return gpo_rule_configured_enabled;
default:
return gpo_rule_configured_wrong_value;
}
}
inline std::optional<std::wstring> getPolicyListValue(const std::wstring& registry_list_path, const std::wstring& registry_list_value_name)
{
// This function returns the value of an entry of a policy list. The user scope is only checked, if the list is not enabled for the machine to not mix the lists.
HKEY key{};
// Try to read from the machine list.
bool machine_list_found = false;
if (RegOpenKeyExW(POLICIES_SCOPE_MACHINE, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
{
machine_list_found = true;
RegCloseKey(key);
// If the path exists in the machine registry, we try to read the value.
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_MACHINE, registry_list_path, registry_list_value_name);
if (regValueData.has_value())
{
// Return the value from the machine list.
return *regValueData;
}
}
// If no list exists for machine, we try to read from the user list.
if (!machine_list_found)
{
if (RegOpenKeyExW(POLICIES_SCOPE_USER, registry_list_path.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS)
{
RegCloseKey(key);
// If the path exists in the user registry, we try to read the value.
auto regValueData = readRegistryStringValue(POLICIES_SCOPE_USER, registry_list_path, registry_list_value_name);
if (regValueData.has_value())
{
// Return the value from the user list.
return *regValueData;
}
}
}
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
return std::nullopt;
}
inline gpo_rule_configured_t getUtilityEnabledValue(const std::wstring& utility_name)
{
auto individual_value = getConfiguredValue(utility_name);
if (individual_value == gpo_rule_configured_disabled || individual_value == gpo_rule_configured_enabled)
{
return individual_value;
}
else
{
return getConfiguredValue(POLICY_CONFIGURE_ENABLED_GLOBAL_ALL_UTILITIES);
}
}
#pragma endregion ReadRegistryMethods
// Utility enabled state policies
// (Always use 'getUtilityEnabledValue()'.)

View File

@@ -1,83 +0,0 @@
#include "pch.h"
#include "modulesRegistry.h"
#include <common/utils/json.h>
#include <filesystem>
namespace fs = std::filesystem;
namespace NonLocalizable
{
const static wchar_t* MONACO_LANGUAGES_FILE_NAME = L"Assets\\Monaco\\monaco_languages.json";
const static wchar_t* ListID = L"list";
const static wchar_t* ExtensionsID = L"extensions";
}
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
{
using namespace registry::shellex;
// Set up a list of extensions for the preview handler to take over
std::vector<std::wstring> extensions;
// Set up a list of extensions that Monaco support but the preview handler shouldn't take over
std::vector<std::wstring> ExtExclusions;
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
bool IsExcluded = false;
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
auto json = json::from_file(languagesFilePath);
if (json)
{
try
{
auto list = json->GetNamedArray(NonLocalizable::ListID);
for (uint32_t i = 0; i < list.Size(); ++i)
{
auto entry = list.GetObjectAt(i);
if (entry.HasKey(NonLocalizable::ExtensionsID))
{
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
{
auto extension = extensionsList.GetStringAt(j);
// Ignore extensions in the exclusion list
IsExcluded = false;
for (std::wstring k : ExtExclusions)
{
if (std::wstring{ extension } == k)
{
IsExcluded = true;
break;
}
}
if (IsExcluded)
{
continue;
}
extensions.push_back(std::wstring{ extension });
}
}
}
}
catch (...)
{
}
}
return generatePreviewHandler(PreviewHandlerType::preview,
perUser,
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
get_std_product_version(),
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
L"MonacoPreviewHandler",
L"Monaco Preview Handler",
extensions);
}

View File

@@ -2,12 +2,17 @@
#include "registry.h"
#include <common/utils/json.h>
#include <filesystem>
namespace fs = std::filesystem;
namespace NonLocalizable
{
const static wchar_t* MONACO_LANGUAGES_FILE_NAME = L"Assets\\Monaco\\monaco_languages.json";
const static wchar_t* ListID = L"list";
const static wchar_t* ExtensionsID = L"extensions";
const static std::vector<std::wstring> ExtSVG = { L".svg" };
const static std::vector<std::wstring> ExtMarkdown = { L".md", L".markdown", L".mdown", L".mkdn", L".mkd", L".mdwn", L".mdtxt", L".mdtext" };
const static std::vector<std::wstring> ExtPDF = { L".pdf" };
@@ -48,8 +53,73 @@ inline registry::ChangeSet getMdPreviewHandlerChangeSet(const std::wstring insta
NonLocalizable::ExtMarkdown);
}
// Implementation in modulesRegistry.cpp
registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser);
inline registry::ChangeSet getMonacoPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
{
using namespace registry::shellex;
// Set up a list of extensions for the preview handler to take over
std::vector<std::wstring> extensions;
// Set up a list of extensions that Monaco support but the preview handler shouldn't take over
std::vector<std::wstring> ExtExclusions;
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtMarkdown.begin(), NonLocalizable::ExtMarkdown.end());
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtSVG.begin(), NonLocalizable::ExtSVG.end());
ExtExclusions.insert(ExtExclusions.end(), NonLocalizable::ExtNoNoNo.begin(), NonLocalizable::ExtNoNoNo.end());
bool IsExcluded = false;
std::wstring languagesFilePath = fs::path{ installationDir } / NonLocalizable::MONACO_LANGUAGES_FILE_NAME;
auto json = json::from_file(languagesFilePath);
if (json)
{
try
{
auto list = json->GetNamedArray(NonLocalizable::ListID);
for (uint32_t i = 0; i < list.Size(); ++i)
{
auto entry = list.GetObjectAt(i);
if (entry.HasKey(NonLocalizable::ExtensionsID))
{
auto extensionsList = entry.GetNamedArray(NonLocalizable::ExtensionsID);
for (uint32_t j = 0; j < extensionsList.Size(); ++j)
{
auto extension = extensionsList.GetStringAt(j);
// Ignore extensions in the exclusion list
IsExcluded = false;
for (std::wstring k : ExtExclusions)
{
if (std::wstring{ extension } == k)
{
IsExcluded = true;
break;
}
}
if (IsExcluded)
{
continue;
}
extensions.push_back(std::wstring{ extension });
}
}
}
}
catch (...)
{
}
}
return generatePreviewHandler(PreviewHandlerType::preview,
perUser,
L"{D8034CFA-F34B-41FE-AD45-62FCBB52A6DA}",
get_std_product_version(),
(fs::path{ installationDir } / LR"d(PowerToys.MonacoPreviewHandlerCpp.dll)d").wstring(),
L"MonacoPreviewHandler",
L"Monaco Preview Handler",
extensions);
}
inline registry::ChangeSet getPdfPreviewHandlerChangeSet(const std::wstring installationDir, const bool perUser)
{

View File

@@ -1,397 +0,0 @@
#include "pch.h"
#include "package.h"
#include <appxpackaging.h>
#include <wrl/client.h>
#include "../logger/logger.h"
namespace package
{
using Microsoft::WRL::ComPtr;
bool GetPackageNameAndVersionFromAppx(
const std::wstring& appxPath,
std::wstring& outName,
PACKAGE_VERSION& outVersion)
{
try
{
ComInitializer comInit;
if (!comInit.Succeeded())
{
Logger::error(L"COM initialization failed.");
return false;
}
ComPtr<IAppxFactory> factory;
ComPtr<IStream> stream;
ComPtr<IAppxPackageReader> reader;
ComPtr<IAppxManifestReader> manifest;
ComPtr<IAppxManifestPackageId> packageId;
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
if (FAILED(hr))
return false;
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
if (FAILED(hr))
return false;
hr = factory->CreatePackageReader(stream.Get(), &reader);
if (FAILED(hr))
return false;
hr = reader->GetManifest(&manifest);
if (FAILED(hr))
return false;
hr = manifest->GetPackageId(&packageId);
if (FAILED(hr))
return false;
LPWSTR name = nullptr;
hr = packageId->GetName(&name);
if (FAILED(hr))
return false;
UINT64 version = 0;
hr = packageId->GetVersion(&version);
if (FAILED(hr))
return false;
outName = std::wstring(name);
CoTaskMemFree(name);
outVersion.Major = static_cast<UINT16>((version >> 48) & 0xFFFF);
outVersion.Minor = static_cast<UINT16>((version >> 32) & 0xFFFF);
outVersion.Build = static_cast<UINT16>((version >> 16) & 0xFFFF);
outVersion.Revision = static_cast<UINT16>(version & 0xFFFF);
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
outName,
outVersion.Major,
outVersion.Minor,
outVersion.Build,
outVersion.Revision,
appxPath);
return true;
}
catch (const std::exception& ex)
{
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
return false;
}
catch (...)
{
Logger::error(L"Unknown or non-standard exception occurred.");
return false;
}
}
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
{
try
{
Uri externalUri{ externalLocation };
Uri packageUri{ sparsePkgPath };
PackageManager packageManager;
// Declare use of an external location
AddPackageOptions options;
options.ExternalLocationUri(externalUri);
options.ForceUpdateFromAnyVersion(true);
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Register {} package canceled.", sparsePkgPath);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Register {} package completed.", sparsePkgPath);
}
else
{
Logger::debug(L"Register {} package started.", sparsePkgPath);
}
return true;
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to register package: {}", e.what());
return false;
}
}
bool UnRegisterPackage(const std::wstring& pkgDisplayName)
{
try
{
PackageManager packageManager;
const static auto packages = packageManager.FindPackagesForUser({});
for (auto const& package : packages)
{
const auto& packageFullName = std::wstring{ package.Id().FullName() };
if (packageFullName.contains(pkgDisplayName))
{
auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) };
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText);
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Unregister {} package canceled.", packageFullName);
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Unregister {} package completed.", packageFullName);
}
else
{
Logger::debug(L"Unregister {} package started.", packageFullName);
}
break;
}
}
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
return false;
}
return true;
}
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
{
if (directoryPath.empty())
{
return {};
}
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
std::vector<std::wstring> matchedFiles;
try
{
if (recursive)
{
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
{
if (entry.is_regular_file())
{
const auto& fileName = entry.path().filename().string();
if (std::regex_match(fileName, pattern))
{
matchedFiles.push_back(entry.path());
}
}
}
}
else
{
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
{
if (entry.is_regular_file())
{
const auto& fileName = entry.path().filename().string();
if (std::regex_match(fileName, pattern))
{
matchedFiles.push_back(entry.path());
}
}
}
}
// Sort by package version in descending order (newest first)
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
std::wstring nameA, nameB;
PACKAGE_VERSION versionA{}, versionB{};
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
// Files that failed to parse go to the end
if (!gotA)
return false;
if (!gotB)
return true;
// Compare versions: Major, Minor, Build, Revision (descending)
if (versionA.Major != versionB.Major)
return versionA.Major > versionB.Major;
if (versionA.Minor != versionB.Minor)
return versionA.Minor > versionB.Minor;
if (versionA.Build != versionB.Build)
return versionA.Build > versionB.Build;
return versionA.Revision > versionB.Revision;
});
}
catch (const std::exception& ex)
{
Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what()));
}
return matchedFiles;
}
bool IsPackageSatisfied(const std::wstring& appxPath)
{
std::wstring targetName;
PACKAGE_VERSION targetVersion{};
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
{
Logger::error(L"Failed to get package name and version from appx: " + appxPath);
return false;
}
PackageManager pm;
for (const auto& package : pm.FindPackagesForUser({}))
{
const auto& id = package.Id();
if (std::wstring(id.Name()) == targetName)
{
const auto& version = id.Version();
if (version.Major > targetVersion.Major ||
(version.Major == targetVersion.Major && version.Minor > targetVersion.Minor) ||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build > targetVersion.Build) ||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build == targetVersion.Build && version.Revision >= targetVersion.Revision))
{
Logger::info(
L"Package {} is already satisfied with version {}.{}.{}.{}; target version {}.{}.{}.{}; appxPath: {}",
id.Name(),
version.Major,
version.Minor,
version.Build,
version.Revision,
targetVersion.Major,
targetVersion.Minor,
targetVersion.Build,
targetVersion.Revision,
appxPath);
return true;
}
}
}
Logger::info(
L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
targetName,
targetVersion.Major,
targetVersion.Minor,
targetVersion.Build,
targetVersion.Revision,
appxPath);
return false;
}
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
{
try
{
Uri packageUri{ pkgPath };
PackageManager packageManager;
// Declare use of an external location
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
if (!dependencies.empty())
{
for (const auto& dependency : dependencies)
{
try
{
if (IsPackageSatisfied(dependency))
{
Logger::info(L"Dependency already satisfied: {}", dependency);
}
else
{
uris.Append(Uri(dependency));
}
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
}
}
}
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageAsync(packageUri, uris, options);
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(errorCode), errorText);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Register {} package canceled.", pkgPath);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Register {} package completed.", pkgPath);
}
else
{
Logger::debug(L"Register {} package started.", pkgPath);
}
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to register package: {}", e.what());
return false;
}
return true;
}
}

View File

@@ -3,12 +3,14 @@
#include <Windows.h>
#include <algorithm>
#include <appxpackaging.h>
#include <exception>
#include <filesystem>
#include <regex>
#include <string>
#include <optional>
#include <vector>
#include <Shlwapi.h>
#include <wrl/client.h>
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Foundation.h>
@@ -29,6 +31,7 @@ namespace package
using winrt::Windows::Management::Deployment::DeploymentProgress;
using winrt::Windows::Management::Deployment::DeploymentResult;
using winrt::Windows::Management::Deployment::PackageManager;
using Microsoft::WRL::ComPtr;
inline BOOL IsWin11OrGreater()
{
@@ -87,11 +90,85 @@ namespace package
bool _initialized;
};
// Implementations in package.cpp
bool GetPackageNameAndVersionFromAppx(
inline bool GetPackageNameAndVersionFromAppx(
const std::wstring& appxPath,
std::wstring& outName,
PACKAGE_VERSION& outVersion);
PACKAGE_VERSION& outVersion)
{
try
{
ComInitializer comInit;
if (!comInit.Succeeded())
{
Logger::error(L"COM initialization failed.");
return false;
}
ComPtr<IAppxFactory> factory;
ComPtr<IStream> stream;
ComPtr<IAppxPackageReader> reader;
ComPtr<IAppxManifestReader> manifest;
ComPtr<IAppxManifestPackageId> packageId;
HRESULT hr = CoCreateInstance(__uuidof(AppxFactory), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory));
if (FAILED(hr))
return false;
hr = SHCreateStreamOnFileEx(appxPath.c_str(), STGM_READ | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &stream);
if (FAILED(hr))
return false;
hr = factory->CreatePackageReader(stream.Get(), &reader);
if (FAILED(hr))
return false;
hr = reader->GetManifest(&manifest);
if (FAILED(hr))
return false;
hr = manifest->GetPackageId(&packageId);
if (FAILED(hr))
return false;
LPWSTR name = nullptr;
hr = packageId->GetName(&name);
if (FAILED(hr))
return false;
UINT64 version = 0;
hr = packageId->GetVersion(&version);
if (FAILED(hr))
return false;
outName = std::wstring(name);
CoTaskMemFree(name);
outVersion.Major = static_cast<UINT16>((version >> 48) & 0xFFFF);
outVersion.Minor = static_cast<UINT16>((version >> 32) & 0xFFFF);
outVersion.Build = static_cast<UINT16>((version >> 16) & 0xFFFF);
outVersion.Revision = static_cast<UINT16>(version & 0xFFFF);
Logger::info(L"Package name: {}, version: {}.{}.{}.{}, appxPath: {}",
outName,
outVersion.Major,
outVersion.Minor,
outVersion.Build,
outVersion.Revision,
appxPath);
return true;
}
catch (const std::exception& ex)
{
Logger::error(L"Standard exception: {}", winrt::to_hstring(ex.what()));
return false;
}
catch (...)
{
Logger::error(L"Unknown or non-standard exception occurred.");
return false;
}
}
inline std::optional<Package> GetRegisteredPackage(std::wstring packageDisplayName, bool checkVersion)
{
@@ -120,10 +197,308 @@ namespace package
return GetRegisteredPackage(packageDisplayName, true).has_value();
}
// Implementations in package.cpp
bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath);
bool UnRegisterPackage(const std::wstring& pkgDisplayName);
std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive);
bool IsPackageSatisfied(const std::wstring& appxPath);
bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies);
inline bool RegisterSparsePackage(const std::wstring& externalLocation, const std::wstring& sparsePkgPath)
{
try
{
Uri externalUri{ externalLocation };
Uri packageUri{ sparsePkgPath };
PackageManager packageManager;
// Declare use of an external location
AddPackageOptions options;
options.ExternalLocationUri(externalUri);
options.ForceUpdateFromAnyVersion(true);
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageByUriAsync(packageUri, options);
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", sparsePkgPath, std::to_wstring(errorCode), errorText);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Register {} package canceled.", sparsePkgPath);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Register {} package completed.", sparsePkgPath);
}
else
{
Logger::debug(L"Register {} package started.", sparsePkgPath);
}
return true;
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to register package: {}", e.what());
return false;
}
}
inline bool UnRegisterPackage(const std::wstring& pkgDisplayName)
{
try
{
PackageManager packageManager;
const static auto packages = packageManager.FindPackagesForUser({});
for (auto const& package : packages)
{
const auto& packageFullName = std::wstring{ package.Id().FullName() };
if (packageFullName.contains(pkgDisplayName))
{
auto deploymentOperation{ packageManager.RemovePackageAsync(packageFullName) };
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Unregister {} package failed. ErrorCode: {}, ErrorText: {}", packageFullName, std::to_wstring(errorCode), errorText);
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Unregister {} package canceled.", packageFullName);
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Unregister {} package completed.", packageFullName);
}
else
{
Logger::debug(L"Unregister {} package started.", packageFullName);
}
break;
}
}
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to unregister package: {}", e.what());
return false;
}
return true;
}
inline std::vector<std::wstring> FindMsixFile(const std::wstring& directoryPath, bool recursive)
{
if (directoryPath.empty())
{
return {};
}
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
std::vector<std::wstring> matchedFiles;
try
{
if (recursive)
{
for (const auto& entry : std::filesystem::recursive_directory_iterator(directoryPath))
{
if (entry.is_regular_file())
{
const auto& fileName = entry.path().filename().string();
if (std::regex_match(fileName, pattern))
{
matchedFiles.push_back(entry.path());
}
}
}
}
else
{
for (const auto& entry : std::filesystem::directory_iterator(directoryPath))
{
if (entry.is_regular_file())
{
const auto& fileName = entry.path().filename().string();
if (std::regex_match(fileName, pattern))
{
matchedFiles.push_back(entry.path());
}
}
}
}
// Sort by package version in descending order (newest first)
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
std::wstring nameA, nameB;
PACKAGE_VERSION versionA{}, versionB{};
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
// Files that failed to parse go to the end
if (!gotA)
return false;
if (!gotB)
return true;
// Compare versions: Major, Minor, Build, Revision (descending)
if (versionA.Major != versionB.Major)
return versionA.Major > versionB.Major;
if (versionA.Minor != versionB.Minor)
return versionA.Minor > versionB.Minor;
if (versionA.Build != versionB.Build)
return versionA.Build > versionB.Build;
return versionA.Revision > versionB.Revision;
});
}
catch (const std::exception& ex)
{
Logger::error("An error occurred while searching for MSIX files: " + std::string(ex.what()));
}
return matchedFiles;
}
inline bool IsPackageSatisfied(const std::wstring& appxPath)
{
std::wstring targetName;
PACKAGE_VERSION targetVersion{};
if (!GetPackageNameAndVersionFromAppx(appxPath, targetName, targetVersion))
{
Logger::error(L"Failed to get package name and version from appx: " + appxPath);
return false;
}
PackageManager pm;
for (const auto& package : pm.FindPackagesForUser({}))
{
const auto& id = package.Id();
if (std::wstring(id.Name()) == targetName)
{
const auto& version = id.Version();
if (version.Major > targetVersion.Major ||
(version.Major == targetVersion.Major && version.Minor > targetVersion.Minor) ||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build > targetVersion.Build) ||
(version.Major == targetVersion.Major && version.Minor == targetVersion.Minor && version.Build == targetVersion.Build && version.Revision >= targetVersion.Revision))
{
Logger::info(
L"Package {} is already satisfied with version {}.{}.{}.{}; target version {}.{}.{}.{}; appxPath: {}",
id.Name(),
version.Major,
version.Minor,
version.Build,
version.Revision,
targetVersion.Major,
targetVersion.Minor,
targetVersion.Build,
targetVersion.Revision,
appxPath);
return true;
}
}
}
Logger::info(
L"Package {} is not satisfied. Target version: {}.{}.{}.{}; appxPath: {}",
targetName,
targetVersion.Major,
targetVersion.Minor,
targetVersion.Build,
targetVersion.Revision,
appxPath);
return false;
}
inline bool RegisterPackage(std::wstring pkgPath, std::vector<std::wstring> dependencies)
{
try
{
Uri packageUri{ pkgPath };
PackageManager packageManager;
// Declare use of an external location
DeploymentOptions options = DeploymentOptions::ForceTargetApplicationShutdown;
IVector<Uri> uris = winrt::single_threaded_vector<Uri>();
if (!dependencies.empty())
{
for (const auto& dependency : dependencies)
{
try
{
if (IsPackageSatisfied(dependency))
{
Logger::info(L"Dependency already satisfied: {}", dependency);
}
else
{
uris.Append(Uri(dependency));
}
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"Error creating Uri for dependency: %s", ex.message().c_str());
}
}
}
IAsyncOperationWithProgress<DeploymentResult, DeploymentProgress> deploymentOperation = packageManager.AddPackageAsync(packageUri, uris, options);
deploymentOperation.get();
// Check the status of the operation
if (deploymentOperation.Status() == AsyncStatus::Error)
{
auto deploymentResult{ deploymentOperation.GetResults() };
auto errorCode = deploymentOperation.ErrorCode();
auto errorText = deploymentResult.ErrorText();
Logger::error(L"Register {} package failed. ErrorCode: {}, ErrorText: {}", pkgPath, std::to_wstring(errorCode), errorText);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Canceled)
{
Logger::error(L"Register {} package canceled.", pkgPath);
return false;
}
else if (deploymentOperation.Status() == AsyncStatus::Completed)
{
Logger::info(L"Register {} package completed.", pkgPath);
}
else
{
Logger::debug(L"Register {} package started.", pkgPath);
}
}
catch (std::exception& e)
{
Logger::error("Exception thrown while trying to register package: {}", e.what());
return false;
}
return true;
}
}

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <Shlwapi.h>
#include <sddl.h>
#include <DbgHelp.h>
#include <Msi.h>
#include <pathcch.h>
#include <atlbase.h>
#include <atlstr.h>
#include <algorithm>
#include <cassert>
#include <exception>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
#include <variant>
#include <vector>
#pragma warning(push)
#pragma warning(disable : 26471 26492 26493 26497)
#include <wil/resource.h>
#pragma warning(pop)
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -1,223 +0,0 @@
#include "pch.h"
#include "registry.h"
#include "../logger/logger.h"
namespace registry
{
namespace install_scope
{
bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey)
{
HKEY uninstallKey{};
if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS)
{
return false;
}
detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } };
DWORD index = 0;
wchar_t subKeyName[256];
// Enumerate all subkeys under Uninstall
while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS)
{
HKEY productKey{};
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS)
{
continue;
}
detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } };
// Check BundleUpgradeCode value (specific to WiX Bundle installations)
wchar_t bundleUpgradeCode[256]{};
DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode);
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr,
reinterpret_cast<LPBYTE>(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS)
{
if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0)
{
return true;
}
}
}
return false;
}
const InstallScope get_current_install_scope()
{
// 1. Check HKCU Uninstall registry first (user-level bundle)
// Note: MSI components are always in HKLM regardless of install scope,
// but the Bundle entry will be in HKCU for per-user installations
if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER))
{
Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU");
return InstallScope::PerUser;
}
// 2. Check HKLM Uninstall registry (machine-level bundle)
if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE))
{
Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM");
return InstallScope::PerMachine;
}
// 3. Fallback to legacy custom registry key detection
Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection");
// Open HKLM key
HKEY perMachineKey{};
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
INSTALL_SCOPE_REG_KEY,
0,
KEY_READ,
&perMachineKey) != ERROR_SUCCESS)
{
// Open HKCU key
HKEY perUserKey{};
if (RegOpenKeyExW(HKEY_CURRENT_USER,
INSTALL_SCOPE_REG_KEY,
0,
KEY_READ,
&perUserKey) != ERROR_SUCCESS)
{
// both keys are missing
Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine");
return InstallScope::PerMachine;
}
else
{
DWORD dataSize{};
if (RegGetValueW(
perUserKey,
nullptr,
L"InstallScope",
RRF_RT_REG_SZ,
nullptr,
nullptr,
&dataSize) != ERROR_SUCCESS)
{
// HKCU key is missing
RegCloseKey(perUserKey);
return InstallScope::PerMachine;
}
std::wstring data;
data.resize(dataSize / sizeof(wchar_t));
if (RegGetValueW(
perUserKey,
nullptr,
L"InstallScope",
RRF_RT_REG_SZ,
nullptr,
&data[0],
&dataSize) != ERROR_SUCCESS)
{
// HKCU key is missing
RegCloseKey(perUserKey);
return InstallScope::PerMachine;
}
RegCloseKey(perUserKey);
if (data.contains(L"perUser"))
{
return InstallScope::PerUser;
}
}
}
return InstallScope::PerMachine;
}
}
namespace shellex
{
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
const bool perUser,
std::wstring handlerClsid,
std::wstring powertoysVersion,
std::wstring fullPathToHandler,
std::wstring className,
std::wstring displayName,
std::vector<std::wstring> fileTypes,
std::wstring perceivedType,
std::wstring fileKindType)
{
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
std::wstring clsidPath = L"Software\\Classes\\CLSID";
clsidPath += L'\\';
clsidPath += handlerClsid;
std::wstring inprocServerPath = clsidPath;
inprocServerPath += L'\\';
inprocServerPath += L"InprocServer32";
std::wstring assemblyKeyValue;
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
{
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
}
else
{
assemblyKeyValue = L"PowerToys." + className;
}
assemblyKeyValue += L", Version=";
assemblyKeyValue += powertoysVersion;
assemblyKeyValue += L", Culture=neutral";
std::wstring versionPath = inprocServerPath;
versionPath += L'\\';
versionPath += powertoysVersion;
using vec_t = std::vector<registry::ValueChange>;
// TODO: verify that we actually need all of those
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
{ scope, clsidPath, std::nullopt, className },
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
{ scope, inprocServerPath, L"Class", className },
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
for (const auto& fileType : fileTypes)
{
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
if (!fileKindType.empty())
{
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
}
if (!perceivedType.empty())
{
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
}
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
{
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
}
}
if (handlerType == PreviewHandlerType::preview)
{
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
}
return registry::ChangeSet{ .changes = std::move(changes) };
}
}
}

View File

@@ -72,9 +72,130 @@ namespace registry
};
// Helper function to find PowerToys bundle in Windows Uninstall registry by BundleUpgradeCode
// Implementation in registry.cpp
bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey);
const InstallScope get_current_install_scope();
inline bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey)
{
HKEY uninstallKey{};
if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS)
{
return false;
}
detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } };
DWORD index = 0;
wchar_t subKeyName[256];
// Enumerate all subkeys under Uninstall
while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS)
{
HKEY productKey{};
if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS)
{
continue;
}
detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } };
// Check BundleUpgradeCode value (specific to WiX Bundle installations)
wchar_t bundleUpgradeCode[256]{};
DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode);
if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr,
reinterpret_cast<LPBYTE>(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS)
{
if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0)
{
return true;
}
}
}
return false;
}
inline const InstallScope get_current_install_scope()
{
// 1. Check HKCU Uninstall registry first (user-level bundle)
// Note: MSI components are always in HKLM regardless of install scope,
// but the Bundle entry will be in HKCU for per-user installations
if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER))
{
Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU");
return InstallScope::PerUser;
}
// 2. Check HKLM Uninstall registry (machine-level bundle)
if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE))
{
Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM");
return InstallScope::PerMachine;
}
// 3. Fallback to legacy custom registry key detection
Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection");
// Open HKLM key
HKEY perMachineKey{};
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
INSTALL_SCOPE_REG_KEY,
0,
KEY_READ,
&perMachineKey) != ERROR_SUCCESS)
{
// Open HKCU key
HKEY perUserKey{};
if (RegOpenKeyExW(HKEY_CURRENT_USER,
INSTALL_SCOPE_REG_KEY,
0,
KEY_READ,
&perUserKey) != ERROR_SUCCESS)
{
// both keys are missing
Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine");
return InstallScope::PerMachine;
}
else
{
DWORD dataSize{};
if (RegGetValueW(
perUserKey,
nullptr,
L"InstallScope",
RRF_RT_REG_SZ,
nullptr,
nullptr,
&dataSize) != ERROR_SUCCESS)
{
// HKCU key is missing
RegCloseKey(perUserKey);
return InstallScope::PerMachine;
}
std::wstring data;
data.resize(dataSize / sizeof(wchar_t));
if (RegGetValueW(
perUserKey,
nullptr,
L"InstallScope",
RRF_RT_REG_SZ,
nullptr,
&data[0],
&dataSize) != ERROR_SUCCESS)
{
// HKCU key is missing
RegCloseKey(perUserKey);
return InstallScope::PerMachine;
}
RegCloseKey(perUserKey);
if (data.contains(L"perUser"))
{
return InstallScope::PerUser;
}
}
}
return InstallScope::PerMachine;
}
}
template<class>
@@ -330,8 +451,7 @@ namespace registry
thumbnail
};
// Implementation in registry.cpp
registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
inline registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
const bool perUser,
std::wstring handlerClsid,
std::wstring powertoysVersion,
@@ -340,6 +460,80 @@ namespace registry
std::wstring displayName,
std::vector<std::wstring> fileTypes,
std::wstring perceivedType = L"",
std::wstring fileKindType = L"");
std::wstring fileKindType = L"")
{
const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
std::wstring clsidPath = L"Software\\Classes\\CLSID";
clsidPath += L'\\';
clsidPath += handlerClsid;
std::wstring inprocServerPath = clsidPath;
inprocServerPath += L'\\';
inprocServerPath += L"InprocServer32";
std::wstring assemblyKeyValue;
if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
{
assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
}
else
{
assemblyKeyValue = L"PowerToys." + className;
}
assemblyKeyValue += L", Version=";
assemblyKeyValue += powertoysVersion;
assemblyKeyValue += L", Culture=neutral";
std::wstring versionPath = inprocServerPath;
versionPath += L'\\';
versionPath += powertoysVersion;
using vec_t = std::vector<registry::ValueChange>;
// TODO: verify that we actually need all of those
vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
{ scope, clsidPath, std::nullopt, className },
{ scope, inprocServerPath, std::nullopt, fullPathToHandler },
{ scope, inprocServerPath, L"Assembly", assemblyKeyValue },
{ scope, inprocServerPath, L"Class", className },
{ scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
for (const auto& fileType : fileTypes)
{
std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
if (!fileKindType.empty())
{
// Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
// Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
}
if (!perceivedType.empty())
{
changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
}
if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
{
// this regfile registry key has precedence over Software\Classes\.reg for .reg files
std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
}
}
if (handlerType == PreviewHandlerType::preview)
{
const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
}
return registry::ChangeSet{ .changes = std::move(changes) };
}
}
}

View File

@@ -1,202 +0,0 @@
#include "pch.h"
#include "resources.h"
#include <atlstr.h>
#include <common/utils/language_helper.h>
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
{
// Try to load en-us string as the first fallback.
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
ATL::CStringW english_string;
try
{
if (!english_string.LoadStringW(instance, resource_id, english_language))
{
return {};
}
}
catch (...)
{
return {};
}
return std::wstring(english_string);
}
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
{
static std::wstring language = LanguageHelpers::load_language();
unsigned lang = LANG_ENGLISH;
unsigned sublang = SUBLANG_ENGLISH_US;
if (!language.empty())
{
// Language list taken from Resources.wxs
if (language == L"ar-SA")
{
lang = LANG_ARABIC;
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
}
else if (language == L"cs-CZ")
{
lang = LANG_CZECH;
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
}
else if (language == L"de-DE")
{
lang = LANG_GERMAN;
sublang = SUBLANG_GERMAN;
}
else if (language == L"en-US")
{
lang = LANG_ENGLISH;
sublang = SUBLANG_ENGLISH_US;
}
else if (language == L"es-ES")
{
lang = LANG_SPANISH;
sublang = SUBLANG_SPANISH;
}
else if (language == L"fa-IR")
{
lang = LANG_PERSIAN;
sublang = SUBLANG_PERSIAN_IRAN;
}
else if (language == L"fr-FR")
{
lang = LANG_FRENCH;
sublang = SUBLANG_FRENCH;
}
else if (language == L"he-IL")
{
lang = LANG_HEBREW;
sublang = SUBLANG_HEBREW_ISRAEL;
}
else if (language == L"hu-HU")
{
lang = LANG_HUNGARIAN;
sublang = SUBLANG_HUNGARIAN_HUNGARY;
}
else if (language == L"it-IT")
{
lang = LANG_ITALIAN;
sublang = SUBLANG_ITALIAN;
}
else if (language == L"ja-JP")
{
lang = LANG_JAPANESE;
sublang = SUBLANG_JAPANESE_JAPAN;
}
else if (language == L"ko-KR")
{
lang = LANG_KOREAN;
sublang = SUBLANG_KOREAN;
}
else if (language == L"nl-NL")
{
lang = LANG_DUTCH;
sublang = SUBLANG_DUTCH;
}
else if (language == L"pl-PL")
{
lang = LANG_POLISH;
sublang = SUBLANG_POLISH_POLAND;
}
else if (language == L"pt-BR")
{
lang = LANG_PORTUGUESE;
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
}
else if (language == L"pt-PT")
{
lang = LANG_PORTUGUESE;
sublang = SUBLANG_PORTUGUESE;
}
else if (language == L"ru-RU")
{
lang = LANG_RUSSIAN;
sublang = SUBLANG_RUSSIAN_RUSSIA;
}
else if (language == L"sv-SE")
{
lang = LANG_SWEDISH;
sublang = SUBLANG_SWEDISH;
}
else if (language == L"tr-TR")
{
lang = LANG_TURKISH;
sublang = SUBLANG_TURKISH_TURKEY;
}
else if (language == L"uk-UA")
{
lang = LANG_UKRAINIAN;
sublang = SUBLANG_UKRAINIAN_UKRAINE;
}
else if (language == L"zh-CN")
{
lang = LANG_CHINESE_SIMPLIFIED;
sublang = SUBLANG_CHINESE_SIMPLIFIED;
}
else if (language == L"zh-TW")
{
lang = LANG_CHINESE_TRADITIONAL;
sublang = SUBLANG_CHINESE_TRADITIONAL;
}
WORD languageID = MAKELANGID(lang, sublang);
ATL::CStringW result;
try
{
if (!result.LoadStringW(instance, resource_id, languageID))
{
return {};
}
}
catch (...)
{
return {};
}
if (!result.IsEmpty())
{
return std::wstring(result);
}
}
return {};
}
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
{
// Try to load en-us string as the first fallback.
std::wstring english_string = get_english_fallback_string(resource_id, instance);
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
if (!language_override_resource.empty())
{
return language_override_resource;
}
else
{
wchar_t* text_ptr;
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
if (length == 0)
{
if (!english_string.empty())
{
return std::wstring(english_string);
}
else
{
return fallback;
}
}
else
{
return { text_ptr, static_cast<std::size_t>(length) };
}
}
}

View File

@@ -2,11 +2,208 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string>
#include <atlstr.h>
// Implementations in resources.cpp
std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance);
std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance);
std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback);
#include <common/utils/language_helper.h>
inline std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
{
// Try to load en-us string as the first fallback.
WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
ATL::CStringW english_string;
try
{
if (!english_string.LoadStringW(instance, resource_id, english_language))
{
return {};
}
}
catch (...)
{
return {};
}
return std::wstring(english_string);
}
inline std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance)
{
static std::wstring language = LanguageHelpers::load_language();
unsigned lang = LANG_ENGLISH;
unsigned sublang = SUBLANG_ENGLISH_US;
if (!language.empty())
{
// Language list taken from Resources.wxs
if (language == L"ar-SA")
{
lang = LANG_ARABIC;
sublang = SUBLANG_ARABIC_SAUDI_ARABIA;
}
else if (language == L"cs-CZ")
{
lang = LANG_CZECH;
sublang = SUBLANG_CZECH_CZECH_REPUBLIC;
}
else if (language == L"de-DE")
{
lang = LANG_GERMAN;
sublang = SUBLANG_GERMAN;
}
else if (language == L"en-US")
{
lang = LANG_ENGLISH;
sublang = SUBLANG_ENGLISH_US;
}
else if (language == L"es-ES")
{
lang = LANG_SPANISH;
sublang = SUBLANG_SPANISH;
}
else if (language == L"fa-IR")
{
lang = LANG_PERSIAN;
sublang = SUBLANG_PERSIAN_IRAN;
}
else if (language == L"fr-FR")
{
lang = LANG_FRENCH;
sublang = SUBLANG_FRENCH;
}
else if (language == L"he-IL")
{
lang = LANG_HEBREW;
sublang = SUBLANG_HEBREW_ISRAEL;
}
else if (language == L"hu-HU")
{
lang = LANG_HUNGARIAN;
sublang = SUBLANG_HUNGARIAN_HUNGARY;
}
else if (language == L"it-IT")
{
lang = LANG_ITALIAN;
sublang = SUBLANG_ITALIAN;
}
else if (language == L"ja-JP")
{
lang = LANG_JAPANESE;
sublang = SUBLANG_JAPANESE_JAPAN;
}
else if (language == L"ko-KR")
{
lang = LANG_KOREAN;
sublang = SUBLANG_KOREAN;
}
else if (language == L"nl-NL")
{
lang = LANG_DUTCH;
sublang = SUBLANG_DUTCH;
}
else if (language == L"pl-PL")
{
lang = LANG_POLISH;
sublang = SUBLANG_POLISH_POLAND;
}
else if (language == L"pt-BR")
{
lang = LANG_PORTUGUESE;
sublang = SUBLANG_PORTUGUESE_BRAZILIAN;
}
else if (language == L"pt-PT")
{
lang = LANG_PORTUGUESE;
sublang = SUBLANG_PORTUGUESE;
}
else if (language == L"ru-RU")
{
lang = LANG_RUSSIAN;
sublang = SUBLANG_RUSSIAN_RUSSIA;
}
else if (language == L"sv-SE")
{
lang = LANG_SWEDISH;
sublang = SUBLANG_SWEDISH;
}
else if (language == L"tr-TR")
{
lang = LANG_TURKISH;
sublang = SUBLANG_TURKISH_TURKEY;
}
else if (language == L"uk-UA")
{
lang = LANG_UKRAINIAN;
sublang = SUBLANG_UKRAINIAN_UKRAINE;
}
else if (language == L"zh-CN")
{
lang = LANG_CHINESE_SIMPLIFIED;
sublang = SUBLANG_CHINESE_SIMPLIFIED;
}
else if (language == L"zh-TW")
{
lang = LANG_CHINESE_TRADITIONAL;
sublang = SUBLANG_CHINESE_TRADITIONAL;
}
WORD languageID = MAKELANGID(lang, sublang);
ATL::CStringW result;
try
{
if (!result.LoadStringW(instance, resource_id, languageID))
{
return {};
}
}
catch (...)
{
return {};
}
if (!result.IsEmpty())
{
return std::wstring(result);
}
}
return {};
}
// Get a string from the resource file
inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
{
// Try to load en-us string as the first fallback.
std::wstring english_string = get_english_fallback_string(resource_id, instance);
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
if (!language_override_resource.empty())
{
return language_override_resource;
}
else
{
wchar_t* text_ptr;
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
if (length == 0)
{
if (!english_string.empty())
{
return std::wstring(english_string);
}
else
{
return fallback;
}
}
else
{
return { text_ptr, static_cast<std::size_t>(length) };
}
}
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
// Wrapper for getting a string from the resource file. Returns the resource id text when fails.

View File

@@ -1,9 +1,24 @@
#include "pch.h"
#include "AudioSampleGenerator.h"
#include "CaptureFrameWait.h"
#include "LoopbackCapture.h"
#include <wrl/client.h>
extern TCHAR g_MicrophoneDeviceId[];
namespace
{
// Declare the IMemoryBufferByteAccess interface for accessing raw buffer data
MIDL_INTERFACE("5b0d3235-4dba-4d44-8657-1f1d0f83e9a3")
IMemoryBufferByteAccess : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetBuffer(
BYTE** value,
UINT32* capacity) = 0;
};
}
namespace winrt
{
using namespace Windows::Foundation;
@@ -19,17 +34,23 @@ namespace winrt
using namespace Windows::Devices::Enumeration;
}
AudioSampleGenerator::AudioSampleGenerator()
AudioSampleGenerator::AudioSampleGenerator(bool captureMicrophone, bool captureSystemAudio)
: m_captureMicrophone(captureMicrophone)
, m_captureSystemAudio(captureSystemAudio)
{
OutputDebugStringA(("AudioSampleGenerator created, captureMicrophone=" +
std::string(captureMicrophone ? "true" : "false") +
", captureSystemAudio=" + std::string(captureSystemAudio ? "true" : "false") + "\n").c_str());
m_audioEvent.create(wil::EventOptions::ManualReset);
m_endEvent.create(wil::EventOptions::ManualReset);
m_startEvent.create(wil::EventOptions::ManualReset);
m_asyncInitialized.create(wil::EventOptions::ManualReset);
}
AudioSampleGenerator::~AudioSampleGenerator()
{
Stop();
if (m_started.load())
if (m_audioGraph)
{
m_audioGraph.Close();
}
@@ -40,6 +61,10 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
auto expected = false;
if (m_initialized.compare_exchange_strong(expected, true))
{
// Reset state in case this instance is reused.
m_endEvent.ResetEvent();
m_startEvent.ResetEvent();
// Initialize the audio graph
auto audioGraphSettings = winrt::AudioGraphSettings(winrt::AudioRenderCategory::Media);
auto audioGraphResult = co_await winrt::AudioGraph::CreateAsync(audioGraphSettings);
@@ -49,28 +74,88 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
}
m_audioGraph = audioGraphResult.Graph();
// Initialize the selected microphone
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
auto microphoneId = (g_MicrophoneDeviceId[0] == 0) ? defaultMicrophoneId : winrt::to_hstring(g_MicrophoneDeviceId);
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
// Get AudioGraph encoding properties for resampling
auto graphProps = m_audioGraph.EncodingProperties();
m_graphSampleRate = graphProps.SampleRate();
m_graphChannels = graphProps.ChannelCount();
// Initialize audio input and output nodes
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
{
// If the selected microphone failed, try again with the default
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
}
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success)
{
throw winrt::hresult_error(E_FAIL, L"Failed to initialize input audio node!");
}
m_audioInputNode = inputNodeResult.DeviceInputNode();
OutputDebugStringA(("AudioGraph initialized: " + std::to_string(m_graphSampleRate) +
" Hz, " + std::to_string(m_graphChannels) + " ch\n").c_str());
// Create submix node to mix microphone and loopback audio
m_submixNode = m_audioGraph.CreateSubmixNode();
m_audioOutputNode = m_audioGraph.CreateFrameOutputNode();
m_submixNode.AddOutgoingConnection(m_audioOutputNode);
// Initialize WASAPI loopback capture for system audio (if enabled)
if (m_captureSystemAudio)
{
m_loopbackCapture = std::make_unique<LoopbackCapture>();
}
if (m_loopbackCapture && SUCCEEDED(m_loopbackCapture->Initialize()))
{
auto loopbackFormat = m_loopbackCapture->GetFormat();
if (loopbackFormat)
{
m_loopbackChannels = loopbackFormat->nChannels;
m_loopbackSampleRate = loopbackFormat->nSamplesPerSec;
m_resampleRatio = static_cast<double>(m_loopbackSampleRate) / static_cast<double>(m_graphSampleRate);
OutputDebugStringA(("Loopback initialized: " + std::to_string(m_loopbackSampleRate) +
" Hz, " + std::to_string(m_loopbackChannels) + " ch, resample ratio=" +
std::to_string(m_resampleRatio) + "\n").c_str());
}
}
else if (m_captureSystemAudio)
{
OutputDebugStringA("WARNING: Failed to initialize loopback capture\n");
m_loopbackCapture.reset();
}
// Always initialize a microphone input node to keep the AudioGraph running at real-time pace.
// When mic capture is disabled, we mute it so only loopback audio is captured.
{
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
auto microphoneId = (m_captureMicrophone && g_MicrophoneDeviceId[0] != 0)
? winrt::to_hstring(g_MicrophoneDeviceId)
: defaultMicrophoneId;
if (!microphoneId.empty())
{
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
// Initialize audio input node
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
{
// If the selected microphone failed, try again with the default
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
}
if (inputNodeResult.Status() == winrt::AudioDeviceNodeCreationStatus::Success)
{
m_audioInputNode = inputNodeResult.DeviceInputNode();
m_audioInputNode.AddOutgoingConnection(m_submixNode);
// If mic capture is disabled, mute the input so only loopback is captured
if (!m_captureMicrophone)
{
m_audioInputNode.OutgoingGain(0.0);
OutputDebugStringA("Mic input created but muted (loopback-only mode)\n");
}
else
{
OutputDebugStringA("Mic input created and active\n");
}
}
}
}
// Loopback capture is only required when system audio capture is enabled
if (m_captureSystemAudio && !m_loopbackCapture)
{
throw winrt::hresult_error(E_FAIL, L"Failed to initialize loopback audio capture!");
}
// Hookup audio nodes
m_audioInputNode.AddOutgoingConnection(m_audioOutputNode);
m_audioGraph.QuantumStarted({ this, &AudioSampleGenerator::OnAudioQuantumStarted });
m_asyncInitialized.SetEvent();
@@ -86,7 +171,37 @@ winrt::AudioEncodingProperties AudioSampleGenerator::GetEncodingProperties()
std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
{
CheckInitialized();
CheckStarted();
// The MediaStreamSource can request audio samples before we've started the audio graph.
// Instead of throwing (which crashes the app), wait until either Start() is called
// or Stop() signals end-of-stream.
if (!m_started.load())
{
std::vector<HANDLE> events = { m_endEvent.get(), m_startEvent.get() };
auto waitResult = WaitForMultipleObjectsEx(static_cast<DWORD>(events.size()), events.data(), false, INFINITE, false);
auto eventIndex = -1;
switch (waitResult)
{
case WAIT_OBJECT_0:
case WAIT_OBJECT_0 + 1:
eventIndex = waitResult - WAIT_OBJECT_0;
break;
}
WINRT_VERIFY(eventIndex >= 0);
if (events[eventIndex] == m_endEvent.get())
{
// End event signaled, but check if there are any remaining samples in the queue
auto lock = m_lock.lock_exclusive();
if (!m_samples.empty())
{
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
}
return std::nullopt;
}
}
{
auto lock = m_lock.lock_exclusive();
@@ -118,11 +233,25 @@ std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
auto signaledEvent = events[eventIndex];
if (signaledEvent == m_endEvent.get())
{
// End was signaled, but check for any remaining samples before returning nullopt
auto lock = m_lock.lock_exclusive();
if (!m_samples.empty())
{
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
}
return std::nullopt;
}
else
{
auto lock = m_lock.lock_exclusive();
if (m_samples.empty())
{
// Spurious wake or race - no samples available
// If end is signaled, return nullopt
return m_endEvent.is_signaled() ? std::nullopt : std::optional<winrt::MediaStreamSample>{};
}
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
@@ -135,23 +264,349 @@ void AudioSampleGenerator::Start()
auto expected = false;
if (m_started.compare_exchange_strong(expected, true))
{
m_endEvent.ResetEvent();
m_startEvent.SetEvent();
// Start loopback capture if available
if (m_loopbackCapture)
{
// Clear any stale samples
{
auto lock = m_loopbackBufferLock.lock_exclusive();
m_loopbackBuffer.clear();
}
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
m_loopbackCapture->Start();
}
m_audioGraph.Start();
}
}
void AudioSampleGenerator::Stop()
{
CheckInitialized();
if (m_started.load())
// Stop may be called during teardown even if initialization hasn't completed.
// It must never throw.
if (!m_initialized.load())
{
m_asyncInitialized.wait();
m_audioGraph.Stop();
m_endEvent.SetEvent();
return;
}
m_asyncInitialized.wait();
// Stop loopback capture first
if (m_loopbackCapture)
{
m_loopbackCapture->Stop();
}
// Flush any remaining samples from the loopback capture before stopping the audio graph
FlushRemainingAudio();
// Stop the audio graph - no more quantum callbacks will run
m_audioGraph.Stop();
// Mark as stopped
m_started.store(false);
// Combine all remaining queued samples into one final sample so it can be
// returned immediately without waiting for additional TryGetNextSample calls
CombineQueuedSamples();
// NOW signal end event - this allows TryGetNextSample to return remaining
// queued samples and then return nullopt
m_endEvent.SetEvent();
m_audioEvent.SetEvent(); // Also wake any waiting TryGetNextSample
// DO NOT clear m_loopbackBuffer or m_samples here - allow MediaTranscoder to
// consume remaining queued audio samples to avoid audio cutoff at end of recording.
// TryGetNextSample() will return nullopt once m_samples is empty and
// m_endEvent is signaled. Buffers will be cleaned up on destruction.
}
void AudioSampleGenerator::AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining)
{
if (rawLoopbackSamples.empty())
{
return;
}
m_resampleInputBuffer.insert(m_resampleInputBuffer.end(), rawLoopbackSamples.begin(), rawLoopbackSamples.end());
if (m_loopbackChannels == 0 || m_graphChannels == 0 || m_resampleRatio <= 0.0)
{
return;
}
std::vector<float> resampledSamples;
while (true)
{
const uint32_t inputFrames = static_cast<uint32_t>(m_resampleInputBuffer.size() / m_loopbackChannels);
if (inputFrames == 0)
{
break;
}
if (!flushRemaining)
{
if (inputFrames < 2 || (m_resampleInputPos + 1.0) >= inputFrames)
{
break;
}
}
else
{
if (m_resampleInputPos >= inputFrames)
{
break;
}
}
uint32_t inputFrame = static_cast<uint32_t>(m_resampleInputPos);
double frac = m_resampleInputPos - inputFrame;
uint32_t nextFrame = (inputFrame + 1 < inputFrames) ? (inputFrame + 1) : inputFrame;
for (uint32_t outCh = 0; outCh < m_graphChannels; outCh++)
{
float sample = 0.0f;
if (m_loopbackChannels == m_graphChannels)
{
uint32_t idx1 = inputFrame * m_loopbackChannels + outCh;
uint32_t idx2 = nextFrame * m_loopbackChannels + outCh;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
else if (m_loopbackChannels > m_graphChannels)
{
float sum = 0.0f;
for (uint32_t inCh = 0; inCh < m_loopbackChannels; inCh++)
{
uint32_t idx1 = inputFrame * m_loopbackChannels + inCh;
uint32_t idx2 = nextFrame * m_loopbackChannels + inCh;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sum += static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
sample = sum / m_loopbackChannels;
}
else
{
uint32_t idx1 = inputFrame * m_loopbackChannels;
uint32_t idx2 = nextFrame * m_loopbackChannels;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
resampledSamples.push_back(sample);
}
m_resampleInputPos += m_resampleRatio;
}
uint32_t consumedFrames = static_cast<uint32_t>(m_resampleInputPos);
if (consumedFrames > 0)
{
size_t samplesToErase = static_cast<size_t>(consumedFrames) * m_loopbackChannels;
if (samplesToErase >= m_resampleInputBuffer.size())
{
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
}
else
{
m_resampleInputBuffer.erase(m_resampleInputBuffer.begin(), m_resampleInputBuffer.begin() + samplesToErase);
m_resampleInputPos -= consumedFrames;
}
}
if (flushRemaining)
{
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
}
if (!resampledSamples.empty())
{
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
const size_t maxBufferSize = static_cast<size_t>(m_graphSampleRate) * m_graphChannels;
if (m_loopbackBuffer.size() + resampledSamples.size() > maxBufferSize)
{
size_t overflow = (m_loopbackBuffer.size() + resampledSamples.size()) - maxBufferSize;
if (overflow >= m_loopbackBuffer.size())
{
m_loopbackBuffer.clear();
}
else
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + overflow);
}
}
m_loopbackBuffer.insert(m_loopbackBuffer.end(), resampledSamples.begin(), resampledSamples.end());
}
}
void AudioSampleGenerator::FlushRemainingAudio()
{
// Called during stop to drain any remaining samples from loopback capture
// and convert them to MediaStreamSamples before the audio graph stops.
if (!m_loopbackCapture)
{
return;
}
auto lock = m_lock.lock_exclusive();
// Drain all remaining samples from the loopback capture client
std::vector<float> rawLoopbackSamples;
{
std::vector<float> tempSamples;
while (m_loopbackCapture->TryGetSamples(tempSamples))
{
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
}
}
// Resample and channel-convert the loopback audio to match AudioGraph format
if (!rawLoopbackSamples.empty())
{
AppendResampledLoopbackSamples(rawLoopbackSamples, true);
}
// Now convert everything in m_loopbackBuffer to MediaStreamSamples
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
if (!m_loopbackBuffer.empty())
{
uint32_t outputSampleCount = static_cast<uint32_t>(m_loopbackBuffer.size());
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
float* outputFloats = reinterpret_cast<float*>(outputData.data());
for (uint32_t i = 0; i < outputSampleCount; i++)
{
float sample = m_loopbackBuffer[i];
if (sample > 1.0f) sample = 1.0f;
else if (sample < -1.0f) sample = -1.0f;
outputFloats[i] = sample;
}
m_loopbackBuffer.clear();
// Create buffer and sample
winrt::Buffer sampleBuffer(outputSampleCount * sizeof(float));
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
if (sampleBuffer.Length() > 0)
{
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
const winrt::TimeSpan duration{ durationTicks };
winrt::TimeSpan timestamp{ 0 };
if (m_hasLastSampleTimestamp)
{
timestamp = winrt::TimeSpan{ m_lastSampleTimestamp.count() + m_lastSampleDuration.count() };
}
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp);
m_samples.push_back(sample);
m_audioEvent.SetEvent();
m_lastSampleTimestamp = timestamp;
m_lastSampleDuration = duration;
m_hasLastSampleTimestamp = true;
}
}
}
void AudioSampleGenerator::CombineQueuedSamples()
{
// Combine all queued samples into a single sample so it can be returned
// immediately in the next TryGetNextSample call. This is critical because
// once video ends, the MediaTranscoder may only request one more audio sample.
auto lock = m_lock.lock_exclusive();
if (m_samples.size() <= 1)
{
return;
}
// Calculate total size and collect all sample data
size_t totalBytes = 0;
std::vector<std::pair<winrt::Windows::Storage::Streams::IBuffer, winrt::Windows::Foundation::TimeSpan>> buffers;
winrt::Windows::Foundation::TimeSpan firstTimestamp{ 0 };
bool hasFirstTimestamp = false;
for (auto& sample : m_samples)
{
auto buffer = sample.Buffer();
if (buffer)
{
totalBytes += buffer.Length();
if (!hasFirstTimestamp)
{
firstTimestamp = sample.Timestamp();
hasFirstTimestamp = true;
}
buffers.push_back({ buffer, sample.Timestamp() });
}
}
if (totalBytes == 0)
{
return;
}
// Create combined buffer
winrt::Buffer combinedBuffer(static_cast<uint32_t>(totalBytes));
uint8_t* dest = combinedBuffer.data();
uint32_t offset = 0;
for (auto& [buffer, ts] : buffers)
{
uint32_t len = buffer.Length();
memcpy(dest + offset, buffer.data(), len);
offset += len;
}
combinedBuffer.Length(static_cast<uint32_t>(totalBytes));
// Create combined sample with first timestamp
auto combinedSample = winrt::Windows::Media::Core::MediaStreamSample::CreateFromBuffer(combinedBuffer, firstTimestamp);
// Clear queue and add combined sample
m_samples.clear();
m_samples.push_back(combinedSample);
// Update timestamp tracking
const uint32_t sampleCount = static_cast<uint32_t>(totalBytes) / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
m_lastSampleTimestamp = firstTimestamp;
m_lastSampleDuration = winrt::Windows::Foundation::TimeSpan{ durationTicks };
m_hasLastSampleTimestamp = true;
}
void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender, winrt::IInspectable const& args)
{
// Don't process if we're not actively recording
if (!m_started.load())
{
return;
}
{
auto lock = m_lock.lock_exclusive();
@@ -159,10 +614,101 @@ void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender
std::optional<winrt::TimeSpan> timestamp = frame.RelativeTime();
auto audioBuffer = frame.LockBuffer(winrt::AudioBufferAccessMode::Read);
// Get mic audio as a buffer (may be empty if no microphone)
auto sampleBuffer = winrt::Buffer::CreateCopyFromMemoryBuffer(audioBuffer);
sampleBuffer.Length(audioBuffer.Length());
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
m_samples.push_back(sample);
// Calculate expected samples per quantum (~10ms at graph sample rate)
// AudioGraph uses 10ms quantums by default
uint32_t expectedSamplesPerQuantum = (m_graphSampleRate / 100) * m_graphChannels;
uint32_t numMicSamples = audioBuffer.Length() / sizeof(float);
// Drain loopback samples regardless of whether we have mic audio
if (m_loopbackCapture)
{
std::vector<float> rawLoopbackSamples;
{
std::vector<float> tempSamples;
while (m_loopbackCapture->TryGetSamples(tempSamples))
{
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
}
}
// Resample and channel-convert the loopback audio to match AudioGraph format
if (!rawLoopbackSamples.empty())
{
AppendResampledLoopbackSamples(rawLoopbackSamples);
}
}
// Determine the actual number of samples we'll output
// Use mic sample count if mic is enabled
uint32_t outputSampleCount = m_captureMicrophone ? numMicSamples : expectedSamplesPerQuantum;
// If microphone is disabled, create a buffer with only loopback audio
if (!m_captureMicrophone && outputSampleCount > 0)
{
// Create a buffer filled with loopback audio or silence
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
float* outputFloats = reinterpret_cast<float*>(outputData.data());
{
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
uint32_t samplesToUse = min(outputSampleCount, static_cast<uint32_t>(m_loopbackBuffer.size()));
for (uint32_t i = 0; i < samplesToUse; i++)
{
float sample = m_loopbackBuffer[i];
if (sample > 1.0f) sample = 1.0f;
else if (sample < -1.0f) sample = -1.0f;
outputFloats[i] = sample;
}
if (samplesToUse > 0)
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToUse);
}
}
// Create a new buffer with our loopback data
sampleBuffer = winrt::Buffer(outputSampleCount * sizeof(float));
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
}
else if (m_captureMicrophone && numMicSamples > 0)
{
// Mix loopback into mic samples
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
float* bufferData = reinterpret_cast<float*>(sampleBuffer.data());
uint32_t samplesToMix = min(numMicSamples, static_cast<uint32_t>(m_loopbackBuffer.size()));
for (uint32_t i = 0; i < samplesToMix; i++)
{
float mixed = bufferData[i] + m_loopbackBuffer[i];
if (mixed > 1.0f) mixed = 1.0f;
else if (mixed < -1.0f) mixed = -1.0f;
bufferData[i] = mixed;
}
if (samplesToMix > 0)
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToMix);
}
}
if (sampleBuffer.Length() > 0)
{
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
m_samples.push_back(sample);
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
m_lastSampleTimestamp = timestamp.value();
m_lastSampleDuration = winrt::TimeSpan{ durationTicks };
m_hasLastSampleTimestamp = true;
}
}
m_audioEvent.SetEvent();
}

View File

@@ -1,9 +1,11 @@
#pragma once
#include "LoopbackCapture.h"
class AudioSampleGenerator
{
public:
AudioSampleGenerator();
AudioSampleGenerator(bool captureMicrophone = true, bool captureSystemAudio = true);
~AudioSampleGenerator();
winrt::Windows::Foundation::IAsyncAction InitializeAsync();
@@ -18,6 +20,10 @@ private:
winrt::Windows::Media::Audio::AudioGraph const& sender,
winrt::Windows::Foundation::IInspectable const& args);
void FlushRemainingAudio();
void CombineQueuedSamples();
void AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining = false);
void CheckInitialized()
{
if (!m_initialized.load())
@@ -37,12 +43,31 @@ private:
private:
winrt::Windows::Media::Audio::AudioGraph m_audioGraph{ nullptr };
winrt::Windows::Media::Audio::AudioDeviceInputNode m_audioInputNode{ nullptr };
winrt::Windows::Media::Audio::AudioSubmixNode m_submixNode{ nullptr };
winrt::Windows::Media::Audio::AudioFrameOutputNode m_audioOutputNode{ nullptr };
std::unique_ptr<LoopbackCapture> m_loopbackCapture;
std::vector<float> m_loopbackBuffer; // Accumulated loopback samples (resampled to match AudioGraph)
wil::srwlock m_loopbackBufferLock;
uint32_t m_loopbackChannels = 2;
uint32_t m_loopbackSampleRate = 48000;
uint32_t m_graphSampleRate = 48000;
uint32_t m_graphChannels = 2;
double m_resampleRatio = 1.0; // loopbackSampleRate / graphSampleRate
winrt::Windows::Foundation::TimeSpan m_lastSampleTimestamp{};
winrt::Windows::Foundation::TimeSpan m_lastSampleDuration{};
bool m_hasLastSampleTimestamp = false;
std::vector<float> m_resampleInputBuffer; // raw loopback samples buffered for resampling
double m_resampleInputPos = 0.0; // fractional input frame position for resampling
wil::srwlock m_lock;
wil::unique_event m_audioEvent;
wil::unique_event m_endEvent;
wil::unique_event m_startEvent;
wil::unique_event m_asyncInitialized;
std::deque<winrt::Windows::Media::Core::MediaStreamSample> m_samples;
std::atomic<bool> m_initialized = false;
std::atomic<bool> m_started = false;
bool m_captureMicrophone = true;
bool m_captureSystemAudio = true;
};

View File

@@ -846,7 +846,6 @@ LRESULT CALLBACK DemoTypeHookProc( int nCode, WPARAM wParam, LPARAM lParam )
if( g_UserDriven )
{
// Set baseline indentation to a blocking flag
// Otherwise indentation seeking will trigger user-driven injection events
g_BaselineIndentation = INDENT_SEEK_FLAG;
// Initialize the injection handler

View File

@@ -242,6 +242,13 @@ std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
//----------------------------------------------------------------------------
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
{
std::lock_guard<std::mutex> lock(m_encoderMutex);
if (m_encoderReleased)
{
OutputDebugStringW(L"EncodeFrame called after encoder released.\n");
return E_FAIL;
}
try
{
// Create a staging texture for CPU access
@@ -367,6 +374,7 @@ HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
// Increment and log frame count
m_frameCount++;
m_hasAnyFrame.store(true);
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
return S_OK;
@@ -405,6 +413,12 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
{
captureAttempts++;
auto frame = m_frameWait->TryGetNextFrame();
if (!frame && !m_isRecording)
{
// Recording was stopped while waiting for frame
OutputDebugStringW(L"[GIF] Recording stopped during frame wait\n");
break;
}
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
@@ -472,8 +486,17 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
// Wait for the next frame interval
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
// Check again after resuming from sleep
if (!m_isRecording || m_closed)
{
OutputDebugStringW(L"[GIF] Loop exiting after resume_after\n");
break;
}
}
OutputDebugStringW(L"[GIF] Capture loop exited\n");
// Commit the GIF encoder
if (m_gifEncoder)
{
@@ -511,6 +534,10 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
CloseInternal();
}
}
// Ensure encoder resources are released in case caller forgets to Close explicitly.
ReleaseEncoderResources();
OutputDebugStringW(L"[GIF] StartAsync completing, about to co_return\n");
co_return;
}
@@ -521,18 +548,18 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
//----------------------------------------------------------------------------
void GifRecordingSession::Close()
{
OutputDebugStringW(L"[GIF] Close() called\n");
auto expected = false;
if (m_closed.compare_exchange_strong(expected, true))
{
expected = true;
if (!m_isRecording.compare_exchange_strong(expected, false))
{
CloseInternal();
}
else
{
m_frameWait->StopCapture();
}
OutputDebugStringW(L"[GIF] Setting m_closed = true\n");
// Signal the capture loop to stop
m_isRecording = false;
OutputDebugStringW(L"[GIF] Setting m_isRecording = false\n");
// Stop the frame wait to unblock any pending frame acquisition
m_frameWait->StopCapture();
OutputDebugStringW(L"[GIF] StopCapture called\n");
}
}
@@ -543,6 +570,42 @@ void GifRecordingSession::Close()
//----------------------------------------------------------------------------
void GifRecordingSession::CloseInternal()
{
ReleaseEncoderResources();
m_frameWait->StopCapture();
m_itemClosed.revoke();
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::ReleaseEncoderResources
// Ensures encoder/stream COM objects release the temp file handle so trim can reopen it.
//
//----------------------------------------------------------------------------
void GifRecordingSession::ReleaseEncoderResources()
{
std::lock_guard<std::mutex> lock(m_encoderMutex);
if (m_encoderReleased)
{
return;
}
// Commit only if we still own the encoder and it has not been committed; swallow failures.
if (m_gifEncoder)
{
try
{
m_gifEncoder->Commit();
}
catch (...)
{
}
}
m_encoderMetadataWriter = nullptr;
m_gifEncoder = nullptr;
m_wicStream = nullptr;
m_wicFactory = nullptr;
m_stream = nullptr;
m_encoderReleased = true;
}

View File

@@ -11,6 +11,7 @@
#include "CaptureFrameWait.h"
#include <d3d11_4.h>
#include <vector>
#include <mutex>
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
{
@@ -27,6 +28,8 @@ public:
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
bool HasCapturedFrames() const { return m_hasAnyFrame.load(); }
private:
GifRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
@@ -35,6 +38,7 @@ private:
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
void ReleaseEncoderResources();
HRESULT EncodeFrame(ID3D11Texture2D* texture);
private:
@@ -58,6 +62,9 @@ private:
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
std::atomic<bool> m_encoderReleased = false;
std::atomic<bool> m_hasAnyFrame = false;
std::mutex m_encoderMutex;
uint32_t m_frameWidth=0;
uint32_t m_frameHeight=0;

View File

@@ -0,0 +1,337 @@
#include "pch.h"
#include "LoopbackCapture.h"
#include <functiondiscoverykeys_devpkey.h>
#pragma comment(lib, "ole32.lib")
LoopbackCapture::LoopbackCapture()
{
m_stopEvent.create(wil::EventOptions::ManualReset);
m_samplesReadyEvent.create(wil::EventOptions::ManualReset);
}
LoopbackCapture::~LoopbackCapture()
{
Stop();
if (m_pwfx)
{
CoTaskMemFree(m_pwfx);
m_pwfx = nullptr;
}
}
HRESULT LoopbackCapture::Initialize()
{
if (m_initialized.load())
{
return S_OK;
}
HRESULT hr = CoCreateInstance(
__uuidof(MMDeviceEnumerator),
nullptr,
CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
m_deviceEnumerator.put_void());
if (FAILED(hr))
{
return hr;
}
// Get the default audio render device (speakers/headphones)
hr = m_deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, m_device.put());
if (FAILED(hr))
{
return hr;
}
hr = m_device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, m_audioClient.put_void());
if (FAILED(hr))
{
return hr;
}
// Get the mix format
hr = m_audioClient->GetMixFormat(&m_pwfx);
if (FAILED(hr))
{
return hr;
}
// Initialize audio client in loopback mode
// AUDCLNT_STREAMFLAGS_LOOPBACK enables capturing what's being played on the device
hr = m_audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK,
1000000, // 100ms buffer to reduce capture latency
0,
m_pwfx,
nullptr);
if (FAILED(hr))
{
return hr;
}
hr = m_audioClient->GetService(__uuidof(IAudioCaptureClient), m_captureClient.put_void());
if (FAILED(hr))
{
return hr;
}
m_initialized.store(true);
return S_OK;
}
HRESULT LoopbackCapture::Start()
{
if (!m_initialized.load())
{
return E_NOT_VALID_STATE;
}
if (m_started.load())
{
return S_OK;
}
m_stopEvent.ResetEvent();
HRESULT hr = m_audioClient->Start();
if (FAILED(hr))
{
return hr;
}
m_started.store(true);
// Start capture thread
m_captureThread = std::thread(&LoopbackCapture::CaptureThread, this);
return S_OK;
}
void LoopbackCapture::Stop()
{
if (!m_started.load())
{
return;
}
m_stopEvent.SetEvent();
if (m_captureThread.joinable())
{
m_captureThread.join();
}
DrainCaptureClient();
if (m_audioClient)
{
m_audioClient->Stop();
}
m_started.store(false);
}
void LoopbackCapture::DrainCaptureClient()
{
if (!m_captureClient)
{
return;
}
while (true)
{
UINT32 packetLength = 0;
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr) || packetLength == 0)
{
break;
}
BYTE* pData = nullptr;
UINT32 numFramesAvailable = 0;
DWORD flags = 0;
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
if (FAILED(hr))
{
break;
}
if (numFramesAvailable > 0)
{
std::vector<float> samples;
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
{
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else
{
float* floatData = reinterpret_cast<float*>(pData);
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
}
}
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
{
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else if (m_pwfx->wBitsPerSample == 16)
{
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
}
}
else if (m_pwfx->wBitsPerSample == 32)
{
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
}
}
}
if (!samples.empty())
{
auto lock = m_lock.lock_exclusive();
m_sampleQueue.push_back(std::move(samples));
m_samplesReadyEvent.SetEvent();
}
}
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr))
{
break;
}
}
}
void LoopbackCapture::CaptureThread()
{
while (WaitForSingleObject(m_stopEvent.get(), 10) == WAIT_TIMEOUT)
{
UINT32 packetLength = 0;
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr))
{
break;
}
while (packetLength != 0)
{
BYTE* pData = nullptr;
UINT32 numFramesAvailable = 0;
DWORD flags = 0;
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
if (FAILED(hr))
{
break;
}
if (numFramesAvailable > 0)
{
std::vector<float> samples;
// Convert to float samples
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
{
// Already float format
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
// Insert silence
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else
{
float* floatData = reinterpret_cast<float*>(pData);
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
}
}
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
{
// Convert PCM to float
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else if (m_pwfx->wBitsPerSample == 16)
{
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
}
}
else if (m_pwfx->wBitsPerSample == 32)
{
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
}
}
}
if (!samples.empty())
{
auto lock = m_lock.lock_exclusive();
m_sampleQueue.push_back(std::move(samples));
m_samplesReadyEvent.SetEvent();
}
}
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr))
{
break;
}
hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr))
{
break;
}
}
}
}
bool LoopbackCapture::TryGetSamples(std::vector<float>& samples)
{
auto lock = m_lock.lock_exclusive();
if (m_sampleQueue.empty())
{
return false;
}
samples = std::move(m_sampleQueue.front());
m_sampleQueue.pop_front();
if (m_sampleQueue.empty())
{
m_samplesReadyEvent.ResetEvent();
}
return true;
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <atomic>
#include <vector>
#include <deque>
#include <wil/resource.h>
class LoopbackCapture
{
public:
LoopbackCapture();
~LoopbackCapture();
HRESULT Initialize();
HRESULT Start();
void Stop();
// Returns audio samples in the format: PCM float, stereo, 48kHz
bool TryGetSamples(std::vector<float>& samples);
WAVEFORMATEX* GetFormat() const { return m_pwfx; }
uint32_t GetSampleRate() const { return m_pwfx ? m_pwfx->nSamplesPerSec : 48000; }
uint32_t GetChannels() const { return m_pwfx ? m_pwfx->nChannels : 2; }
private:
void CaptureThread();
void DrainCaptureClient();
winrt::com_ptr<IMMDeviceEnumerator> m_deviceEnumerator;
winrt::com_ptr<IMMDevice> m_device;
winrt::com_ptr<IAudioClient> m_audioClient;
winrt::com_ptr<IAudioCaptureClient> m_captureClient;
WAVEFORMATEX* m_pwfx{ nullptr };
wil::unique_event m_stopEvent;
wil::unique_event m_samplesReadyEvent;
std::thread m_captureThread;
wil::srwlock m_lock;
std::deque<std::vector<float>> m_sampleQueue;
std::atomic<bool> m_initialized{ false };
std::atomic<bool> m_started{ false };
};

View File

@@ -8,6 +8,579 @@
//==============================================================================
#include "pch.h"
#include "Utility.h"
#include <string>
#pragma comment(lib, "uxtheme.lib")
//----------------------------------------------------------------------------
// Dark Mode - Static/Global State
//----------------------------------------------------------------------------
static bool g_darkModeInitialized = false;
static bool g_darkModeEnabled = false;
static HBRUSH g_darkBackgroundBrush = nullptr;
static HBRUSH g_darkControlBrush = nullptr;
static HBRUSH g_darkSurfaceBrush = nullptr;
// Theme override from registry (defined in ZoomItSettings.h)
extern DWORD g_ThemeOverride;
// Preferred App Mode values for Windows 10/11 dark mode
enum class PreferredAppMode
{
Default,
AllowDark,
ForceDark,
ForceLight,
Max
};
// Undocumented ordinals from uxtheme.dll for dark mode support
using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode);
using fnAllowDarkModeForWindow = bool(WINAPI*)(HWND hWnd, bool allow);
using fnShouldAppsUseDarkMode = bool(WINAPI*)();
using fnRefreshImmersiveColorPolicyState = void(WINAPI*)();
using fnFlushMenuThemes = void(WINAPI*)();
static fnSetPreferredAppMode pSetPreferredAppMode = nullptr;
static fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr;
static fnShouldAppsUseDarkMode pShouldAppsUseDarkMode = nullptr;
static fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr;
static fnFlushMenuThemes pFlushMenuThemes = nullptr;
//----------------------------------------------------------------------------
//
// InitializeDarkModeSupport
//
// Initialize dark mode function pointers from uxtheme.dll
//
//----------------------------------------------------------------------------
static void InitializeDarkModeSupport()
{
if (g_darkModeInitialized)
return;
g_darkModeInitialized = true;
HMODULE hUxTheme = GetModuleHandleW(L"uxtheme.dll");
if (hUxTheme)
{
// These are undocumented ordinal exports
// Ordinal 135: SetPreferredAppMode (Windows 10 1903+)
pSetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(135)));
// Ordinal 133: AllowDarkModeForWindow
pAllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)));
// Ordinal 132: ShouldAppsUseDarkMode
pShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(132)));
// Ordinal 104: RefreshImmersiveColorPolicyState
pRefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(104)));
// Ordinal 136: FlushMenuThemes
pFlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(136)));
// Set preferred app mode based on our theme override or system setting
// Note: We check g_ThemeOverride directly here because IsDarkModeEnabled
// calls InitializeDarkModeSupport, which would cause recursion
if (pSetPreferredAppMode)
{
bool useDarkMode = false;
if (g_ThemeOverride == 0)
{
useDarkMode = false; // Force light
}
else if (g_ThemeOverride == 1)
{
useDarkMode = true; // Force dark
}
else if (pShouldAppsUseDarkMode)
{
useDarkMode = pShouldAppsUseDarkMode(); // Use system setting
}
if (useDarkMode)
{
pSetPreferredAppMode(PreferredAppMode::ForceDark);
}
else
{
pSetPreferredAppMode(PreferredAppMode::ForceLight);
}
}
// Flush menu themes to apply dark mode to context menus
if (pFlushMenuThemes)
{
pFlushMenuThemes();
}
}
// Update cached dark mode state
g_darkModeEnabled = false;
if (g_ThemeOverride == 0)
{
g_darkModeEnabled = false;
}
else if (g_ThemeOverride == 1)
{
g_darkModeEnabled = true;
}
else if (pShouldAppsUseDarkMode)
{
g_darkModeEnabled = pShouldAppsUseDarkMode();
}
}
//----------------------------------------------------------------------------
//
// IsDarkModeEnabled
//
//----------------------------------------------------------------------------
bool IsDarkModeEnabled()
{
// Check for theme override from registry (0=light, 1=dark, 2+=system)
if (g_ThemeOverride == 0)
{
return false; // Force light mode
}
else if (g_ThemeOverride == 1)
{
return true; // Force dark mode
}
InitializeDarkModeSupport();
// Check the undocumented API first
if (pShouldAppsUseDarkMode)
{
return pShouldAppsUseDarkMode();
}
// Fallback: Check registry for system theme preference
HKEY hKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
DWORD value = 1;
DWORD size = sizeof(value);
RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr,
reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
return value == 0; // 0 = dark mode, 1 = light mode
}
return false;
}
//----------------------------------------------------------------------------
//
// RefreshDarkModeState
//
//----------------------------------------------------------------------------
void RefreshDarkModeState()
{
InitializeDarkModeSupport();
if (pRefreshImmersiveColorPolicyState)
{
pRefreshImmersiveColorPolicyState();
}
// Update preferred app mode based on our IsDarkModeEnabled (respects override)
bool useDark = IsDarkModeEnabled();
if (pSetPreferredAppMode)
{
if (useDark)
{
pSetPreferredAppMode(PreferredAppMode::ForceDark);
}
else
{
pSetPreferredAppMode(PreferredAppMode::ForceLight);
}
}
// Flush menu themes to apply dark mode to context menus
if (pFlushMenuThemes)
{
pFlushMenuThemes();
}
g_darkModeEnabled = useDark;
}
//----------------------------------------------------------------------------
//
// SetDarkModeForWindow
//
//----------------------------------------------------------------------------
void SetDarkModeForWindow(HWND hWnd, bool enable)
{
InitializeDarkModeSupport();
if (pAllowDarkModeForWindow)
{
pAllowDarkModeForWindow(hWnd, enable);
}
// Use DWMWA_USE_IMMERSIVE_DARK_MODE attribute (Windows 10 build 17763+)
// Attribute 20 is DWMWA_USE_IMMERSIVE_DARK_MODE
BOOL useDarkMode = enable ? TRUE : FALSE;
HMODULE hDwmapi = GetModuleHandleW(L"dwmapi.dll");
if (hDwmapi)
{
using fnDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
auto pDwmSetWindowAttribute = reinterpret_cast<fnDwmSetWindowAttribute>(
GetProcAddress(hDwmapi, "DwmSetWindowAttribute"));
if (pDwmSetWindowAttribute)
{
// Try attribute 20 first (Windows 11 / newer Windows 10)
HRESULT hr = pDwmSetWindowAttribute(hWnd, 20, &useDarkMode, sizeof(useDarkMode));
if (FAILED(hr))
{
// Fall back to attribute 19 (older Windows 10)
pDwmSetWindowAttribute(hWnd, 19, &useDarkMode, sizeof(useDarkMode));
}
}
}
}
//----------------------------------------------------------------------------
//
// GetDarkModeBrush / GetDarkModeControlBrush / GetDarkModeSurfaceBrush
//
//----------------------------------------------------------------------------
HBRUSH GetDarkModeBrush()
{
if (!g_darkBackgroundBrush)
{
g_darkBackgroundBrush = CreateSolidBrush(DarkMode::BackgroundColor);
}
return g_darkBackgroundBrush;
}
HBRUSH GetDarkModeControlBrush()
{
if (!g_darkControlBrush)
{
g_darkControlBrush = CreateSolidBrush(DarkMode::ControlColor);
}
return g_darkControlBrush;
}
HBRUSH GetDarkModeSurfaceBrush()
{
if (!g_darkSurfaceBrush)
{
g_darkSurfaceBrush = CreateSolidBrush(DarkMode::SurfaceColor);
}
return g_darkSurfaceBrush;
}
//----------------------------------------------------------------------------
//
// ApplyDarkModeToDialog
//
//----------------------------------------------------------------------------
void ApplyDarkModeToDialog(HWND hDlg)
{
if (IsDarkModeEnabled())
{
SetDarkModeForWindow(hDlg, true);
// Set dark theme for the dialog
SetWindowTheme(hDlg, L"DarkMode_Explorer", nullptr);
// Apply dark theme to common controls (buttons, edit boxes, etc.)
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
wchar_t className[64] = { 0 };
GetClassNameW(hChild, className, _countof(className));
// Apply appropriate theme based on control type
if (_wcsicmp(className, L"Button") == 0)
{
// Check if this is a checkbox or radio button
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG buttonType = style & BS_TYPEMASK;
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
{
// Subclass checkbox/radio for dark mode painting - but keep DarkMode_Explorer theme
// for proper hit testing (empty theme can break mouse interaction)
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
SetWindowSubclass(hChild, CheckboxSubclassProc, 2, 0);
}
else if (buttonType == BS_GROUPBOX)
{
// Subclass group box for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, GroupBoxSubclassProc, 4, 0);
}
else
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
}
else if (_wcsicmp(className, L"Edit") == 0)
{
// Use empty theme and subclass for dark mode border drawing
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, EditControlSubclassProc, 3, 0);
}
else if (_wcsicmp(className, L"ComboBox") == 0)
{
SetWindowTheme(hChild, L"DarkMode_CFD", nullptr);
}
else if (_wcsicmp(className, L"SysListView32") == 0 ||
_wcsicmp(className, L"SysTreeView32") == 0)
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
{
// Subclass trackbar controls for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, SliderSubclassProc, 1, 0);
}
else if (_wcsicmp(className, L"SysTabControl32") == 0)
{
// Use empty theme for tab control to allow dark background
SetWindowTheme(hChild, L"", L"");
}
else if (_wcsicmp(className, L"msctls_updown32") == 0)
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
else if (_wcsicmp(className, L"msctls_hotkey32") == 0)
{
// Subclass hotkey controls for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, HotkeyControlSubclassProc, 1, 0);
}
else if (_wcsicmp(className, L"Static") == 0)
{
// Check if this is a text label (not an owner-draw or image control)
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG staticType = style & SS_TYPEMASK;
// Options header uses a dedicated static subclass (to support large title font).
// Avoid applying the generic static subclass on top of it.
const int controlId = GetDlgCtrlID( hChild );
if( controlId == IDC_VERSION || controlId == IDC_COPYRIGHT )
{
SetWindowTheme( hChild, L"", L"" );
return TRUE;
}
if (staticType == SS_LEFT || staticType == SS_CENTER || staticType == SS_RIGHT ||
staticType == SS_LEFTNOWORDWRAP || staticType == SS_SIMPLE)
{
// Subclass text labels for proper dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, StaticTextSubclassProc, 5, 0);
}
else
{
// Other static controls (icons, bitmaps, frames) - just remove theme
SetWindowTheme(hChild, L"", L"");
}
}
else
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
return TRUE;
}, 0);
}
else
{
// Light mode - remove dark mode
SetDarkModeForWindow(hDlg, false);
SetWindowTheme(hDlg, nullptr, nullptr);
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
// Remove subclass from controls
wchar_t className[64] = { 0 };
GetClassNameW(hChild, className, _countof(className));
if (_wcsicmp(className, L"msctls_hotkey32") == 0)
{
RemoveWindowSubclass(hChild, HotkeyControlSubclassProc, 1);
}
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
{
RemoveWindowSubclass(hChild, SliderSubclassProc, 1);
}
else if (_wcsicmp(className, L"Button") == 0)
{
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG buttonType = style & BS_TYPEMASK;
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
{
RemoveWindowSubclass(hChild, CheckboxSubclassProc, 2);
}
else if (buttonType == BS_GROUPBOX)
{
RemoveWindowSubclass(hChild, GroupBoxSubclassProc, 4);
}
}
else if (_wcsicmp(className, L"Edit") == 0)
{
RemoveWindowSubclass(hChild, EditControlSubclassProc, 3);
}
else if (_wcsicmp(className, L"Static") == 0)
{
RemoveWindowSubclass(hChild, StaticTextSubclassProc, 5);
}
SetWindowTheme(hChild, nullptr, nullptr);
return TRUE;
}, 0);
}
}
//----------------------------------------------------------------------------
//
// HandleDarkModeCtlColor
//
//----------------------------------------------------------------------------
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message)
{
if (!IsDarkModeEnabled())
{
return nullptr;
}
switch (message)
{
case WM_CTLCOLORDLG:
SetBkColor(hdc, DarkMode::BackgroundColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeBrush();
case WM_CTLCOLORSTATIC:
SetBkMode(hdc, TRANSPARENT);
// Use dimmed color for disabled static controls
if (!IsWindowEnabled(hCtrl))
{
SetTextColor(hdc, RGB(100, 100, 100));
}
else
{
SetTextColor(hdc, DarkMode::TextColor);
}
return GetDarkModeBrush();
case WM_CTLCOLORBTN:
SetBkColor(hdc, DarkMode::ControlColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeControlBrush();
case WM_CTLCOLOREDIT:
SetBkColor(hdc, DarkMode::SurfaceColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeSurfaceBrush();
case WM_CTLCOLORLISTBOX:
SetBkColor(hdc, DarkMode::SurfaceColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeSurfaceBrush();
}
return nullptr;
}
//----------------------------------------------------------------------------
//
// ApplyDarkModeToMenu
//
// Uses undocumented uxtheme functions to enable dark mode for menus
//
//----------------------------------------------------------------------------
void ApplyDarkModeToMenu(HMENU hMenu)
{
if (!hMenu)
{
return;
}
if (!IsDarkModeEnabled())
{
// Light mode - clear any dark background
MENUINFO mi = { sizeof(mi) };
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
mi.hbrBack = nullptr;
SetMenuInfo(hMenu, &mi);
return;
}
// For popup menus, we need to use MENUINFO to set the background
MENUINFO mi = { sizeof(mi) };
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
mi.hbrBack = GetDarkModeSurfaceBrush();
SetMenuInfo(hMenu, &mi);
}
//----------------------------------------------------------------------------
//
// RefreshWindowTheme
//
// Forces a window and all its children to redraw with current theme
//
//----------------------------------------------------------------------------
void RefreshWindowTheme(HWND hWnd)
{
if (!hWnd)
{
return;
}
// Reapply theme to this window
ApplyDarkModeToDialog(hWnd);
// Force redraw
RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME);
}
//----------------------------------------------------------------------------
//
// CleanupDarkModeResources
//
//----------------------------------------------------------------------------
void CleanupDarkModeResources()
{
if (g_darkBackgroundBrush)
{
DeleteObject(g_darkBackgroundBrush);
g_darkBackgroundBrush = nullptr;
}
if (g_darkControlBrush)
{
DeleteObject(g_darkControlBrush);
g_darkControlBrush = nullptr;
}
if (g_darkSurfaceBrush)
{
DeleteObject(g_darkSurfaceBrush);
g_darkSurfaceBrush = nullptr;
}
}
//----------------------------------------------------------------------------
//
// InitializeDarkMode
//
// Public wrapper to initialize dark mode support early in app startup
//
//----------------------------------------------------------------------------
void InitializeDarkMode()
{
InitializeDarkModeSupport();
}
//----------------------------------------------------------------------------
//
@@ -151,3 +724,177 @@ POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target )
return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ),
targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) };
}
//----------------------------------------------------------------------------
//
// ScaleDialogForDpi
//
// Scales a dialog and all its child controls for the specified DPI.
// oldDpi defaults to DPI_BASELINE (96) for initial scaling.
//
//----------------------------------------------------------------------------
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi )
{
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
{
return;
}
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
// We only need to scale when moving between monitors with different DPIs.
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
if( oldDpi == DPI_BASELINE )
{
return;
}
// Scale the dialog window itself
RECT dialogRect;
GetWindowRect( hDlg, &dialogRect );
int dialogWidth = MulDiv( dialogRect.right - dialogRect.left, newDpi, oldDpi );
int dialogHeight = MulDiv( dialogRect.bottom - dialogRect.top, newDpi, oldDpi );
SetWindowPos( hDlg, nullptr, 0, 0, dialogWidth, dialogHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE );
// Enumerate and scale all child controls
HWND hChild = GetWindow( hDlg, GW_CHILD );
while( hChild != nullptr )
{
RECT childRect;
GetWindowRect( hChild, &childRect );
MapWindowPoints( nullptr, hDlg, reinterpret_cast<LPPOINT>(&childRect), 2 );
int x = MulDiv( childRect.left, newDpi, oldDpi );
int y = MulDiv( childRect.top, newDpi, oldDpi );
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
// Scale the font for the control
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
if( hFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
// Note: The old font might be shared, so we don't delete it here
// The system will clean up fonts when the dialog is destroyed
}
}
}
hChild = GetWindow( hChild, GW_HWNDNEXT );
}
// Also scale the dialog's own font
HFONT hDialogFont = reinterpret_cast<HFONT>(SendMessage( hDlg, WM_GETFONT, 0, 0 ));
if( hDialogFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hDialogFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hDlg, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
}
}
}
}
//----------------------------------------------------------------------------
//
// ScaleChildControlsForDpi
//
// Scales a window's direct child controls (and their fonts) for the specified DPI.
// Unlike ScaleDialogForDpi, this does not resize the parent window itself.
//
// This is useful for child dialogs used as tab pages: the tab page window is
// already scaled when the parent options dialog is scaled, but the controls
// inside the page are not (because they are grandchildren of the options dialog).
//
//----------------------------------------------------------------------------
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi )
{
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
{
return;
}
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
// We only need to scale when moving between monitors with different DPIs.
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
if( oldDpi == DPI_BASELINE )
{
return;
}
HWND hChild = GetWindow( hParent, GW_CHILD );
while( hChild != nullptr )
{
RECT childRect;
GetWindowRect( hChild, &childRect );
MapWindowPoints( nullptr, hParent, reinterpret_cast<LPPOINT>(&childRect), 2 );
int x = MulDiv( childRect.left, newDpi, oldDpi );
int y = MulDiv( childRect.top, newDpi, oldDpi );
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
// Scale the font for the control
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
if( hFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
}
}
}
hChild = GetWindow( hChild, GW_HWNDNEXT );
}
}
//----------------------------------------------------------------------------
//
// HandleDialogDpiChange
//
// Handles WM_DPICHANGED message for dialogs. Call this from the dialog's
// WndProc when WM_DPICHANGED is received.
//
//----------------------------------------------------------------------------
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi )
{
UINT newDpi = HIWORD( wParam );
if( newDpi != currentDpi && newDpi != 0 )
{
const RECT* pSuggestedRect = reinterpret_cast<const RECT*>(lParam);
// Scale the dialog controls from the current DPI to the new DPI
ScaleDialogForDpi( hDlg, newDpi, currentDpi );
// Move and resize the dialog to the suggested rectangle
SetWindowPos( hDlg, nullptr,
pSuggestedRect->left,
pSuggestedRect->top,
pSuggestedRect->right - pSuggestedRect->left,
pSuggestedRect->bottom - pSuggestedRect->top,
SWP_NOZORDER | SWP_NOACTIVATE );
currentDpi = newDpi;
}
}

View File

@@ -9,6 +9,10 @@
#pragma once
#include "pch.h"
#include <uxtheme.h>
// DPI baseline for scaling calculations (dialog units are designed at 96 DPI)
constexpr UINT DPI_BASELINE = USER_DEFAULT_SCREEN_DPI;
RECT ForceRectInBounds( RECT rect, const RECT& bounds );
UINT GetDpiForWindowHelper( HWND window );
@@ -16,3 +20,86 @@ RECT GetMonitorRectFromCursor();
RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize );
int ScaleForDpi( int value, UINT dpi );
POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target );
// Dialog DPI scaling functions
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi = DPI_BASELINE );
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi = DPI_BASELINE );
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi );
//----------------------------------------------------------------------------
// Dark Mode Support
//----------------------------------------------------------------------------
// Dark mode colors
namespace DarkMode
{
// Background colors
constexpr COLORREF BackgroundColor = RGB(32, 32, 32);
constexpr COLORREF SurfaceColor = RGB(45, 45, 48);
constexpr COLORREF ControlColor = RGB(51, 51, 55);
// Text colors
constexpr COLORREF TextColor = RGB(200, 200, 200);
constexpr COLORREF DisabledTextColor = RGB(120, 120, 120);
constexpr COLORREF LinkColor = RGB(86, 156, 214);
// Border/accent colors
constexpr COLORREF BorderColor = RGB(67, 67, 70);
constexpr COLORREF AccentColor = RGB(0, 120, 215);
constexpr COLORREF HoverColor = RGB(62, 62, 66);
// Light mode colors for contrast
constexpr COLORREF LightBackgroundColor = RGB(255, 255, 255);
constexpr COLORREF LightTextColor = RGB(0, 0, 0);
}
// Check if system dark mode is enabled
bool IsDarkModeEnabled();
// Refresh dark mode state (call when WM_SETTINGCHANGE received)
void RefreshDarkModeState();
// Enable dark mode title bar for a window
void SetDarkModeForWindow(HWND hWnd, bool enable);
// Apply dark mode to a dialog and enable dark title bar
void ApplyDarkModeToDialog(HWND hDlg);
// Get the appropriate background brush for dark/light mode
HBRUSH GetDarkModeBrush();
HBRUSH GetDarkModeControlBrush();
HBRUSH GetDarkModeSurfaceBrush();
// Handle WM_CTLCOLOR* messages for dark mode
// Returns the brush to use, or nullptr if default handling should be used
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message);
// Apply dark mode theme to a popup menu
void ApplyDarkModeToMenu(HMENU hMenu);
// Force redraw of a window and all its children for theme change
void RefreshWindowTheme(HWND hWnd);
// Cleanup dark mode resources (call at app exit)
void CleanupDarkModeResources();
// Initialize dark mode support early in app startup (call before creating windows)
void InitializeDarkMode();
// Subclass procedure for hotkey controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK HotkeyControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for checkbox controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK CheckboxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for edit controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK EditControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for group box controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK GroupBoxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for slider/trackbar controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK SliderSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for static text controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK StaticTextSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,12 @@
#include "CaptureFrameWait.h"
#include "AudioSampleGenerator.h"
#include <d3d11_4.h>
#include <ppltasks.h>
#include <atomic>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <vector>
class VideoRecordingSession : public std::enable_shared_from_this<VideoRecordingSession>
{
@@ -21,6 +27,7 @@ public:
RECT const& cropRect,
uint32_t frameRate,
bool captureAudio,
bool captureSystemAudio,
winrt::Streams::IRandomAccessStream const& stream);
~VideoRecordingSession();
@@ -28,6 +35,151 @@ public:
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
bool HasCapturedVideoFrames() const { return m_hasVideoSample.load(); }
// Trim and save functionality
static std::wstring ShowSaveDialogWithTrim(
HWND hWnd,
const std::wstring& suggestedFileName,
const std::wstring& originalVideoPath,
std::wstring& trimmedVideoPath);
struct TrimDialogData
{
struct GifFrame
{
HBITMAP hBitmap{ nullptr };
winrt::Windows::Foundation::TimeSpan start{ 0 };
winrt::Windows::Foundation::TimeSpan duration{ 0 };
UINT width{ 0 };
UINT height{ 0 };
};
std::wstring videoPath;
winrt::Windows::Foundation::TimeSpan videoDuration{ 0 };
winrt::Windows::Foundation::TimeSpan trimStart{ 0 };
winrt::Windows::Foundation::TimeSpan trimEnd{ 0 };
winrt::Windows::Foundation::TimeSpan originalTrimStart{ 0 }; // Initial value to detect if trim needed
winrt::Windows::Foundation::TimeSpan originalTrimEnd{ 0 }; // Initial value to detect if trim needed
winrt::Windows::Foundation::TimeSpan currentPosition{ 0 };
// Playback loop anchor. This is set when the user explicitly positions the playhead
// (e.g., dragging or using the jog buttons). Pausing/resuming should not change it.
winrt::Windows::Foundation::TimeSpan playbackStartPosition{ 0 };
bool playbackStartPositionValid{ false };
// Cached preview frame at playback start position for instant restore when playback stops.
HBITMAP hCachedStartFrame{ nullptr };
winrt::Windows::Foundation::TimeSpan cachedStartFramePosition{ -1 };
// When starting playback at a non-zero position, MediaPlayer may briefly report Position==0
// before the initial seek is applied. Use this to suppress a one-frame UI jump to 0.
std::atomic<bool> pendingInitialSeek{ false };
std::atomic<int64_t> pendingInitialSeekTicks{ 0 };
winrt::Windows::Media::Editing::MediaComposition composition{ nullptr };
winrt::Windows::Media::Playback::MediaPlayer mediaPlayer{ nullptr };
winrt::Windows::Storage::StorageFile playbackFile{ nullptr };
HBITMAP hPreviewBitmap{ nullptr };
HWND hDialog{ nullptr };
std::atomic<bool> loadingPreview{ false };
std::atomic<int64_t> latestPreviewRequest{ 0 };
std::atomic<int64_t> lastRenderedPreview{ -1 };
std::atomic<bool> isPlaying{ false };
// Monotonic serial used to cancel in-flight StartPlaybackAsync work when the user
// immediately pauses after starting playback.
std::atomic<uint64_t> playbackCommandSerial{ 0 };
std::atomic<bool> frameCopyInProgress{ false };
std::atomic<bool> smoothActive{ false };
std::atomic<int64_t> smoothBaseTicks{ 0 };
std::atomic<int64_t> smoothLastSyncMicroseconds{ 0 };
std::atomic<bool> smoothHasNonZeroSample{ false };
std::mutex previewBitmapMutex;
winrt::event_token frameAvailableToken{};
winrt::event_token positionChangedToken{};
winrt::event_token stateChangedToken{};
winrt::com_ptr<ID3D11Device> previewD3DDevice;
winrt::com_ptr<ID3D11DeviceContext> previewD3DContext;
winrt::com_ptr<ID3D11Texture2D> previewFrameTexture;
winrt::com_ptr<ID3D11Texture2D> previewFrameStaging;
bool hoverPlay{ false };
bool hoverRewind{ false };
bool hoverForward{ false };
bool hoverSkipStart{ false };
bool hoverSkipEnd{ false };
bool hoverVolumeIcon{ false };
double volume{ 0.70 }; // Volume level 0.0 to 1.0, initialized from g_TrimDialogVolume in dialog init
double previousVolume{ 0.70 }; // Volume before muting, for unmute restoration
winrt::Windows::Foundation::TimeSpan previewOverride{ 0 };
winrt::Windows::Foundation::TimeSpan positionBeforeOverride{ 0 };
bool previewOverrideActive{ false };
bool restorePreviewOnRelease{ false };
bool playheadPushed{ false };
int dialogX{ 0 };
int dialogY{ 0 };
bool isGif{ false };
bool previewBitmapOwned{ true };
std::vector<GifFrame> gifFrames;
bool gifFramesLoaded{ false };
size_t gifLastFrameIndex{ 0 };
std::chrono::steady_clock::time_point gifFrameStartTime{}; // When the current GIF frame started displaying
// Font for time labels
HFONT hTimeLabelFont{ nullptr };
// Mouse tracking for timeline
enum DragMode { None, TrimStart, Position, TrimEnd };
DragMode dragMode{ None };
bool isDragging{ false };
int lastPlayheadX{ -1 }; // Track last playhead pixel position for efficient invalidation
MMRESULT mmTimerId{ 0 }; // Multimedia timer for smooth MP4 playback
// Helper to convert time to pixel position
int TimeToPixel(winrt::Windows::Foundation::TimeSpan time, int timelineWidth) const
{
if (timelineWidth <= 0 || videoDuration.count() <= 0)
{
return 0;
}
double ratio = static_cast<double>(time.count()) / static_cast<double>(videoDuration.count());
ratio = std::clamp(ratio, 0.0, 1.0);
return static_cast<int>(ratio * timelineWidth);
}
// Helper to convert pixel to time
winrt::Windows::Foundation::TimeSpan PixelToTime(int pixel, int timelineWidth) const
{
if (timelineWidth <= 0 || videoDuration.count() <= 0)
{
return winrt::Windows::Foundation::TimeSpan{ 0 };
}
int clampedPixel = std::clamp(pixel, 0, timelineWidth);
double ratio = static_cast<double>(clampedPixel) / static_cast<double>(timelineWidth);
return winrt::Windows::Foundation::TimeSpan{ static_cast<int64_t>(ratio * videoDuration.count()) };
}
};
static INT_PTR ShowTrimDialog(
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
private:
static INT_PTR CALLBACK TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimVideoAsync(
const std::wstring& sourceVideoPath,
winrt::Windows::Foundation::TimeSpan trimTimeStart,
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimGifAsync(
const std::wstring& sourceGifPath,
winrt::Windows::Foundation::TimeSpan trimTimeStart,
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
static INT_PTR ShowTrimDialogInternal(
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
private:
VideoRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
@@ -35,6 +187,7 @@ private:
RECT const cropRect,
uint32_t frameRate,
bool captureAudio,
bool captureSystemAudio,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
@@ -68,4 +221,7 @@ private:
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
// Set once the MediaStreamSource successfully returns at least one video sample.
std::atomic<bool> m_hasVideoSample = false;
};

View File

@@ -32,18 +32,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// TEXTINCLUDE
//
1 TEXTINCLUDE
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
3 TEXTINCLUDE
BEGIN
"#include ""binres.rc""\0"
END
@@ -113,26 +113,26 @@ END
// Dialog
//
OPTIONS DIALOGEX 0, 0, 279, 325
OPTIONS DIALOGEX 0, 0, 299, 325
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CONTROLPARENT
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
LTEXT "ZoomIt v10.0",IDC_VERSION,42,7,73,10
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,265,245
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,285,247
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
END
ADVANCED_BREAK DIALOGEX 0, 0, 209, 219
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
ADVANCED_BREAK DIALOGEX 0, 0, 209, 225
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Advanced Break Options"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
@@ -158,23 +158,22 @@ BEGIN
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
DEFPUSHBUTTON "OK",IDOK,97,201,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,201,50,14
DEFPUSHBUTTON "OK",IDOK,97,199,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,199,50,14
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
END
ZOOM DIALOGEX 0, 0, 260, 170
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,230,26
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,230,10
LTEXT "1.25",IDC_STATIC,52,136,16,8
LTEXT "1.5",IDC_STATIC,82,136,12,8
LTEXT "1.75",IDC_STATIC,108,136,16,8
@@ -183,52 +182,52 @@ BEGIN
LTEXT "4.0",IDC_STATIC,190,136,12,8
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,230,17
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,230,18
END
DRAW DIALOGEX 0, 0, 260, 228
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,246,24
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,230,24
LTEXT "Pen Control ",IDC_PEN_CONTROL,7,38,40,8
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,233,16
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,218,16
LTEXT "Colors",IDC_COLORS,7,70,21,8
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,233,16
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,218,16
LTEXT "Highlight and Blur",IDC_HIGHLIGHT_AND_BLUR,7,102,58,8
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,233,16
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,218,16
LTEXT "Shapes",IDC_SHAPES,7,134,23,8
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,233,16
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,218,16
LTEXT "Screen",IDC_SCREEN,7,166,22,8
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,233,24
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,218,24
CONTROL "",IDC_DRAW_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,207,80,12
LTEXT "Draw w/out Zoom:",IDC_STATIC,7,210,63,11
END
TYPE DIALOGEX 0, 0, 260, 104
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,246,32
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,211,9
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,230,32
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,230,9
PUSHBUTTON "&Font",IDC_FONT,112,69,41,14
GROUPBOX "Text Font",IDC_TEXT_FONT,8,61,99,28
GROUPBOX "Sample",IDC_TEXT_FONT,8,61,99,28
END
BREAK DIALOGEX 0, 0, 260, 123
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
EDITTEXT IDC_TIMER,31,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,45,86,11,12
LTEXT "minutes",IDC_STATIC,67,88,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,212,102,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,246,33
EDITTEXT IDC_TIMER,52,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,86,11,12
LTEXT "minutes",IDC_STATIC,88,88,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,192,102,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,230,33
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
LTEXT "Timer:",IDC_STATIC,7,88,20,8
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,219,20
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,230,20
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
END
@@ -251,69 +250,90 @@ BEGIN
END
LIVEZOOM DIALOGEX 0, 0, 260, 134
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,246,18
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,230,18
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,218,13
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,246,27
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,246,32
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,230,13
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,230,27
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,230,32
END
RECORD DIALOGEX 0, 0, 260, 169
RECORD DIALOGEX 0, 0, 260, 181
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_RECORD_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,61,96,80,12
LTEXT "Record Toggle:",IDC_STATIC,7,98,54,8
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,246,28
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,248,28
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,249,19
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
LTEXT "Format:",IDC_STATIC,30,132,26,8
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_STATIC,32,166,47,8
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,35,245,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,251,19
CONTROL "Capture &system audio",IDC_CAPTURE_SYSTEM_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,161,83,10
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
END
SNIP DIALOGEX 0, 0, 260, 68
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,246,19
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,230,19
END
DEMOTYPE DIALOGEX 0, 0, 259, 249
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
DEMOTYPE DIALOGEX 0, 0, 260, 249
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,211,137,16,13
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,230,10
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
EDITTEXT IDC_DEMOTYPE_FILE,44,137,187,12,ES_AUTOHSCROLL | ES_READONLY
EDITTEXT IDC_DEMOTYPE_FILE,44,137,167,12,ES_AUTOHSCROLL | ES_READONLY
LTEXT "Input file:",IDC_STATIC,7,139,32,8
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,248,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,248,24
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,212,11
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,248,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,248,16
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,178,8
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,211,8
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,230,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,230,24
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,218,11
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,230,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,230,16
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,210,8
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,210,8
END
IDD_VIDEO_TRIM DIALOGEX 0, 0, 521, 380
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
CAPTION "ZoomIt Video Trim"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "",IDC_TRIM_DURATION_LABEL,12,267,160,8
CONTROL "",IDC_TRIM_PREVIEW,"Static",SS_OWNERDRAW | SS_NOTIFY,12,12,498,244
CTEXT "00:00.000",IDC_TRIM_POSITION_LABEL,155,267,200,8
CONTROL "",IDC_TRIM_TIMELINE,"Static",SS_OWNERDRAW | SS_NOTIFY,11,277,498,47,WS_EX_TRANSPARENT
CONTROL "",IDC_TRIM_SKIP_START,"Button",BS_OWNERDRAW | WS_TABSTOP,183,327,30,26
CONTROL "",IDC_TRIM_REWIND,"Button",BS_OWNERDRAW | WS_TABSTOP,215,327,30,26
CONTROL "",IDC_TRIM_PLAY_PAUSE,"Button",BS_OWNERDRAW | WS_TABSTOP,247,325,44,32
CONTROL "",IDC_TRIM_FORWARD,"Button",BS_OWNERDRAW | WS_TABSTOP,293,327,30,26
CONTROL "",IDC_TRIM_SKIP_END,"Button",BS_OWNERDRAW | WS_TABSTOP,325,327,30,26
CONTROL "",IDC_TRIM_VOLUME_ICON,"Static",SS_OWNERDRAW | SS_NOTIFY,365,334,14,12
CONTROL "",IDC_TRIM_VOLUME,"msctls_trackbar32",TBS_NOTICKS | WS_TABSTOP,380,333,70,14
DEFPUSHBUTTON "OK",IDOK,404,358,50,14
PUSHBUTTON "Cancel",IDCANCEL,458,358,50,14
END
@@ -327,7 +347,7 @@ GUIDELINES DESIGNINFO
BEGIN
"OPTIONS", DIALOG
BEGIN
RIGHTMARGIN, 273
RIGHTMARGIN, 293
BOTTOMMARGIN, 320
END
@@ -340,7 +360,6 @@ BEGIN
"ZOOM", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 151
END
@@ -348,7 +367,6 @@ BEGIN
"DRAW", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 221
END
@@ -356,7 +374,6 @@ BEGIN
"TYPE", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 97
END
@@ -364,7 +381,6 @@ BEGIN
"BREAK", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 116
END
@@ -378,7 +394,6 @@ BEGIN
"LIVEZOOM", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 127
END
@@ -386,7 +401,6 @@ BEGIN
"RECORD", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 164
END
@@ -394,7 +408,6 @@ BEGIN
"SNIP", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 61
END
@@ -402,10 +415,13 @@ BEGIN
"DEMOTYPE", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 255
TOPMARGIN, 7
BOTTOMMARGIN, 205
END
IDD_VIDEO_TRIM, DIALOG
BEGIN
END
END
#endif // APSTUDIO_INVOKED
@@ -474,6 +490,11 @@ BEGIN
0
END
IDD_VIDEO_TRIM AFX_DIALOG_LAYOUT
BEGIN
0
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View File

@@ -216,6 +216,14 @@
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
</ClCompile>
<ClCompile Include="LoopbackCapture.cpp">
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
</ClCompile>
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\dll.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
@@ -293,6 +301,7 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="AudioSampleGenerator.h" />
<ClInclude Include="LoopbackCapture.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />

View File

@@ -33,6 +33,9 @@
<ClCompile Include="AudioSampleGenerator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoopbackCapture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DemoType.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -80,6 +83,9 @@
<ClInclude Include="AudioSampleGenerator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoopbackCapture.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DemoType.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -49,8 +49,15 @@ DWORD g_RecordScaling = 100;
DWORD g_RecordScalingGIF = 50;
DWORD g_RecordScalingMP4 = 100;
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
BOOLEAN g_CaptureSystemAudio = TRUE;
BOOLEAN g_CaptureAudio = FALSE;
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
TCHAR g_RecordingSaveLocationBuffer[MAX_PATH] = {0};
TCHAR g_ScreenshotSaveLocationBuffer[MAX_PATH] = {0};
DWORD g_ThemeOverride = 2; // 0=light, 1=dark, 2=system default
DWORD g_TrimDialogWidth = 0; // 0 means use default; stored in screen pixels
DWORD g_TrimDialogHeight = 0; // 0 means use default; stored in screen pixels
DWORD g_TrimDialogVolume = 70; // 0-100 volume level for trim dialog preview
REG_SETTING RegSettings[] = {
{ L"ToggleKey", SETTING_TYPE_DWORD, 0, &g_ToggleKey, static_cast<DOUBLE>(g_ToggleKey) },
@@ -91,6 +98,13 @@ REG_SETTING RegSettings[] = {
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
{ L"CaptureSystemAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureSystemAudio, static_cast<DOUBLE>(g_CaptureSystemAudio) },
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
{ L"RecordingSaveLocation", SETTING_TYPE_STRING, sizeof(g_RecordingSaveLocationBuffer), g_RecordingSaveLocationBuffer, static_cast<DOUBLE>(0) },
{ L"ScreenshotSaveLocation", SETTING_TYPE_STRING, sizeof(g_ScreenshotSaveLocationBuffer), g_ScreenshotSaveLocationBuffer, static_cast<DOUBLE>(0) },
{ L"Theme", SETTING_TYPE_DWORD, 0, &g_ThemeOverride, static_cast<DOUBLE>(g_ThemeOverride) },
{ L"TrimDialogWidth", SETTING_TYPE_DWORD, 0, &g_TrimDialogWidth, static_cast<DOUBLE>(0) },
{ L"TrimDialogHeight", SETTING_TYPE_DWORD, 0, &g_TrimDialogHeight, static_cast<DOUBLE>(0) },
{ L"TrimDialogVolume", SETTING_TYPE_DWORD, 0, &g_TrimDialogVolume, static_cast<DOUBLE>(g_TrimDialogVolume) },
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }
};

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,10 @@
#include <shlobj.h>
#include <tchar.h>
#include <wincodec.h>
#include <shcore.h>
#include <magnification.h>
#include <Uxtheme.h>
#include <vssym32.h>
#include <math.h>
#include <shellapi.h>
#include <shlwapi.h>
@@ -41,12 +43,15 @@
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Core.h>
#include <winrt/Windows.Media.Editing.h>
#include <winrt/Windows.Media.Playback.h>
#include <winrt/Windows.Media.Transcoding.h>
#include <winrt/Windows.Media.MediaProperties.h>
#include <winrt/Windows.Media.Devices.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Storage.FileProperties.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <filesystem>
@@ -69,6 +74,9 @@
#include <d3d11_4.h>
#include <dxgi1_6.h>
#include <d2d1_3.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
// STL

View File

@@ -12,6 +12,7 @@
// Non-localizable
//////////////////////////////
#define IDC_AUDIO 117
#define IDD_VIDEO_TRIM 119
#define IDC_LINK 1000
#define IDC_ALT 1001
#define IDC_CTRL 1002
@@ -94,9 +95,22 @@
#define IDC_DEMOTYPE_STATIC2 1074
#define IDC_COPYRIGHT 1075
#define IDC_RECORD_FORMAT 1076
#define IDC_TRIM_POSITION_LABEL 1087
#define IDC_TRIM_PREVIEW 1088
#define IDC_TRIM_TIMELINE 1089
#define IDC_TRIM_PLAY_PAUSE 1090
#define IDC_TRIM_REWIND 1091
#define IDC_TRIM_FORWARD 1092
#define IDC_TRIM_DURATION_LABEL 1094
#define IDC_TRIM_SKIP_START 1095
#define IDC_TRIM_SKIP_END 1096
#define IDC_TRIM_VOLUME 1097
#define IDC_TRIM_VOLUME_ICON 1098
#define IDC_PEN_WIDTH 1105
#define IDC_TIMER 1106
#define IDC_SMOOTH_IMAGE 1107
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
#define IDC_MICROPHONE_LABEL 1109
#define IDC_SAVE 40002
#define IDC_COPY 40004
#define IDC_RECORD 40006
@@ -109,9 +123,9 @@
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 118
#define _APS_NEXT_RESOURCE_VALUE 120
#define _APS_NEXT_COMMAND_VALUE 40013
#define _APS_NEXT_CONTROL_VALUE 1078
#define _APS_NEXT_CONTROL_VALUE 1099
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -7,3 +7,4 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DestroyMenuSafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.FreeLibrarySafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.UnhookWindowsHookExSafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DeleteObjectSafeHandle")]

View File

@@ -89,6 +89,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public StringProperty RecordFormat { get; set; }
public BoolProperty CaptureSystemAudio { get; set; }
public BoolProperty CaptureAudio { get; set; }
public StringProperty MicrophoneDeviceId { get; set; }

View File

@@ -285,6 +285,9 @@
<ComboBoxItem>MP4</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureSystemAudio" ContentAlignment="Left">
<CheckBox x:Uid="ZoomIt_Record_CaptureSystemAudio" IsChecked="{x:Bind ViewModel.RecordCaptureSystemAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" ContentAlignment="Left">
<CheckBox x:Uid="ZoomIt_Record_CaptureAudio" IsChecked="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -4834,9 +4834,9 @@ Activate by holding the key for the character you want to add an accent to, then
<value>Zoom in or out to enlarge content and make details clearer.</value>
</data>
<data name="ZoomIt_ZoomFAQ.Text" xml:space="preserve">
<value>Press **the mouse wheel** or **the Up / Down arrow keys** to zoom in or out.
Press **Esc** or **the right mouse button** to exit zoom mode.
Press **Ctrl + C** to capture the zoomed view, or **Ctrl + S** to save it.
<value>Press **the mouse wheel** or **the Up / Down arrow keys** to zoom in or out.
Press **Esc** or **the right mouse button** to exit zoom mode.
Press **Ctrl + C** to capture the zoomed view, or **Ctrl + S** to save it.
Press **Ctrl + Shift** to crop before copying or saving.</value>
</data>
<data name="ZoomIt_Zoom_Shortcut.Header" xml:space="preserve">
@@ -4864,23 +4864,23 @@ Press **Ctrl + Shift** to crop before copying or saving.</value>
<value>Draw</value>
</data>
<data name="ZoomIt_DrawFAQ.Text" xml:space="preserve">
<value>Press **the left mouse button** to toggle drawing mode when zoomed in, and **the right mouse button** to exit.
<value>Press **the left mouse button** to toggle drawing mode when zoomed in, and **the right mouse button** to exit.
Press **Ctrl + Z** to undo, **E** to clear drawings, and **Space** to center the cursor.
**Pen control**
**Pen control**
Press **Ctrl + the mouse wheel** or **Ctrl + Up / Down** to adjust the pen width.
**Colors**
**Colors**
Press **R** (Red), **G** (Green), **B** (Blue), **O** (Orange), **Y** (Yellow), or **P** (Pink) to switch colors.
**Highlight and blur**
**Highlight and blur**
Press **Shift + a color key** for a translucent highlighter, **X** for blur, or **Shift + X** for a stronger blur.
**Shapes**
**Shapes**
Press **Shift** for a line, **Ctrl** for a rectangle, **Tab** for an ellipse, or **Shift + Ctrl** for an arrow.
**Screen**
Press **W** or **K** for a white or black sketch pad.
**Screen**
Press **W** or **K** for a white or black sketch pad.
Press **Ctrl + C** to copy or **Ctrl + S** to save, and **Ctrl + Shift** to crop.
</value>
</data>
@@ -4907,16 +4907,16 @@ Press **Ctrl + C** to copy or **Ctrl + S** to save, and **Ctrl + Shift** to crop
<value>Insert predefined text snippets with a shortcut using a text file.</value>
</data>
<data name="ZoomIt_DemoTypeFAQ" xml:space="preserve">
<value>Text can be pulled from the clipboard when it starts with **[start]**.
Use **[end]** to separate snippets, **[pause:n]** to insert pauses (in seconds), and **[paste]** / **[/paste]** to send clipboard text.
<value>Text can be pulled from the clipboard when it starts with **[start]**.
Use **[end]** to separate snippets, **[pause:n]** to insert pauses (in seconds), and **[paste]** / **[/paste]** to send clipboard text.
Use **[enter]**, **[up]**, **[down]**, **[left]**, and **[right]** to issue keystrokes.
ZoomIt can send text automatically or run in manual mode. Keyboard input is blocked while text is being sent.
In manual mode, press **Space** to unblock keyboard input at the end of a snippet.
In manual mode, press **Space** to unblock keyboard input at the end of a snippet.
In auto mode, control returns automatically after completion.
At the end of the file, ZoomIt reloads the file and restarts from the beginning.
At the end of the file, ZoomIt reloads the file and restarts from the beginning.
Press the hotkey with **Shift** in the opposite mode to step back to the previous **[end]** marker.
Press **{0}** to reset DemoType and start from the beginning.</value>
@@ -4952,12 +4952,12 @@ Press **{0}** to reset DemoType and start from the beginning.</value>
<value>Displays a countdown overlay for timed breaks or presentations.</value>
</data>
<data name="ZoomIt_BreakFAQ.Text" xml:space="preserve">
<value>Enter timer mode from the ZoomIt tray icons Break menu.
<value>Enter timer mode from the ZoomIt tray icons Break menu.
Press **the arrow keys** to adjust the time. If the timer window loses focus through **Alt + Tab**, press **the left mouse button** on the ZoomIt tray icon to reactivate it.
Press **Esc** to exit timer mode.
Change the break timer color using the same keys as the drawing colors.
Change the break timer color using the same keys as the drawing colors.
The break timer font matches the text font.</value>
</data>
<data name="ZoomIt_Break_Shortcut.Header" xml:space="preserve">
@@ -5098,6 +5098,9 @@ The break timer font matches the text font.</value>
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
<value>Format</value>
</data>
<data name="ZoomIt_Record_CaptureSystemAudio.Content" xml:space="preserve">
<value>Capture system audio</value>
</data>
<data name="ZoomIt_Record_CaptureAudio.Content" xml:space="preserve">
<value>Capture audio input</value>
</data>
@@ -6167,14 +6170,14 @@ The break timer font matches the text font.</value>
</data>
<data name="ZoomIt_LiveZoom_Shortcut_Draw" xml:space="preserve">
<value>Press **{0}** to activate live drawing and **Esc** to clear annotations or to exit.
Press **Ctrl + Up / Down** to adjust the zoom level.
</value>
</data>
<data name="ZoomIt_TypeFAQ.Text" xml:space="preserve">
<value>Press **T** to switch to typing when drawing mode is active, and **Shift** for right-aligned text.
Press **Esc** or **the left mouse button** to exit typing mode.
Press **the mouse wheel** or **Up / Down** to adjust the font size.
<value>Press **T** to switch to typing when drawing mode is active, and **Shift** for right-aligned text.
Press **Esc** or **the left mouse button** to exit typing mode.
Press **the mouse wheel** or **Up / Down** to adjust the font size.
Text uses the current drawing color.</value>
</data>
<data name="ZoomIt_DrawGroup.Description" xml:space="preserve">

View File

@@ -850,6 +850,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool RecordCaptureSystemAudio
{
get => _zoomItSettings.Properties.CaptureSystemAudio.Value;
set
{
if (_zoomItSettings.Properties.CaptureSystemAudio.Value != value)
{
_zoomItSettings.Properties.CaptureSystemAudio.Value = value;
OnPropertyChanged(nameof(RecordCaptureSystemAudio));
NotifySettingsChanged();
}
}
}
public bool RecordCaptureAudio
{
get => _zoomItSettings.Properties.CaptureAudio.Value;