mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-03 17:10:14 +02:00
Compare commits
198 Commits
crutkas/de
...
shortcutgu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad8d00cda | ||
|
|
1b71d64ef8 | ||
|
|
111d5b387e | ||
|
|
47d348f4ec | ||
|
|
4a71f79901 | ||
|
|
79f69b87f2 | ||
|
|
6a0ff6a131 | ||
|
|
9123365993 | ||
|
|
82e4705ed6 | ||
|
|
cffddcdcb7 | ||
|
|
e7b6e36987 | ||
|
|
c07a3d9bd9 | ||
|
|
dcfcd0a699 | ||
|
|
7da07303b3 | ||
|
|
97d3b99bc5 | ||
|
|
e0409b0271 | ||
|
|
b58803642d | ||
|
|
87c91c5a2a | ||
|
|
7716010a38 | ||
|
|
e77e7dfe68 | ||
|
|
7aae7343aa | ||
|
|
bf8493225d | ||
|
|
12eddcb320 | ||
|
|
9473657670 | ||
|
|
9948a2831f | ||
|
|
e746675ebf | ||
|
|
c3ec5b529b | ||
|
|
6a8e439847 | ||
|
|
36c30ae2fd | ||
|
|
d09408ce43 | ||
|
|
ab79ae7477 | ||
|
|
b6eb027558 | ||
|
|
999ba28e34 | ||
|
|
03ff49601d | ||
|
|
406b42db64 | ||
|
|
8d4a24cd3f | ||
|
|
0a20b7efb5 | ||
|
|
bf18e7815b | ||
|
|
43b37bc733 | ||
|
|
c011b2223c | ||
|
|
019ee046da | ||
|
|
e8579c39d0 | ||
|
|
911b39256c | ||
|
|
250a8cb5b7 | ||
|
|
9f9d221eab | ||
|
|
b9c5c15be2 | ||
|
|
3a4f9c2274 | ||
|
|
a51fe19f82 | ||
|
|
efc814a610 | ||
|
|
1d1ae0d191 | ||
|
|
3df4b45849 | ||
|
|
88357a5a99 | ||
|
|
b4773affa7 | ||
|
|
e82c2d20cb | ||
|
|
e3a1f97ef3 | ||
|
|
b26ded5370 | ||
|
|
2e8ad4827b | ||
|
|
877626ef45 | ||
|
|
e0f72df36c | ||
|
|
552b02d596 | ||
|
|
c184acbada | ||
|
|
55038c3c5e | ||
|
|
f537c43139 | ||
|
|
f867323677 | ||
|
|
d58145eb8c | ||
|
|
486bec0ebd | ||
|
|
e76506dffd | ||
|
|
841a5c5555 | ||
|
|
f646e0328e | ||
|
|
5054a776dd | ||
|
|
9d480c8e2c | ||
|
|
d652285a81 | ||
|
|
200afb5c4b | ||
|
|
e7582ebd6a | ||
|
|
b9c1181d9f | ||
|
|
4aff3418e4 | ||
|
|
e53e1b4376 | ||
|
|
73f718c233 | ||
|
|
0917a64e7d | ||
|
|
a764bf3e0c | ||
|
|
4853bd0345 | ||
|
|
dceb1d7730 | ||
|
|
72be09554e | ||
|
|
19f95066c3 | ||
|
|
bb16ae1709 | ||
|
|
dc0877ebe5 | ||
|
|
cd844e3889 | ||
|
|
ac789a7fbe | ||
|
|
3b7df37ac2 | ||
|
|
387b7e9795 | ||
|
|
b553addcdd | ||
|
|
5c11c751fe | ||
|
|
6d7d5f9cde | ||
|
|
4cb9c53809 | ||
|
|
84d4cbb16d | ||
|
|
97cba618da | ||
|
|
145247c4fb | ||
|
|
84ab12027b | ||
|
|
3458d01d4c | ||
|
|
2e6f80f944 | ||
|
|
f8cc513f9c | ||
|
|
b1d5233622 | ||
|
|
68b7b4183f | ||
|
|
68a10d0488 | ||
|
|
e70ca56e9d | ||
|
|
16c4a56ca1 | ||
|
|
0d5c85a00d | ||
|
|
509ad636fe | ||
|
|
26f76105d4 | ||
|
|
639b29eb8c | ||
|
|
bff3874b5f | ||
|
|
eff58e1df5 | ||
|
|
411f4df2c0 | ||
|
|
7dc8c1000b | ||
|
|
48d8e33375 | ||
|
|
afc27e873f | ||
|
|
3302e61d72 | ||
|
|
e6edca93e7 | ||
|
|
7acab452d5 | ||
|
|
0a07811233 | ||
|
|
9ecf82d2ea | ||
|
|
44d12c6e63 | ||
|
|
6b8a3e65f7 | ||
|
|
2b16068a7d | ||
|
|
440e75184a | ||
|
|
acf510dff5 | ||
|
|
ddd090cc81 | ||
|
|
2f4766df19 | ||
|
|
6558260c53 | ||
|
|
55b3e15f10 | ||
|
|
69c6475e15 | ||
|
|
8e7be164a9 | ||
|
|
f42b3922c7 | ||
|
|
d568d16560 | ||
|
|
0abae1d190 | ||
|
|
271e0c0533 | ||
|
|
6bd5c4c811 | ||
|
|
a41be807a4 | ||
|
|
e11626550e | ||
|
|
7266745124 | ||
|
|
77a5bc2ff5 | ||
|
|
0b6683eb34 | ||
|
|
3796fdb706 | ||
|
|
2d12932e44 | ||
|
|
1da76e55bb | ||
|
|
3c1a6a5b16 | ||
|
|
3b77feb879 | ||
|
|
7e7bb04d48 | ||
|
|
273b50cb16 | ||
|
|
498c8d534f | ||
|
|
0e2f466454 | ||
|
|
1982f2615d | ||
|
|
2f89281178 | ||
|
|
56f056e492 | ||
|
|
71dd8fe83f | ||
|
|
f9183af53d | ||
|
|
0f85f8bad6 | ||
|
|
e0e7bf4df2 | ||
|
|
b12fcf6699 | ||
|
|
2ee02c4bbe | ||
|
|
a306797d21 | ||
|
|
7e50caa04e | ||
|
|
e3b1ec356e | ||
|
|
fa54b49fca | ||
|
|
7e2fc4481d | ||
|
|
201a27d2bb | ||
|
|
fe3d481407 | ||
|
|
3475c92f32 | ||
|
|
82b0ca71fd | ||
|
|
3a8431ae9d | ||
|
|
eed6dc6f8d | ||
|
|
038cd23423 | ||
|
|
be799ddd82 | ||
|
|
e26bf2acd6 | ||
|
|
95e0a20444 | ||
|
|
3e75fb0c52 | ||
|
|
ba4098960c | ||
|
|
1580279be1 | ||
|
|
07760e4730 | ||
|
|
d73ab4f2d3 | ||
|
|
a6b761433e | ||
|
|
da77396da5 | ||
|
|
46df48684d | ||
|
|
7596e965ef | ||
|
|
0b823ea8bd | ||
|
|
54d176c4c4 | ||
|
|
569f07268b | ||
|
|
7ce5182695 | ||
|
|
b16b4579fa | ||
|
|
a292a92f4d | ||
|
|
6952deb4ae | ||
|
|
da7b789bfe | ||
|
|
a14c458f19 | ||
|
|
22dc870991 | ||
|
|
d5f5500347 | ||
|
|
c81a423880 | ||
|
|
7a6189ba3e | ||
|
|
03587ae800 |
8
.github/actions/spell-check/allow/code.txt
vendored
8
.github/actions/spell-check/allow/code.txt
vendored
@@ -308,11 +308,8 @@ pwa
|
||||
|
||||
AOT
|
||||
Aot
|
||||
cswinrt
|
||||
ify
|
||||
rsp
|
||||
TFM
|
||||
RTIID
|
||||
|
||||
# YML
|
||||
onefuzz
|
||||
@@ -368,10 +365,7 @@ FILESYSONLY
|
||||
URLIS
|
||||
WAITTIMEOUT
|
||||
DEFAULTTONEAREST
|
||||
DWRITE
|
||||
LWIN
|
||||
VCENTER
|
||||
VREDRAW
|
||||
|
||||
|
||||
# COM/WinRT interface prefixes and type fragments
|
||||
BAlt
|
||||
|
||||
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -954,7 +954,6 @@ keynum
|
||||
keyremaps
|
||||
keyring
|
||||
keyvault
|
||||
kfull
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
@@ -1062,7 +1061,6 @@ lstrcmpi
|
||||
lstrcpyn
|
||||
lstrlen
|
||||
LTEXT
|
||||
LTM
|
||||
LTRREADING
|
||||
luid
|
||||
LUMA
|
||||
|
||||
@@ -391,7 +391,6 @@
|
||||
"WinUI3Apps\\Google.Apis.Auth.dll",
|
||||
"WinUI3Apps\\Google.Apis.Core.dll",
|
||||
"WinUI3Apps\\Google.GenAI.dll",
|
||||
"WinUI3Apps\\YamlDotNet.dll",
|
||||
|
||||
"boost_regex-vc143-mt-gd-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-gd-x64-1_87.dll",
|
||||
|
||||
@@ -48,11 +48,6 @@ foreach ($csprojFile in $csprojFilesArray) {
|
||||
continue
|
||||
}
|
||||
|
||||
# The PowerAccent.Common project does not target WinRT, so skip it
|
||||
if ($csprojFile -like '*PowerAccent.Common.csproj') {
|
||||
continue
|
||||
}
|
||||
|
||||
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
|
||||
if (!$importExists) {
|
||||
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
<Folder Name="/common/interop/">
|
||||
@@ -802,14 +801,6 @@
|
||||
<Project Path="src/modules/peek/peek/peek.vcxproj" Id="a1425b53-3d61-4679-8623-e64a0d3d0a48" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerAccent/">
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Common.UnitTests/PowerAccent.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Common/PowerAccent.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -1142,5 +1133,3 @@
|
||||
<Project Path="src/Update/PowerToys.Update.vcxproj" Id="44ce9ae1-4390-42c5-bacc-0fd6b40aa203" />
|
||||
<Project Path="tools/project_template/ModuleTemplate/ModuleTemplateCompileTest.vcxproj" Id="64a80062-4d8b-4229-8a38-dfa1d7497749" />
|
||||
</Solution>
|
||||
|
||||
|
||||
|
||||
2
deps/spdlog
vendored
2
deps/spdlog
vendored
Submodule deps/spdlog updated: 79524ddd08...616866fcf4
94
deps/spdlog-msvc-fix/include/spdlog-msvc-fix.h
vendored
Normal file
94
deps/spdlog-msvc-fix/include/spdlog-msvc-fix.h
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
// spdlog-msvc-fix.h
|
||||
//
|
||||
// Workaround for MSVC 14.51 (compiler version 19.51, _MSC_VER >= 1951) removing
|
||||
// stdext::checked_array_iterator. Force-included for all spdlog consumers via
|
||||
// deps/spdlog.props, because spdlog v1.8.5's bundled fmt format.h(357) still
|
||||
// references this type inside #if defined(_SECURE_SCL) && _SECURE_SCL -- a
|
||||
// branch entered in Debug builds where _ITERATOR_DEBUG_LEVEL > 0.
|
||||
//
|
||||
// On MSVC 14.50 and earlier, the type still exists in <iterator>, so this shim
|
||||
// is a no-op via the _MSC_VER guard. On MSVC 14.51+, it provides a minimal
|
||||
// pointer-backed substitute that satisfies the bundled fmt's usage:
|
||||
//
|
||||
// template <typename T> using checked_ptr = stdext::checked_array_iterator<T*>;
|
||||
// template <typename T> checked_ptr<T> make_checked(T* p, size_t size) {
|
||||
// return {p, size};
|
||||
// }
|
||||
// ... return make_checked(get_data(c) + size, n);
|
||||
//
|
||||
// When deps/spdlog is bumped past v1.14 (which ships fmt 10.2 and drops this
|
||||
// dependency), this shim and its <ForcedIncludeFiles> entry in deps/spdlog.props
|
||||
// can be deleted.
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(__cplusplus) && defined(_MSC_VER) && _MSC_VER >= 1951
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <type_traits>
|
||||
|
||||
namespace stdext
|
||||
{
|
||||
template <typename _Ptr>
|
||||
class checked_array_iterator
|
||||
{
|
||||
_Ptr _Myarray = nullptr;
|
||||
std::size_t _Mysize = 0;
|
||||
std::size_t _Myindex = 0;
|
||||
|
||||
public:
|
||||
using iterator_category = std::random_access_iterator_tag;
|
||||
using value_type = std::remove_cv_t<std::remove_pointer_t<_Ptr>>;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = _Ptr;
|
||||
using reference = std::remove_pointer_t<_Ptr>&;
|
||||
|
||||
constexpr checked_array_iterator() = default;
|
||||
|
||||
constexpr checked_array_iterator(_Ptr arr, std::size_t size, std::size_t idx = 0) noexcept
|
||||
: _Myarray(arr), _Mysize(size), _Myindex(idx)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr reference operator*() const noexcept { return _Myarray[_Myindex]; }
|
||||
constexpr pointer operator->() const noexcept { return _Myarray + _Myindex; }
|
||||
constexpr reference operator[](difference_type n) const noexcept
|
||||
{
|
||||
return _Myarray[_Myindex + static_cast<std::size_t>(n)];
|
||||
}
|
||||
|
||||
constexpr checked_array_iterator& operator++() noexcept { ++_Myindex; return *this; }
|
||||
constexpr checked_array_iterator operator++(int) noexcept { auto t = *this; ++_Myindex; return t; }
|
||||
constexpr checked_array_iterator& operator--() noexcept { --_Myindex; return *this; }
|
||||
constexpr checked_array_iterator operator--(int) noexcept { auto t = *this; --_Myindex; return t; }
|
||||
|
||||
constexpr checked_array_iterator& operator+=(difference_type n) noexcept
|
||||
{
|
||||
_Myindex = static_cast<std::size_t>(static_cast<difference_type>(_Myindex) + n);
|
||||
return *this;
|
||||
}
|
||||
constexpr checked_array_iterator& operator-=(difference_type n) noexcept
|
||||
{
|
||||
_Myindex = static_cast<std::size_t>(static_cast<difference_type>(_Myindex) - n);
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend constexpr checked_array_iterator operator+(checked_array_iterator it, difference_type n) noexcept { it += n; return it; }
|
||||
friend constexpr checked_array_iterator operator+(difference_type n, checked_array_iterator it) noexcept { return it + n; }
|
||||
friend constexpr checked_array_iterator operator-(checked_array_iterator it, difference_type n) noexcept { it -= n; return it; }
|
||||
friend constexpr difference_type operator-(checked_array_iterator a, checked_array_iterator b) noexcept
|
||||
{
|
||||
return static_cast<difference_type>(a._Myindex) - static_cast<difference_type>(b._Myindex);
|
||||
}
|
||||
|
||||
friend constexpr bool operator==(checked_array_iterator a, checked_array_iterator b) noexcept { return a._Myindex == b._Myindex; }
|
||||
friend constexpr bool operator!=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(a == b); }
|
||||
friend constexpr bool operator<(checked_array_iterator a, checked_array_iterator b) noexcept { return a._Myindex < b._Myindex; }
|
||||
friend constexpr bool operator>(checked_array_iterator a, checked_array_iterator b) noexcept { return b < a; }
|
||||
friend constexpr bool operator<=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(b < a); }
|
||||
friend constexpr bool operator>=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(a < b); }
|
||||
};
|
||||
} // namespace stdext
|
||||
|
||||
#endif // __cplusplus && _MSC_VER >= 1951
|
||||
3
deps/spdlog.props
vendored
3
deps/spdlog.props
vendored
@@ -2,7 +2,8 @@
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)spdlog\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ForcedIncludeFiles>$(MSBuildThisFileDirectory)spdlog-msvc-fix\include\spdlog-msvc-fix.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,17 +12,6 @@
|
||||
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Opt out of CsWinRT 2.2 IIDOptimizer. On .NET 10 / CsWinRT 2.2 the tool exits with code -1
|
||||
after producing "0 IID calculations/fetches patched", which generates a noisy MSB3073
|
||||
warning for every CsWinRT-consuming project. The IIDOptimizer is a runtime-perf optimization
|
||||
that interns GUID lookups; disabling it just means a small first-call cost. This switch
|
||||
causes Microsoft.Windows.CsWinRT.IIDOptimizer.targets to not be imported at all.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIIDOptimizerOptOut>true</CsWinRTIIDOptimizerOptOut>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Common from the debug / release items -->
|
||||
<PropertyGroup>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
@@ -52,54 +41,7 @@
|
||||
<!-- this may need to be removed on future CsWinRT upgrades-->
|
||||
<Target Name="RemoveCsWinRTPackageAnalyzer" BeforeTargets="CoreCompile">
|
||||
<ItemGroup>
|
||||
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
|
||||
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!--
|
||||
Ensure any referenced C++/WinRT (.vcxproj) projects are fully built BEFORE the CsWinRT
|
||||
source generator runs in this csproj. On a clean machine the SDK-style ProjectReference
|
||||
graph does not guarantee that the producing vcxproj has emitted its .winmd before the
|
||||
consuming C# Compile / source-generator stage starts in a parallel solution build,
|
||||
which manifests as CS0246 on the projected namespace (e.g. 'PowerToys.Interop').
|
||||
Forcing a serialized Build of the .vcxproj references here closes that race.
|
||||
|
||||
We hook BEFORE ResolveProjectReferences so the produced .winmd is visible to
|
||||
CsWinRTRemoveWinMDReferences (which moves it into @(CsWinRTInputs)) and we also
|
||||
delete a possibly stale cswinrt.rsp so CsWinRTGenerateProjection re-invokes
|
||||
cswinrt.exe instead of incrementally skipping.
|
||||
-->
|
||||
<Target Name="EnsureNativeWinMDProjectionInputsBuilt"
|
||||
BeforeTargets="ResolveProjectReferences;ResolveAssemblyReferences;CsWinRTPrepareProjection;CsWinRTGenerateProjection"
|
||||
Condition="'@(ProjectReference)' != '' and '$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' != 'false'">
|
||||
<ItemGroup>
|
||||
<_NativeWinMDProjectionRef Include="@(ProjectReference)" Condition="'%(Extension)' == '.vcxproj'" />
|
||||
</ItemGroup>
|
||||
<MSBuild Projects="@(_NativeWinMDProjectionRef)"
|
||||
Properties="Configuration=$(Configuration);Platform=$(Platform)"
|
||||
Targets="Build"
|
||||
BuildInParallel="false"
|
||||
Condition="'@(_NativeWinMDProjectionRef)' != ''" />
|
||||
<!-- Force CsWinRTGenerateProjection to re-run so stale-rsp incremental skip cannot
|
||||
leave us without generated .cs files when the .winmd has just been (re)produced. -->
|
||||
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp"
|
||||
Condition="'@(_NativeWinMDProjectionRef)' != '' and '$(CsWinRTGeneratedFilesDir)' != ''" />
|
||||
<!-- Mark that we need to delete the rsp again once CsWinRTGeneratedFilesDir is fully resolved
|
||||
(some projects set it to $(OutDir) which is not evaluated this early). -->
|
||||
<PropertyGroup>
|
||||
<_DeleteStaleCsWinRTRspPending>true</_DeleteStaleCsWinRTRspPending>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
||||
<!--
|
||||
Second pass: after CsWinRTPrepareProjection has resolved $(CsWinRTGeneratedFilesDir) to its
|
||||
final value (which may depend on $(OutDir)), delete any stale cswinrt.rsp so the
|
||||
CsWinRTGenerateProjection target's incremental-skip cannot leave us without generated .cs files.
|
||||
-->
|
||||
<Target Name="DeleteStaleCsWinRTRspAfterPrepare"
|
||||
AfterTargets="CsWinRTPrepareProjection"
|
||||
BeforeTargets="CsWinRTGenerateProjection"
|
||||
Condition="'$(_DeleteStaleCsWinRTRspPending)' == 'true' and '$(CsWinRTGeneratedFilesDir)' != ''">
|
||||
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
#include <common/updating/updating.h>
|
||||
#include <common/updating/updateState.h>
|
||||
#include <common/updating/installer.h>
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/HttpClient.h>
|
||||
@@ -23,8 +21,6 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
@@ -40,59 +36,17 @@ using namespace cmdArg;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void CleanupStaleTempUpdaters()
|
||||
{
|
||||
// Remove orphaned PowerToys.Update.*.exe files from previous runs
|
||||
try
|
||||
{
|
||||
std::error_code ec;
|
||||
const auto tempDir = fs::temp_directory_path();
|
||||
for (const auto& entry : fs::directory_iterator(tempDir, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto filename = entry.path().filename().wstring();
|
||||
if (filename.starts_with(L"PowerToys.Update.") && filename.ends_with(L".exe"))
|
||||
{
|
||||
// Skip our own file (current PID)
|
||||
const auto ownFilename = L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe";
|
||||
if (filename == ownFilename)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::remove(entry.path(), ec);
|
||||
// Failure to delete is expected if another updater is still running
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Best-effort cleanup; don't block the update
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<fs::path> CopySelfToTempDir()
|
||||
{
|
||||
CleanupStaleTempUpdaters();
|
||||
|
||||
std::error_code error;
|
||||
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
|
||||
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
|
||||
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
|
||||
if (error)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return dst_path;
|
||||
return std::move(dst_path);
|
||||
}
|
||||
|
||||
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
@@ -103,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
|
||||
auto state = UpdateState::read();
|
||||
|
||||
// Handle readyToInstall first — the installer is already on disk,
|
||||
// so we don't need a GitHub API call (which may fail if offline).
|
||||
if (state.state == UpdateState::readyToInstall)
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
else if (state.state == UpdateState::readyToInstall)
|
||||
{
|
||||
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
|
||||
if (fs::is_regular_file(installer))
|
||||
@@ -118,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::upToDate)
|
||||
else if (state.state == UpdateState::upToDate)
|
||||
{
|
||||
isUpToDate = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
|
||||
// Check for error BEFORE dereferencing — the old code crashed here
|
||||
// when GitHub API was unreachable (new_version_info held an error string).
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
|
||||
Logger::error("Invoked with -update_now argument, but update state was invalid");
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -169,32 +116,13 @@ bool InstallNewVersionStage1(fs::path installer)
|
||||
|
||||
if (pt_main_window != nullptr)
|
||||
{
|
||||
// Get the process that owns the tray window so we can wait for it to exit
|
||||
DWORD ptProcessId = 0;
|
||||
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
|
||||
|
||||
// Use SendMessageTimeoutW to avoid blocking indefinitely if the
|
||||
// tray window thread is hung or unresponsive.
|
||||
DWORD_PTR result = 0;
|
||||
SendMessageTimeoutW(pt_main_window, WM_CLOSE, 0, 0, SMTO_ABORTIFHUNG, 5000, &result);
|
||||
|
||||
// Wait for PT to actually exit before launching installer.
|
||||
// Without this, the installer may find PT files locked.
|
||||
if (ptProcessId != 0)
|
||||
{
|
||||
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
|
||||
if (ptProcess)
|
||||
{
|
||||
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
|
||||
}
|
||||
}
|
||||
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
|
||||
}
|
||||
|
||||
// Pass the install directory so Stage 2 can relaunch PowerToys after install
|
||||
const std::wstring installDir = get_module_folderpath();
|
||||
|
||||
std::wstring arguments = updating::BuildStage2Arguments(
|
||||
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
|
||||
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
|
||||
arguments += L" \"";
|
||||
arguments += installer.c_str();
|
||||
arguments += L"\"";
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = copy_in_temp->c_str();
|
||||
@@ -262,16 +190,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
|
||||
if (!args || nArgs < 2)
|
||||
{
|
||||
if (args)
|
||||
{
|
||||
LocalFree(args);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// D3 fix: ensure args is freed on all exit paths
|
||||
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
|
||||
|
||||
std::wstring_view action{ args[1] };
|
||||
|
||||
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
|
||||
@@ -280,11 +201,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
|
||||
if (action == UPDATE_NOW_LAUNCH_STAGE1)
|
||||
{
|
||||
// Backup config files before the update to protect against corruption
|
||||
Logger::info("Backing up config files before update");
|
||||
auto backupResult = updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
Logger::info("Config backup complete: {} files backed up, {} errors", backupResult.filesBackedUp, backupResult.errors);
|
||||
|
||||
bool isUpToDate = false;
|
||||
auto installerPath = ObtainInstaller(isUpToDate);
|
||||
bool failed = !installerPath.has_value();
|
||||
@@ -301,12 +217,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
}
|
||||
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
|
||||
{
|
||||
if (nArgs < 3)
|
||||
{
|
||||
Logger::error("Stage 2 invoked without installer path argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
const bool failed = !InstallNewVersionStage2(args[2]);
|
||||
if (failed)
|
||||
@@ -317,39 +227,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
state.state = UpdateState::errorDownloading;
|
||||
});
|
||||
}
|
||||
|
||||
// Always check for corrupted configs after Stage 2, regardless
|
||||
// of install success/failure. A failed install may still corrupt configs.
|
||||
Logger::info("Checking for corrupted config files after update");
|
||||
auto restoreResult = updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
Logger::info("Config restore check complete: {}/{} files restored, {} errors",
|
||||
restoreResult.filesRestored, restoreResult.filesChecked, restoreResult.errors);
|
||||
|
||||
if (!failed)
|
||||
{
|
||||
// Relaunch PowerToys from the install directory
|
||||
if (updating::CanRelaunchAfterUpdate(nArgs))
|
||||
{
|
||||
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
|
||||
|
||||
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = ptExePath.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = UPDATE_REPORT_SUCCESS;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::error(L"Failed to relaunch PowerToys after update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\;..\utils;..\Telemetry;..\..\;..\..\..\deps\;..\..\..\deps\spdlog\include;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<LanguageStandard>stdcpp23</LanguageStandard>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
#pragma once
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <type_traits>
|
||||
#include "logger_settings.h"
|
||||
|
||||
// fmt 9+ no longer auto-formats enums. Provide a generic formatter that
|
||||
// converts any scoped or unscoped enum to its underlying integer type so
|
||||
// existing Logger::xxx(L"... {} ...", someEnum) call sites keep working
|
||||
// after the spdlog 1.17 / fmt 12 upgrade.
|
||||
namespace fmt
|
||||
{
|
||||
template <typename E, typename Char>
|
||||
struct formatter<E, Char, std::enable_if_t<std::is_enum_v<E>>>
|
||||
: formatter<std::underlying_type_t<E>, Char>
|
||||
{
|
||||
template <typename FormatContext>
|
||||
auto format(E value, FormatContext& ctx) const
|
||||
{
|
||||
return formatter<std::underlying_type_t<E>, Char>::format(
|
||||
static_cast<std::underlying_type_t<E>>(value), ctx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class Logger
|
||||
{
|
||||
private:
|
||||
@@ -37,44 +17,44 @@ public:
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void trace(const FormatString& formatString, const Args&... args)
|
||||
static void trace(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->trace(fmt::runtime(formatString), args...);
|
||||
logger->trace(fmt, args...);
|
||||
}
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void debug(const FormatString& formatString, const Args&... args)
|
||||
static void debug(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->debug(fmt::runtime(formatString), args...);
|
||||
logger->debug(fmt, args...);
|
||||
}
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void info(const FormatString& formatString, const Args&... args)
|
||||
static void info(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->info(fmt::runtime(formatString), args...);
|
||||
logger->info(fmt, args...);
|
||||
}
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void warn(const FormatString& formatString, const Args&... args)
|
||||
static void warn(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->warn(fmt::runtime(formatString), args...);
|
||||
logger->warn(fmt, args...);
|
||||
}
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void error(const FormatString& formatString, const Args&... args)
|
||||
static void error(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->error(fmt::runtime(formatString), args...);
|
||||
logger->error(fmt, args...);
|
||||
}
|
||||
|
||||
// log message should not be localized
|
||||
template<typename FormatString, typename... Args>
|
||||
static void critical(const FormatString& formatString, const Args&... args)
|
||||
static void critical(const FormatString& fmt, const Args&... args)
|
||||
{
|
||||
logger->critical(fmt::runtime(formatString), args...);
|
||||
logger->critical(fmt, args...);
|
||||
}
|
||||
|
||||
static void flush()
|
||||
|
||||
@@ -1,679 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace UpdatingUnitTests
|
||||
{
|
||||
// Helper to create a temp directory for test isolation.
|
||||
// Each instance gets a unique subdirectory to prevent test interference.
|
||||
class TempDir
|
||||
{
|
||||
public:
|
||||
TempDir()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH + 1];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
static std::atomic<int> counter{0};
|
||||
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
|
||||
|
||||
// Ensure clean state
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
fs::create_directories(m_path, ec);
|
||||
}
|
||||
|
||||
~TempDir()
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
}
|
||||
|
||||
const fs::path& path() const { return m_path; }
|
||||
|
||||
// Write a file with the given content
|
||||
void WriteFile(const fs::path& relativePath, const std::string& content)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(content.data(), content.size());
|
||||
}
|
||||
|
||||
// Write a file with raw bytes (including null bytes for corruption testing)
|
||||
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
// Read file content as string
|
||||
std::string ReadFile(const fs::path& relativePath)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
std::ifstream file(fullPath, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool FileExists(const fs::path& relativePath)
|
||||
{
|
||||
return fs::exists(m_path / relativePath);
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path;
|
||||
};
|
||||
|
||||
TEST_CLASS(IsJsonFileCorruptedTests)
|
||||
{
|
||||
public:
|
||||
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
|
||||
TEST_METHOD(CleanJsonFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
|
||||
TEST_METHOD(EmptyFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"empty.json", "");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
|
||||
TEST_METHOD(FileWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"corrupted.json", corrupted);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
|
||||
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
|
||||
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> allNulls(1024, '\0');
|
||||
dir.WriteFileBytes(L"workspaces.json", allNulls);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: path that does not exist returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
|
||||
TEST_METHOD(NonExistentFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
|
||||
// with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
|
||||
TEST_METHOD(LargeCleanFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string largeContent(8192, 'x');
|
||||
dir.WriteFile(L"large.json", largeContent);
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
|
||||
// chunk boundary is still detected.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
|
||||
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string content(5000, 'x');
|
||||
content[4999] = '\0';
|
||||
std::vector<char> bytes(content.begin(), content.end());
|
||||
dir.WriteFileBytes(L"sneaky.json", bytes);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(BackupConfigFilesTests)
|
||||
{
|
||||
public:
|
||||
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
|
||||
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
|
||||
// is_regular_file && extension == ".json" branch.
|
||||
// Setup: Two root-level JSON files.
|
||||
TEST_METHOD(BackupCopiesRootJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: .json files inside module subdirectories are
|
||||
// copied to ConfigBackup/<module>/.
|
||||
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
|
||||
// module directory_iterator with extension filter.
|
||||
// Setup: Root JSON + two module directories with JSON files.
|
||||
TEST_METHOD(BackupCopiesModuleJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(std::string(R"({"zones":[]})"),
|
||||
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files at root level are not copied.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
|
||||
// Setup: One JSON file + one .log file at root.
|
||||
TEST_METHOD(BackupSkipsNonJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"debug.log", "log data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
|
||||
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
|
||||
// Setup: Root JSON + Updates directory containing a file.
|
||||
TEST_METHOD(BackupSkipsUpdatesDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: running backup twice overwrites the previous
|
||||
// backup with current file content.
|
||||
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
|
||||
// copy_options::overwrite_existing.
|
||||
// Setup: Backup, modify original, backup again.
|
||||
TEST_METHOD(BackupOverwritesPreviousBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Update the original
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files inside module subdirectories
|
||||
// (e.g., FancyZones/zones.dat) should NOT be backed up.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
|
||||
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: empty root directory with no files produces
|
||||
// an empty ConfigBackup dir without errors.
|
||||
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
|
||||
TEST_METHOD(BackupEmptyRootDirSucceeds)
|
||||
{
|
||||
TempDir dir;
|
||||
// Root dir exists but has no files
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(RestoreCorruptedConfigsTests)
|
||||
{
|
||||
public:
|
||||
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
|
||||
// fs::exists + IsJsonFileCorrupted + backup integrity check.
|
||||
// Setup: Good file -> backup -> corrupt original -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedRootFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"theme":"dark"})";
|
||||
dir.WriteFile(L"settings.json", goodContent);
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the original
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"settings.json", corrupted);
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
|
||||
// moduleBackupEntry restore with integrity check.
|
||||
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedModuleFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"workspaces":[]})";
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the module file
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
|
||||
// overwritten by backup — preserves user changes made after backup.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
|
||||
// returns false, copy_file is skipped.
|
||||
// Setup: File -> backup -> modify (but keep valid) -> restore.
|
||||
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Modify original (but keep it clean JSON)
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT have been restored since it's not corrupted
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
|
||||
// restore silently does nothing (no crash, no data loss).
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
|
||||
// early return.
|
||||
// Setup: File with no prior backup.
|
||||
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
|
||||
// No backup was created - restore should silently do nothing
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
|
||||
// some corrupted and some clean, verifying selective restore.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
|
||||
// branches, selective restore based on corruption status.
|
||||
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
|
||||
TEST_METHOD(FullBackupAndRestoreRoundTrip)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Set up a realistic config structure
|
||||
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt some files (simulating #46179 scenario)
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
|
||||
// Leave FancyZones and KBM clean
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Corrupted files should be restored
|
||||
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be unchanged
|
||||
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the original file has been deleted
|
||||
// (not corrupted), restore should NOT recreate it from backup. The installer
|
||||
// may have intentionally removed obsolete config files.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
|
||||
TEST_METHOD(RestoreSkipsDeletedOriginals)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"obsolete.json", R"({"old":true})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Installer deletes the file
|
||||
std::error_code ec;
|
||||
fs::remove(dir.path() / L"obsolete.json", ec);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT be recreated
|
||||
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
|
||||
// (e.g., disk error during backup), restore should NOT copy corrupted
|
||||
// backup over the original — that would make things worse.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
|
||||
TEST_METHOD(RestoreSkipsCorruptedBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt BOTH the original AND the backup
|
||||
std::vector<char> nulls(50, '\0');
|
||||
dir.WriteFileBytes(L"settings.json", nulls);
|
||||
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Original should still be corrupted — we don't restore from bad backup
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Simulates what actually happens during a PowerToys upgrade:
|
||||
// 1. User has settings from normal use
|
||||
// 2. Updater backs up before install (Stage 1)
|
||||
// 3. Installer runs and corrupts some files (simulated)
|
||||
// 4. Updater restores corrupted files (Stage 2)
|
||||
// 5. PT relaunches and finds working configs
|
||||
TEST_CLASS(UpgradeSimulationTests)
|
||||
{
|
||||
public:
|
||||
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
|
||||
// Verifies that corrupted files are restored and clean files are untouched.
|
||||
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
|
||||
// end-to-end with 5 modules, 2 corrupted, 3 clean.
|
||||
// Setup: Realistic config structure with multiple modules.
|
||||
TEST_METHOD(SimulateUpgradeWithCorruption)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// === User's real config state before upgrade ===
|
||||
dir.WriteFile(L"settings.json",
|
||||
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json",
|
||||
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json",
|
||||
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json",
|
||||
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
|
||||
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
|
||||
R"({"machineKey":"abc123","connectToAll":true})");
|
||||
|
||||
// Non-JSON files that should be left alone
|
||||
dir.WriteFile(L"update.log", "2026-04-11 update started");
|
||||
|
||||
// === Stage 1: Backup before killing PT ===
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Verify backup was created correctly
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
|
||||
|
||||
// === Installer runs: some files get corrupted (the #46179 scenario) ===
|
||||
// Workspaces JSON filled with null bytes
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
|
||||
// Main settings partially corrupted (null bytes injected)
|
||||
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"settings.json", partialCorrupt);
|
||||
|
||||
// FancyZones, KBM, and MWB survive the install fine
|
||||
// (this is realistic - not all files get corrupted)
|
||||
|
||||
// === Stage 2: Restore after install completes ===
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// === Verify: PT relaunches and finds working configs ===
|
||||
|
||||
// Corrupted files should be restored from backup
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
|
||||
dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be untouched (not overwritten with backup)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
|
||||
dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
|
||||
dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
|
||||
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests upgrade from an old version that has fewer modules than the new version.
|
||||
// Verifies that new module configs (created by the installer) are not touched
|
||||
// by restore, while corrupted old configs are restored.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
|
||||
// has no corresponding backup entry.
|
||||
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
|
||||
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Old version had fewer modules - only settings.json
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// New installer creates new module dirs that didn't exist before
|
||||
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
|
||||
|
||||
// Old settings get corrupted during upgrade
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Old settings restored
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
|
||||
// New module settings untouched (no backup existed for them)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"enabled":true})"),
|
||||
dir.ReadFile(L"NewModule\\settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
|
||||
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
|
||||
TEST_CLASS(UpdateLifecycleTests)
|
||||
{
|
||||
public:
|
||||
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
|
||||
// and install directory — all three components needed for Stage 2.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
|
||||
// Setup: Typical paths with spaces (Program Files).
|
||||
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Must contain the stage 2 flag
|
||||
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
|
||||
// Must contain the installer path (quoted)
|
||||
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
|
||||
// Must contain the install directory (quoted) — this was MISSING before our fix
|
||||
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
|
||||
// survive CommandLineToArgvW parsing when paths contain spaces.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
|
||||
// Setup: Installer path with spaces.
|
||||
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\path with spaces\\installer.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Count quotes — should have 4 (open/close for each path)
|
||||
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
|
||||
Assert::AreEqual(size_t{ 4 }, quoteCount);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
|
||||
// Setup: Standard install path without trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: trailing backslash does not produce double
|
||||
// backslash (e.g., "...PowerToys\\PowerToys.exe").
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
|
||||
// Setup: Install path with trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
|
||||
// Setup: Empty install directory string.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"");
|
||||
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
|
||||
// the install directory (argCount >= 4), false otherwise.
|
||||
// This is the gate that prevents relaunch when using an old Stage 1
|
||||
// that didn't pass the install dir (#42004/#43011/#44071).
|
||||
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
|
||||
TEST_METHOD(CanRelaunchReflectsArgCount)
|
||||
{
|
||||
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
|
||||
|
||||
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
|
||||
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
|
||||
// Verifies quoting is correct so paths with spaces survive the round trip.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
|
||||
// Setup: Realistic paths with spaces and version numbers.
|
||||
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
|
||||
{
|
||||
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
|
||||
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
|
||||
|
||||
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
|
||||
|
||||
// Simulate what Windows does: prepend a fake exe name and parse
|
||||
std::wstring commandLine = L"PowerToys.Update.exe " + args;
|
||||
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
|
||||
Assert::IsNotNull(argv);
|
||||
Assert::AreEqual(4, argc);
|
||||
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
|
||||
Assert::AreEqual(installerPath, std::wstring(argv[2]));
|
||||
Assert::AreEqual(installDir, std::wstring(argv[3]));
|
||||
|
||||
LocalFree(argv);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UpdatingUnitTests</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Updating.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="UpdatingTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
#include <atomic>
|
||||
#include <Windows.h>
|
||||
|
||||
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26466)
|
||||
#include "CppUnitTest.h"
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif //PCH_H
|
||||
@@ -1,228 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct BackupResult
|
||||
{
|
||||
int filesBackedUp{ 0 };
|
||||
int errors{ 0 };
|
||||
};
|
||||
|
||||
struct RestoreResult
|
||||
{
|
||||
int filesRestored{ 0 };
|
||||
int filesChecked{ 0 };
|
||||
int errors{ 0 };
|
||||
};
|
||||
|
||||
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
|
||||
inline bool IsJsonFileCorrupted(const fs::path& filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t c_readChunkSize{ 4096 };
|
||||
char buffer[c_readChunkSize];
|
||||
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
|
||||
{
|
||||
const auto bytesRead = file.gcount();
|
||||
for (std::streamsize i = 0; i < bytesRead; ++i)
|
||||
{
|
||||
if (buffer[i] == '\0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup all JSON config files before update to protect against corruption (#46179)
|
||||
inline BackupResult BackupConfigFiles(const fs::path& rootPath)
|
||||
{
|
||||
BackupResult result{};
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(backupDir, ec);
|
||||
fs::create_directories(backupDir, ec);
|
||||
if (ec)
|
||||
{
|
||||
result.errors++;
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(rootPath, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
result.errors++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file() && entry.path().extension() == L".json")
|
||||
{
|
||||
std::error_code copyEc;
|
||||
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, copyEc);
|
||||
if (copyEc)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.filesBackedUp++;
|
||||
}
|
||||
}
|
||||
else if (entry.is_directory())
|
||||
{
|
||||
const auto dirName = entry.path().filename().wstring();
|
||||
if (dirName == L"ConfigBackup" || dirName == L"Updates")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto moduleBackup = backupDir / entry.path().filename();
|
||||
fs::create_directories(moduleBackup, ec);
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
result.errors++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
|
||||
{
|
||||
std::error_code copyEc;
|
||||
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, copyEc);
|
||||
if (copyEc)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.filesBackedUp++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Restore JSON configs from backup if corruption is detected after update.
|
||||
// Cleans up the backup directory afterward.
|
||||
inline RestoreResult RestoreCorruptedConfigs(const fs::path& rootPath)
|
||||
{
|
||||
RestoreResult result{};
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
if (!fs::exists(backupDir))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
result.errors++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalPath = rootPath / backupEntry.path().filename();
|
||||
result.filesChecked++;
|
||||
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
|
||||
{
|
||||
std::error_code copyEc;
|
||||
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, copyEc);
|
||||
if (copyEc)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.filesRestored++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (backupEntry.is_directory())
|
||||
{
|
||||
const auto moduleDir = rootPath / backupEntry.path().filename();
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
result.errors++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
|
||||
result.filesChecked++;
|
||||
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
|
||||
{
|
||||
std::error_code copyEc;
|
||||
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, copyEc);
|
||||
if (copyEc)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.filesRestored++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up backup directory after restore check
|
||||
fs::remove_all(backupDir, ec);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
result.errors++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Build the command-line arguments for Stage 2.
|
||||
// Stage 1 passes the installer path and the PT install directory
|
||||
// so Stage 2 can run the installer and relaunch PowerToys afterward.
|
||||
// Note: paths containing embedded double-quote characters are not supported.
|
||||
// This is safe because install paths come from get_module_folderpath().
|
||||
inline std::wstring BuildStage2Arguments(
|
||||
const std::wstring& stage2Flag,
|
||||
const fs::path& installerPath,
|
||||
const fs::path& installDir)
|
||||
{
|
||||
std::wstring arguments{ stage2Flag };
|
||||
arguments += L" \"";
|
||||
arguments += installerPath.c_str();
|
||||
arguments += L"\" \"";
|
||||
arguments += installDir.c_str();
|
||||
arguments += L"\"";
|
||||
return arguments;
|
||||
}
|
||||
|
||||
// Build the full path to PowerToys.exe from the install directory.
|
||||
// Used by Stage 2 to relaunch PT after a successful update.
|
||||
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
|
||||
{
|
||||
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
|
||||
}
|
||||
|
||||
// Determine whether Stage 2 has enough information to relaunch PT.
|
||||
// Returns true if the install directory argument was provided.
|
||||
inline bool CanRelaunchAfterUpdate(int argCount)
|
||||
{
|
||||
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
|
||||
return argCount >= 4;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<RuntimeTypeInfo>true</RuntimeTypeInfo>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<PreprocessorDefinitions>WIN32;_WINDOWS;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;SPDLOG_WCHAR_TO_UTF8_SUPPORT;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32;_WINDOWS;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ObjectFileName>$(IntDir)</ObjectFileName>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<EnableParallelCodeGeneration>true</EnableParallelCodeGeneration>
|
||||
@@ -71,7 +71,7 @@
|
||||
<ClCompile Include="$(RepoRoot)deps\spdlog\src\file_sinks.cpp" />
|
||||
<ClCompile Include="$(RepoRoot)deps\spdlog\src\async.cpp" />
|
||||
<ClCompile Include="$(RepoRoot)deps\spdlog\src\cfg.cpp" />
|
||||
<ClCompile Include="$(RepoRoot)deps\spdlog\src\bundled_fmtlib_format.cpp" />
|
||||
<ClCompile Include="$(RepoRoot)deps\spdlog\src\fmt.cpp" />
|
||||
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async.h" />
|
||||
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async_logger-inl.h" />
|
||||
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async_logger.h" />
|
||||
|
||||
@@ -698,7 +698,7 @@ void LightSwitchInterface::init_settings()
|
||||
}
|
||||
catch (const winrt::hresult_error& e)
|
||||
{
|
||||
Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", static_cast<int32_t>(e.code()), e.message().c_str());
|
||||
Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", e.code(), e.message().c_str());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
|
||||
@@ -58,22 +58,10 @@ namespace ShortcutGuide.Converters
|
||||
|
||||
foreach (var key in description.Keys)
|
||||
{
|
||||
// Try to parse a string key number to a key code
|
||||
// Try to parse a string key number to a VirtualKey
|
||||
if (int.TryParse(key, out int keyCode))
|
||||
{
|
||||
switch (keyCode)
|
||||
{
|
||||
// https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348
|
||||
case 38: // The Up Arrow key or button.
|
||||
case 40: // The Down Arrow key or button.
|
||||
case 37: // The Left Arrow key or button.
|
||||
case 39: // The Right Arrow key or button.
|
||||
shortcutList.Add(keyCode);
|
||||
break;
|
||||
default:
|
||||
shortcutList.Add(Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetKeyName((uint)keyCode));
|
||||
break;
|
||||
}
|
||||
shortcutList.Add(keyCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -39,15 +39,20 @@ namespace ShortcutGuide.Helpers
|
||||
public static ShortcutFile GetShortcutsOfApplication(string applicationName)
|
||||
{
|
||||
string path = PathOfManifestFiles;
|
||||
IEnumerable<string> files = Directory.EnumerateFiles(path, applicationName + ".*.yml") ??
|
||||
throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}'.");
|
||||
string localizedPath = Path.Combine(path, applicationName + $".{Language}.yml");
|
||||
string fallbackPath = Path.Combine(path, applicationName + ".en-US.yml");
|
||||
|
||||
IEnumerable<string> filesEnumerable = files as string[] ?? [.. files];
|
||||
return filesEnumerable.Any(f => f.EndsWith($".{Language}.yml", StringComparison.InvariantCulture))
|
||||
? YamlToShortcutList(File.ReadAllText(Path.Combine(path, applicationName + $".{Language}.yml")))
|
||||
: filesEnumerable.Any(f => f.EndsWith(".en-US.yml", StringComparison.InvariantCulture))
|
||||
? YamlToShortcutList(File.ReadAllText(filesEnumerable.First(f => f.EndsWith(".en-US.yml", StringComparison.InvariantCulture))))
|
||||
: throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}' with the language '{Language}' or 'en-US'.");
|
||||
if (File.Exists(localizedPath))
|
||||
{
|
||||
return YamlToShortcutList(File.ReadAllText(localizedPath));
|
||||
}
|
||||
|
||||
if (File.Exists(fallbackPath))
|
||||
{
|
||||
return YamlToShortcutList(File.ReadAllText(fallbackPath));
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}' with the language '{Language}' or 'en-US'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,6 +83,36 @@ namespace ShortcutGuide.Helpers
|
||||
return deserializer.Deserialize<IndexFile>(content);
|
||||
}
|
||||
|
||||
private static readonly object IndexLock = new();
|
||||
private static IndexFile? cachedIndexFile;
|
||||
private static DateTime cachedIndexLastWriteTimeUtc;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the index YAML file that contains the list of all applications and their shortcuts from the cache.
|
||||
/// </summary>
|
||||
/// <returns>A deserialized <see cref="IndexFile"/> object.</returns>
|
||||
private static IndexFile GetCachedIndexYamlFile()
|
||||
{
|
||||
string indexPath = Path.Combine(PathOfManifestFiles, "index.yml");
|
||||
DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(indexPath);
|
||||
|
||||
lock (IndexLock)
|
||||
{
|
||||
if (cachedIndexFile is not null && cachedIndexLastWriteTimeUtc == lastWriteTimeUtc)
|
||||
{
|
||||
return cachedIndexFile.Value;
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(indexPath);
|
||||
Deserializer deserializer = new();
|
||||
|
||||
cachedIndexFile = deserializer.Deserialize<IndexFile>(content);
|
||||
cachedIndexLastWriteTimeUtc = lastWriteTimeUtc;
|
||||
|
||||
return cachedIndexFile.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all application IDs that should be displayed, based on the foreground window and background processes.
|
||||
/// </summary>
|
||||
@@ -93,8 +128,6 @@ namespace ShortcutGuide.Helpers
|
||||
|
||||
Dictionary<string, string?> applicationIds = new(StringComparer.Ordinal);
|
||||
|
||||
Process[] processes = Process.GetProcesses();
|
||||
|
||||
if (NativeMethods.GetWindowThreadProcessId(handle, out uint processId) > 0)
|
||||
{
|
||||
string? name = null;
|
||||
@@ -119,7 +152,7 @@ namespace ShortcutGuide.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
IndexFile.IndexItem match = GetIndexYamlFile().Index.First((s) => !s.BackgroundProcess && IsMatch(name, s.WindowFilter));
|
||||
IndexFile.IndexItem match = GetCachedIndexYamlFile().Index.First((s) => !s.BackgroundProcess && IsMatch(name, s.WindowFilter));
|
||||
string? pathForApp = match.WindowFilter == "*" ? null : executablePath;
|
||||
foreach (var item in match.Apps)
|
||||
{
|
||||
@@ -132,48 +165,15 @@ namespace ShortcutGuide.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in GetIndexYamlFile().Index.Where((s) => s.BackgroundProcess))
|
||||
foreach (var item in GetCachedIndexYamlFile().Index.Where((s) => s.BackgroundProcess))
|
||||
{
|
||||
try
|
||||
var foundProcesses = Process.GetProcessesByName(item.WindowFilter);
|
||||
if (foundProcesses.Length > 0)
|
||||
{
|
||||
string? matchedExecutablePath = null;
|
||||
bool matched = false;
|
||||
foreach (var p in processes)
|
||||
foreach (var app in item.Apps)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsMatch(p.MainModule!.ModuleName, item.WindowFilter))
|
||||
{
|
||||
matched = true;
|
||||
if (item.WindowFilter != "*")
|
||||
{
|
||||
matchedExecutablePath = p.MainModule!.FileName;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
// Access denied for elevated processes; skip.
|
||||
}
|
||||
applicationIds[app] = foundProcesses[0].MainModule?.FileName;
|
||||
}
|
||||
|
||||
if (matched)
|
||||
{
|
||||
foreach (var app in item.Apps)
|
||||
{
|
||||
// Preserve an existing (foreground) path if one was already set;
|
||||
// only fill in a path when the slot is currently null.
|
||||
if (!applicationIds.TryGetValue(app, out string? existing) || existing is null)
|
||||
{
|
||||
applicationIds[app] = matchedExecutablePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +181,17 @@ namespace ShortcutGuide.Helpers
|
||||
|
||||
static bool IsMatch(string input, string filter)
|
||||
{
|
||||
input = input.ToLower(CultureInfo.InvariantCulture);
|
||||
filter = filter.ToLower(CultureInfo.InvariantCulture);
|
||||
string regexPattern = "^" + Regex.Escape(filter).Replace("\\*", ".*") + "$";
|
||||
return Regex.IsMatch(input, regexPattern);
|
||||
if (filter == "*")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filter.ToLowerInvariant().EndsWith(".exe", StringComparison.InvariantCulture))
|
||||
{
|
||||
return input == filter[..^4];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -32,7 +33,9 @@ namespace ShortcutGuide.Helpers
|
||||
content = new(PopulateRegex().Replace(content.ToString(), populateStartString + Environment.NewLine));
|
||||
|
||||
SettingsUtils settingsUtils = SettingsUtils.Default;
|
||||
EnabledModules enabledModules = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.Enabled;
|
||||
SettingsRepository<GeneralSettings> settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils);
|
||||
settingsRepository.ReloadSettings();
|
||||
EnabledModules enabledModules = settingsRepository.SettingsConfig.Enabled;
|
||||
if (enabledModules.AdvancedPaste)
|
||||
{
|
||||
AdvancedPasteProperties advancedPasteProperties = SettingsRepository<AdvancedPasteSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
|
||||
@@ -57,11 +60,19 @@ namespace ShortcutGuide.Helpers
|
||||
content.Append(HotkeySettingsToYaml(advancedPasteProperties.AdditionalActions.Transcode.TranscodeToMp3.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), SettingsResourceLoader.GetString("TranscodeToMp3/Header")));
|
||||
content.Append(HotkeySettingsToYaml(advancedPasteProperties.AdditionalActions.Transcode.TranscodeToMp4.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), SettingsResourceLoader.GetString("TranscodeToMp4/Header")));
|
||||
}
|
||||
|
||||
foreach (var action in advancedPasteProperties.CustomActions.Value.Where(a => a.IsShown))
|
||||
{
|
||||
content.Append(HotkeySettingsToYaml(action.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), action.Name));
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledModules.AlwaysOnTop)
|
||||
{
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.Hotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_ShortDescription")));
|
||||
AlwaysOnTopProperties alwaysOnTopProperties = SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
|
||||
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.Hotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_ShortDescription")));
|
||||
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.IncreaseOpacityHotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_IncreaseOpacityShortcut/Header")));
|
||||
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.DecreaseOpacityHotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_DecreaseOpacityShortcut/Header")));
|
||||
}
|
||||
|
||||
if (enabledModules.ColorPicker)
|
||||
@@ -79,6 +90,7 @@ namespace ShortcutGuide.Helpers
|
||||
CropAndLockProperties cropAndLockProperties = SettingsRepository<CropAndLockSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
|
||||
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ThumbnailHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Thumbnail")));
|
||||
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ReparentHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Reparent")));
|
||||
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ScreenshotHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Screenshot")));
|
||||
}
|
||||
|
||||
if (enabledModules.CursorWrap)
|
||||
@@ -116,6 +128,11 @@ namespace ShortcutGuide.Helpers
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<PeekSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ActivationShortcut, SettingsResourceLoader.GetString("Peek/ModuleTitle")));
|
||||
}
|
||||
|
||||
if (enabledModules.PowerDisplay)
|
||||
{
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<PowerDisplaySettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ActivationShortcut, SettingsResourceLoader.GetString("PowerDisplay/ModuleTitle"), SettingsResourceLoader.GetString("Launch_PowerDisplay/Content")));
|
||||
}
|
||||
|
||||
if (enabledModules.PowerLauncher)
|
||||
{
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<PowerLauncherSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.OpenPowerLauncher, SettingsResourceLoader.GetString("PowerLauncher/ModuleTitle")));
|
||||
@@ -128,7 +145,7 @@ namespace ShortcutGuide.Helpers
|
||||
|
||||
if (enabledModules.ShortcutGuide)
|
||||
{
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.DefaultOpenShortcutGuide, SettingsResourceLoader.GetString("ShortcutGuide/ModuleTitle"), SettingsResourceLoader.GetString("ShortcutGuide_ShortDescription")));
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.OpenShortcutGuide, SettingsResourceLoader.GetString("ShortcutGuide/ModuleTitle"), SettingsResourceLoader.GetString("ShortcutGuide_ShortDescription")));
|
||||
}
|
||||
|
||||
if (enabledModules.PowerOcr)
|
||||
@@ -145,6 +162,8 @@ namespace ShortcutGuide.Helpers
|
||||
/*
|
||||
if (enabledModules.ZoomIt)
|
||||
{
|
||||
settingsUtils.GetSettings<ZoomItSettings>("ZoomIt")
|
||||
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_ZoomGroup/Header")));
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.LiveZoomToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_LiveZoomGroup/Header")));
|
||||
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.DrawToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_DrawGroup/Header")));
|
||||
@@ -188,6 +207,11 @@ namespace ShortcutGuide.Helpers
|
||||
/// <inheritdoc cref="HotkeySettingsToYaml(HotkeySettings, string, string?)"/>
|
||||
private static string HotkeySettingsToYaml(KeyboardKeysProperty hotkeySettings, string moduleName, string? description = null)
|
||||
{
|
||||
if (hotkeySettings is null)
|
||||
{
|
||||
return HotkeySettingsToYaml(new HotkeySettings(), moduleName, description);
|
||||
}
|
||||
|
||||
return HotkeySettingsToYaml(hotkeySettings.Value, moduleName, description);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
@@ -27,8 +28,8 @@ namespace ShortcutGuide
|
||||
// The module interface passes: <powertoys_pid> [telemetry]
|
||||
if (args.Length >= 2 && args[1] == "telemetry")
|
||||
{
|
||||
Logger.LogInfo("Telemetry mode requested. Sending settings telemetry.");
|
||||
SendSettingsTelemetry();
|
||||
// Telemetry-only invocation: send settings telemetry and exit silently.
|
||||
Logger.LogInfo("Telemetry mode requested. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +72,8 @@ namespace ShortcutGuide
|
||||
indexGeneration.WaitForExit();
|
||||
if (indexGeneration.ExitCode != 0)
|
||||
{
|
||||
Logger.LogError($"Index generation failed with exit code {indexGeneration.ExitCode}. There may be a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\".");
|
||||
Logger.LogError("Index generation failed with exit code: " + indexGeneration.ExitCode);
|
||||
MessageBox.Show($"Shortcut Guide encountered an error while generating the index file. There is likely a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\". Try deleting this directory.", "Error displaying shortcuts", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
@@ -45,6 +46,7 @@ namespace ShortcutGuide
|
||||
MainWindow.SessionDurationMs,
|
||||
MainWindow.CloseType));
|
||||
Current.Exit();
|
||||
Application.Current.Exit();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Common.UI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -32,8 +33,7 @@ namespace ShortcutGuide
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
private readonly Dictionary<string, string?> _currentApplicationIds;
|
||||
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
|
||||
private Dictionary<string, string?> _currentApplicationIds = [];
|
||||
private ShortcutFile? _shortcutFile;
|
||||
private string _selectedAppName = null!;
|
||||
private string _closeType = "Unknown";
|
||||
@@ -46,9 +46,17 @@ namespace ShortcutGuide
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
this._currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds();
|
||||
|
||||
this.InitializeComponent();
|
||||
Activated += Window_Activated;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds();
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
this.SetNavItems();
|
||||
});
|
||||
});
|
||||
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
@@ -111,10 +111,10 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
switch (res.error())
|
||||
{
|
||||
case JsonUtils::WorkspacesFileError::FileReadingError:
|
||||
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR)), file);
|
||||
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
|
||||
break;
|
||||
case JsonUtils::WorkspacesFileError::IncorrectFileError:
|
||||
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR)), file);
|
||||
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -137,10 +137,10 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
switch (res.error())
|
||||
{
|
||||
case JsonUtils::WorkspacesFileError::FileReadingError:
|
||||
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR)), file);
|
||||
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
|
||||
break;
|
||||
case JsonUtils::WorkspacesFileError::IncorrectFileError:
|
||||
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR)), file);
|
||||
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
if (workspaces.empty())
|
||||
{
|
||||
Logger::warn("Workspaces file is empty");
|
||||
std::wstring formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_EMPTY_FILE)), file);
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), file);
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
@@ -169,7 +169,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
|
||||
if (projectToLaunch.id.empty())
|
||||
{
|
||||
Logger::critical(L"Workspace {} not found", cmdArgs.workspaceId);
|
||||
std::wstring formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND)), cmdArgs.workspaceId);
|
||||
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND), cmdArgs.workspaceId);
|
||||
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "KeyboardListener.g.h"
|
||||
#include <windows.h>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <spdlog/stopwatch.h>
|
||||
#include <set>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.11.260520004" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -113,13 +112,7 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
|
||||
private set => SetProperty(ref field, value);
|
||||
}
|
||||
|
||||
// NOTE: This MUST be ObservableCollection<T> (not IReadOnlyList<T> or List<T>).
|
||||
// ItemsView.ItemsSource (used on ExtensionGalleryItemPage) goes through a WinRT
|
||||
// vector adapter; under AOT/trimming, only ObservableCollection<T> has a
|
||||
// preserved IBindableObservableVector adapter. Other list types raise
|
||||
// "Argument 'source' is not a supported vector" at set_ItemsSource time,
|
||||
// crashing the page's first measure pass.
|
||||
public ObservableCollection<ExtensionGalleryScreenshotViewModel> Screenshots { get; }
|
||||
public IReadOnlyList<ExtensionGalleryScreenshotViewModel> Screenshots { get; }
|
||||
|
||||
public bool HasScreenshots => Screenshots.Count > 0;
|
||||
|
||||
@@ -531,14 +524,14 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
|
||||
|| uri.Scheme.Equals("ms-appx", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static ObservableCollection<ExtensionGalleryScreenshotViewModel> BuildScreenshots(List<string>? screenshotUrls)
|
||||
private static IReadOnlyList<ExtensionGalleryScreenshotViewModel> BuildScreenshots(List<string>? screenshotUrls)
|
||||
{
|
||||
ObservableCollection<ExtensionGalleryScreenshotViewModel> screenshots = [];
|
||||
if (screenshotUrls is null || screenshotUrls.Count == 0)
|
||||
{
|
||||
return screenshots;
|
||||
return [];
|
||||
}
|
||||
|
||||
List<ExtensionGalleryScreenshotViewModel> screenshots = [];
|
||||
HashSet<string> seenUris = new(OrdinalIgnoreCase);
|
||||
for (var i = 0; i < screenshotUrls.Count; i++)
|
||||
{
|
||||
|
||||
@@ -49,14 +49,6 @@ public partial class ProviderSettingsViewModel : ObservableObject
|
||||
|
||||
public string DisplayName => _provider.DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// Stable, non-localized identifier from the underlying provider (e.g.
|
||||
/// "com.microsoft.cmdpal.builtin.calculator"). Exposed for UI tests
|
||||
/// to target the per-provider <see cref="ProviderSettingsViewModel"/>
|
||||
/// row via <c>AutomationProperties.AutomationId</c>.
|
||||
/// </summary>
|
||||
public string Id => _provider.Id;
|
||||
|
||||
public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? Resources.builtin_extension_name;
|
||||
|
||||
public string ExtensionSubtext
|
||||
|
||||
@@ -779,48 +779,30 @@ public sealed partial class DockWindow : WindowEx,
|
||||
|
||||
private void RequestShowPaletteOnUiThread(Point posDips)
|
||||
{
|
||||
// pos is relative to our root. We need to convert to absolute
|
||||
// virtual-screen coords.
|
||||
//
|
||||
// TransformToVisual(null) yields a point in the XamlRoot's coordinate
|
||||
// space (i.e. the window's client area in DIPs), NOT in screen space.
|
||||
// To get true screen coordinates we must offset by the window's
|
||||
// screen-space origin (GetWindowRect, which is in pixels). Without
|
||||
// this offset, X (for Top/Bottom docks) or Y (for Left/Right docks)
|
||||
// stays in window-local pixels and the palette ends up on the primary
|
||||
// monitor when the dock lives on a secondary monitor.
|
||||
// pos is relative to our root. We need to convert to screen coords.
|
||||
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
var scaleFactor = dpi / 96.0;
|
||||
PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
|
||||
var screenPosPixels = new Point(
|
||||
ourRect.left + (screenPosDips.X * scaleFactor),
|
||||
ourRect.top + (screenPosDips.Y * scaleFactor));
|
||||
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
|
||||
|
||||
// Use monitor-specific bounds when available
|
||||
// Note: we compute the quadrant in monitor-local coordinates, but
|
||||
// keep screenPosPixels in absolute virtual-screen coordinates. Mixing
|
||||
// the two below (when only one axis is overridden from ourRect, which
|
||||
// is in virtual-screen coords) produced an off-screen final position
|
||||
// on secondary monitors.
|
||||
int screenWidth, screenHeight;
|
||||
double localX, localY;
|
||||
if (_targetMonitor is not null)
|
||||
{
|
||||
screenWidth = _targetMonitor.Bounds.Width;
|
||||
screenHeight = _targetMonitor.Bounds.Height;
|
||||
localX = screenPosPixels.X - _targetMonitor.Bounds.Left;
|
||||
localY = screenPosPixels.Y - _targetMonitor.Bounds.Top;
|
||||
|
||||
// Adjust to monitor-local coordinates for quadrant calculation
|
||||
screenPosPixels = new Point(
|
||||
screenPosPixels.X - _targetMonitor.Bounds.Left,
|
||||
screenPosPixels.Y - _targetMonitor.Bounds.Top);
|
||||
}
|
||||
else
|
||||
{
|
||||
screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
localX = screenPosPixels.X;
|
||||
localY = screenPosPixels.Y;
|
||||
}
|
||||
|
||||
// Now we're going to find the best position for the palette.
|
||||
@@ -838,8 +820,8 @@ public sealed partial class DockWindow : WindowEx,
|
||||
// On the bottom:
|
||||
// - anchor to the bottom, left if we're on the left half of the screen
|
||||
// - anchor to the bottom, right if we're on the right half of the screen
|
||||
var onTopHalf = localY < screenHeight / 2;
|
||||
var onLeftHalf = localX < screenWidth / 2;
|
||||
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
|
||||
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
|
||||
var onRightHalf = !onLeftHalf;
|
||||
var onBottomHalf = !onTopHalf;
|
||||
|
||||
@@ -855,6 +837,7 @@ public sealed partial class DockWindow : WindowEx,
|
||||
// we also need to slide the anchor point a bit away from the dock
|
||||
var paddingDips = 8;
|
||||
var paddingPixels = paddingDips * scaleFactor;
|
||||
PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
|
||||
// Depending on the side we're on, we need to offset differently
|
||||
switch (EffectiveSide)
|
||||
|
||||
@@ -350,10 +350,7 @@
|
||||
Small="{StaticResource IconGridViewItemStyle}" />
|
||||
|
||||
<DataTemplate x:Key="ListItemSingleRowViewModelTemplate" x:DataType="viewModels:ListItemViewModel">
|
||||
<Grid
|
||||
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
ColumnSpacing="12">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -465,7 +462,6 @@
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
|
||||
@@ -490,7 +486,6 @@
|
||||
Width="{StaticResource MediumGridContainerSize}"
|
||||
Height="{StaticResource MediumGridContainerSize}"
|
||||
Padding="8"
|
||||
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
@@ -533,7 +528,6 @@
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{StaticResource GalleryGridViewItemRadius}"
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
Name="Command"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
|
||||
Click="Command_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
x:Uid="Settings_AppearancePage_OpenCommandPaletteButton"
|
||||
MinWidth="200"
|
||||
HorizontalContentAlignment="Left"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_OpenCommandPalette"
|
||||
Click="OpenCommandPalette_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -58,7 +57,6 @@
|
||||
x:Uid="Settings_AppearancePage_ResetAppearanceButton"
|
||||
MinWidth="200"
|
||||
HorizontalContentAlignment="Left"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_ResetAppearance"
|
||||
Command="{x:Bind ViewModel.Appearance.ResetAppearanceSettingsCommand}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -70,10 +68,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_Theme"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -103,10 +98,7 @@
|
||||
x:Uid="Settings_GeneralPage_BackdropStyle_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BackdropStyle"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Acrylic" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Transparent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Mica" />
|
||||
@@ -134,7 +126,6 @@
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackdropOpacity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BackdropOpacity"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
@@ -152,7 +143,6 @@
|
||||
<ComboBox
|
||||
x:Uid="Settings_GeneralPage_ColorizationMode"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_ColorizationMode"
|
||||
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
@@ -212,15 +202,11 @@
|
||||
x:Uid="Settings_GeneralPage_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.Appearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button
|
||||
x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_ChooseBackgroundImage"
|
||||
Click="PickBackgroundImage_Click" />
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageBrightness"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
@@ -229,14 +215,13 @@
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageBlur"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.Appearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageFit" SelectedIndex="{x:Bind ViewModel.Appearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.Appearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
@@ -245,7 +230,6 @@
|
||||
<!-- Background tint color and intensity -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintVisible, Mode=OneWay}">
|
||||
<ptControls:ColorPickerButton
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgTintColor"
|
||||
HasSelectedColor="True"
|
||||
IsAlphaEnabled="False"
|
||||
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
|
||||
@@ -254,7 +238,6 @@
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsColorIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgTintIntensity"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
@@ -263,7 +246,6 @@
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ImageTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsImageTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageTintIntensity"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
@@ -273,10 +255,7 @@
|
||||
<!-- Reset appearance properties -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsResetButtonVisible, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_ResetBgImage"
|
||||
Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
@@ -288,18 +267,15 @@
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowAppDetails_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_ShowAppDetails" IsOn="{x:Bind ViewModel.ShowAppDetails, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowAppDetails, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackspaceGoesBack_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_BackspaceGoesBack" IsOn="{x:Bind ViewModel.BackspaceGoesBack, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.BackspaceGoesBack, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EscapeKeyBehavior_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_AppearancePage_EscapeKeyBehavior"
|
||||
SelectedIndex="{x:Bind ViewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysGoBack" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysDismiss" />
|
||||
@@ -308,11 +284,11 @@
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_SingleClickActivation_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_SingleClickActivates" IsOn="{x:Bind ViewModel.SingleClickActivates, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SingleClickActivates, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_DisableAnimations_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_DisableAnimations" IsOn="{x:Bind ViewModel.DisableAnimations, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.DisableAnimations, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -41,12 +41,11 @@
|
||||
x:Uid="CmdPalDock_LearnMore"
|
||||
Margin="0,0,0,36"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_LearnMore"
|
||||
FontWeight="SemiBold"
|
||||
NavigateUri="https://aka.ms/cmdpal-dock" />
|
||||
<!-- Enable Dock -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_DockSettingsPage_EnableDock" IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Appearance Section -->
|
||||
@@ -83,10 +82,7 @@
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_Theme"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
@@ -127,10 +123,7 @@
|
||||
x:Uid="DockAppearance_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ColorizationMode"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
@@ -183,15 +176,11 @@
|
||||
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button
|
||||
x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ChooseBackgroundImage"
|
||||
Click="PickBackgroundImage_Click" />
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageBrightness"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
@@ -200,14 +189,13 @@
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageBlur"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageFit" SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
@@ -224,7 +212,6 @@
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgTintIntensity"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
@@ -233,10 +220,7 @@
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button
|
||||
x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton"
|
||||
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ResetBgImage"
|
||||
Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
@@ -246,7 +230,7 @@
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="DockBehavior_AlwaysOnTop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_DockSettingsPage_AlwaysOnTop" IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Monitors Section -->
|
||||
@@ -259,17 +243,10 @@
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockMonitorConfigViewModel">
|
||||
<controls:SettingsExpander
|
||||
AutomationProperties.AutomationId="{x:Bind DeviceId, Mode=OneWay}"
|
||||
Description="{x:Bind Resolution, Mode=OneWay}"
|
||||
Header="{x:Bind DisplayName, Mode=OneWay}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="False">
|
||||
<!--
|
||||
AutomationId on the inner ToggleSwitch / ComboBox is intentionally
|
||||
omitted because the parent SettingsExpander already exposes a
|
||||
per-monitor unique AutomationId (DeviceId), so tests can locate
|
||||
each control relative to its monitor without ambiguity.
|
||||
-->
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard x:Uid="DockMonitor_Position_SettingsCard">
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<Button
|
||||
Margin="0,12,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_InstallViaWinGet"
|
||||
Command="{x:Bind ViewModel.InstallViaWinGetCommand, Mode=OneWay}"
|
||||
Content="{x:Bind ViewModel.InstallViaWinGetText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.CanInstallViaWinGet, Mode=OneWay}"
|
||||
@@ -49,7 +48,6 @@
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_CancelWinGetAction"
|
||||
Command="{x:Bind ViewModel.CancelWinGetActionCommand, Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.ShowCancelWinGetActionButton), Mode=OneWay, FallbackValue=Collapsed}">
|
||||
@@ -85,7 +83,6 @@
|
||||
Height="28"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_CopyInstallCommand"
|
||||
Command="{x:Bind ViewModel.CopyWinGetInstallCommand, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.CanCopyWinGetInstallCommand, Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
@@ -219,7 +216,6 @@
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_GalleryItemPage_AuthorLink"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_AuthorLink"
|
||||
Command="{x:Bind ViewModel.OpenAuthorPageCommand, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasAuthorUrl), Mode=OneWay, FallbackValue=Collapsed}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
@@ -231,7 +227,6 @@
|
||||
<HyperlinkButton
|
||||
Grid.Row="3"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_ViewRepository"
|
||||
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ViewModel.Homepage, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasHomepage), Mode=OneWay, FallbackValue=Collapsed}">
|
||||
@@ -315,7 +310,6 @@
|
||||
x:Uid="Settings_GalleryItemPage_Uninstall_Link"
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_OpenInstalledApps"
|
||||
Command="{x:Bind ViewModel.OpenInstalledAppsCommand}"
|
||||
FontSize="12"
|
||||
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.ShowInstalledBadge), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<Run Text="{x:Bind ViewModel.DisplayName}" />
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Header>
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_ExtensionPage_Enable" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -109,15 +109,11 @@
|
||||
<TextBox
|
||||
x:Uid="Settings_ExtensionPage_Alias_PlaceholderText"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_ExtensionPage_AliasText"
|
||||
AutomationProperties.LabeledBy="{x:Bind AliasSettingsCard}"
|
||||
Text="{x:Bind AliasText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_ExtensionPage_AliasActivation_SettingsCard" IsEnabled="{x:Bind AliasText, Converter={StaticResource StringEmptyToBoolConverter}, Mode=OneWay}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_ExtensionPage_AliasActivationType"
|
||||
SelectedIndex="{x:Bind IsDirectAlias, Converter={StaticResource BoolToOptionConverter}, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind IsDirectAlias, Converter={StaticResource BoolToOptionConverter}, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_DirectComboBox" />
|
||||
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_IndirectComboBox" />
|
||||
</ComboBox>
|
||||
@@ -140,7 +136,6 @@
|
||||
Margin="0,0,0,4"
|
||||
Padding="0"
|
||||
VerticalAlignment="Bottom"
|
||||
AutomationProperties.AutomationId="CmdPal_ExtensionPage_ManageFallbackRank"
|
||||
Click="RankButton_Click">
|
||||
<HyperlinkButton.Content>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -162,7 +157,6 @@
|
||||
<DataTemplate x:DataType="viewModels:FallbackSettingsViewModel">
|
||||
<controls:SettingsExpander
|
||||
Grid.Column="1"
|
||||
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
|
||||
Header="{x:Bind DisplayName, Mode=OneWay}"
|
||||
IsExpanded="False">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
@@ -178,13 +172,6 @@
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
|
||||
<!--
|
||||
AutomationId on the inner ToggleSwitch is intentionally
|
||||
omitted because the parent SettingsExpander already
|
||||
exposes a per-fallback unique AutomationId (Id), so
|
||||
tests can locate the toggle relative to that without
|
||||
ambiguity across rows.
|
||||
-->
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_ExtensionsPage_Banner_Hyperlink"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_ExtensionsPage_LearnMore"
|
||||
NavigateUri="https://aka.ms/building-cmdpal-extensions" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
@@ -100,10 +99,7 @@
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button
|
||||
x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore"
|
||||
AutomationProperties.AutomationId="CmdPal_ExtensionsPage_OpenMicrosoftStore"
|
||||
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
|
||||
<Button x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Viewbox Width="16">
|
||||
<Image AutomationProperties.AccessibilityView="Raw" Source="{ThemeResource StoreLogo}" />
|
||||
@@ -216,7 +212,6 @@
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
|
||||
<controls:SettingsCard
|
||||
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
|
||||
Click="SettingsCard_Click"
|
||||
DataContext="{x:Bind}"
|
||||
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
|
||||
@@ -248,12 +243,6 @@
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<!--
|
||||
AutomationId on the inner ToggleSwitch is intentionally omitted
|
||||
because the parent SettingsCard already exposes a unique
|
||||
provider-derived AutomationId (Provider.Id), so tests can locate
|
||||
the toggle relative to that without ambiguity across rows.
|
||||
-->
|
||||
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.GeneralPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
@@ -39,41 +39,27 @@
|
||||
<HyperlinkButton
|
||||
x:Uid="CmdPal_LearnMore"
|
||||
Padding="0"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_LearnMore"
|
||||
FontWeight="SemiBold"
|
||||
NavigateUri="https://aka.ms/cmdpal" />
|
||||
|
||||
<TextBlock x:Uid="ActivationSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_ActivationKey_SettingsExpander"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_ActivationKey"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ptControls:ShortcutControl AutomationProperties.AutomationId="CmdPal_GeneralPage_HotkeyShortcut" HotkeySettings="{x:Bind viewModel.Hotkey, Mode=TwoWay}" />
|
||||
<ptControls:ShortcutControl HotkeySettings="{x:Bind viewModel.Hotkey, Mode=TwoWay}" />
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl
|
||||
x:Uid="Settings_GeneralPage_LowLevelHook_SettingsCard"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_LowLevelHook"
|
||||
IsChecked="{x:Bind viewModel.UseLowLevelGlobalHotkey, Mode=TwoWay}" />
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_LowLevelHook_SettingsCard" IsChecked="{x:Bind viewModel.UseLowLevelGlobalHotkey, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl
|
||||
x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_IgnoreShortcutWhenFullscreen"
|
||||
IsChecked="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl
|
||||
x:Uid="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_IgnoreShortcutWhenBusy"
|
||||
IsChecked="{x:Bind viewModel.IgnoreShortcutWhenBusy, Mode=TwoWay}" />
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenBusy, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl
|
||||
x:Uid="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_AllowBreakthroughShortcut"
|
||||
IsChecked="{x:Bind viewModel.AllowBreakthroughShortcut, Mode=TwoWay}" />
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard" IsChecked="{x:Bind viewModel.AllowBreakthroughShortcut, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
<controls:SettingsExpander.ItemsFooter>
|
||||
@@ -85,10 +71,7 @@
|
||||
</controls:SettingsExpander.ItemsFooter>
|
||||
</controls:SettingsExpander>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_AutoGoHome"
|
||||
SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
|
||||
@@ -101,16 +84,13 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_HighlightSearch" IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_KeepPreviousQuery_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_KeepPreviousQuery" IsOn="{x:Bind viewModel.KeepPreviousQuery, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.KeepPreviousQuery, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Run_PositionHeader" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_MonitorPosition"
|
||||
SelectedIndex="{x:Bind viewModel.MonitorPositionIndex, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.MonitorPositionIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Run_Radio_Position_Cursor" />
|
||||
<ComboBoxItem x:Uid="Run_Radio_Position_Primary_Monitor" />
|
||||
<ComboBoxItem x:Uid="Run_Radio_Position_Focus" />
|
||||
@@ -124,7 +104,7 @@
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_ShowSystemTrayIcon" IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'For Developers' section -->
|
||||
@@ -132,17 +112,14 @@
|
||||
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AllowExternalReload_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_AllowExternalReload" IsOn="{x:Bind viewModel.AllowExternalReload, Mode=TwoWay}" />
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.AllowExternalReload, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'About' section -->
|
||||
|
||||
<TextBlock x:Uid="AboutSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_About_SettingsExpander"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_About"
|
||||
HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/StoreLogo.png}">
|
||||
<controls:SettingsExpander x:Uid="Settings_GeneralPage_About_SettingsExpander" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/StoreLogo.png}">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
IsTextSelectionEnabled="True"
|
||||
@@ -150,14 +127,8 @@
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
|
||||
<StackPanel Margin="-12,0,0,0" Orientation="Vertical">
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_GithubLink"
|
||||
NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" />
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_SDKDocs"
|
||||
NavigateUri="https://aka.ms/cmdpalextensions-devdocs" />
|
||||
<HyperlinkButton x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink" NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" />
|
||||
<HyperlinkButton x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink" NavigateUri="https://aka.ms/cmdpalextensions-devdocs" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
@@ -165,7 +136,6 @@
|
||||
<HyperlinkButton
|
||||
x:Uid="Settings_GeneralPage_SendFeedback_Hyperlink"
|
||||
Margin="0,8,0,0"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_SendFeedback"
|
||||
NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310638" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -32,22 +32,13 @@
|
||||
IsExpanded="True">
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowMainThreadException"
|
||||
Click="ThrowPlainMainThreadException_Click"
|
||||
Content="Throw" />
|
||||
<Button Click="ThrowPlainMainThreadException_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread (with PII)">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowMainThreadExceptionPii"
|
||||
Click="ThrowPlainMainThreadExceptionPii_Click"
|
||||
Content="Throw" />
|
||||
<Button Click="ThrowPlainMainThreadExceptionPii_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard Description="Throw with delay, when the task is collected by the GC" Header="Throw unobserved exception from a task">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowUnobservedTaskException"
|
||||
Click="ThrowExceptionInUnobservedTask_Click"
|
||||
Content="Throw" />
|
||||
<Button Click="ThrowExceptionInUnobservedTask_Click" Content="Throw" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
@@ -58,29 +49,20 @@
|
||||
x:Name="LogsSettingsCard"
|
||||
Header="Logs folder"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenLogsFolder"
|
||||
Click="OpenLogsCardClicked"
|
||||
Content="Open folder" />
|
||||
<Button Click="OpenLogsCardClicked" Content="Open folder" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard
|
||||
x:Name="CurrentLogFileSettingsCard"
|
||||
Header="Current log file"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenCurrentLog"
|
||||
Click="OpenCurrentLogCardClicked"
|
||||
Content="Open log" />
|
||||
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard
|
||||
x:Name="ToggleDevRibbonVisibilitySettingsCard"
|
||||
Description="This is only temporary and state is not saved"
|
||||
Header="Toggle dev ribbon visibility"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_ToggleDevRibbon"
|
||||
Click="ToggleDevRibbonClicked"
|
||||
Content="Toggle dev ribbon" />
|
||||
<Button Click="ToggleDevRibbonClicked" Content="Toggle dev ribbon" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Gallery Section -->
|
||||
@@ -103,10 +85,7 @@
|
||||
x:Name="ConfigurationFolderSettingsCard"
|
||||
Header="Configuration folder"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<Button
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenConfigFolder"
|
||||
Click="OpenConfigFolderCardClick"
|
||||
Content="Open folder" />
|
||||
<Button Click="OpenConfigFolderCardClick" Content="Open folder" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace PowerAccent.Common.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class CharacterMappingsTests
|
||||
{
|
||||
// Every Language enum value must appear in All exactly once. If a value is missing,
|
||||
// GetCharacters will silently return no characters for that language. If it appears
|
||||
// more than once, the second entry is dead code.
|
||||
[TestMethod]
|
||||
public void All_ContainsEveryLanguageEnumValue_ExactlyOnce()
|
||||
{
|
||||
foreach (Language lang in Enum.GetValues<Language>())
|
||||
{
|
||||
var count = CharacterMappings.All.Count(e => e.Id == lang);
|
||||
Assert.AreEqual(
|
||||
1,
|
||||
count,
|
||||
$"Language.{lang} appears {count} time(s) in CharacterMappings.All — expected exactly 1.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Characters dictionary for each entry in All must not contain null or empty
|
||||
/// mappings.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void All_Characters_ContainsNoNullOrEmptyEntries()
|
||||
{
|
||||
foreach (var entry in CharacterMappings.All)
|
||||
{
|
||||
foreach (var kvp in entry.Characters)
|
||||
{
|
||||
var key = kvp.Key;
|
||||
var mappings = kvp.Value;
|
||||
|
||||
Assert.IsNotNull(
|
||||
mappings,
|
||||
$"Language.{entry.Id} has a null mappings array for key {key}.");
|
||||
|
||||
Assert.IsTrue(
|
||||
mappings.Length > 0,
|
||||
$"Language.{entry.Id} has an empty mappings array for key {key}.");
|
||||
|
||||
Assert.IsFalse(
|
||||
mappings.Any(c => string.IsNullOrEmpty(c)),
|
||||
$"Language.{entry.Id} has null or empty string(s) in its mappings array for key {key}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every Language enum value must appear in DisplayOrder exactly once. If a value is
|
||||
// missing, its characters will be silently omitted from the popup. If it appears more
|
||||
// than once, Collect will emit its characters twice (before Distinct removes them).
|
||||
[TestMethod]
|
||||
public void DisplayOrder_ContainsEveryLanguageEnumValue_ExactlyOnce()
|
||||
{
|
||||
foreach (Language lang in Enum.GetValues<Language>())
|
||||
{
|
||||
var count = CharacterMappings.DisplayOrder.Count(l => l == lang);
|
||||
Assert.AreEqual(
|
||||
1,
|
||||
count,
|
||||
$"Language.{lang} appears {count} time(s) in CharacterMappings.DisplayOrder - expected exactly 1.");
|
||||
}
|
||||
}
|
||||
|
||||
// Every LanguageGroup enum value must appear in GroupDisplayOrder exactly once.
|
||||
[TestMethod]
|
||||
public void GroupDisplayOrder_ContainsEveryLanguageGroupValue_ExactlyOnce()
|
||||
{
|
||||
foreach (LanguageGroup group in Enum.GetValues<LanguageGroup>())
|
||||
{
|
||||
var count = CharacterMappings.GroupDisplayOrder.Count(g => g == group);
|
||||
Assert.AreEqual(
|
||||
1,
|
||||
count,
|
||||
$"LanguageGroup.{group} appears {count} time(s) in CharacterMappings.GroupDisplayOrder - expected exactly 1.");
|
||||
}
|
||||
}
|
||||
|
||||
// LanguageLookup must contain an entry for every Language enum value, derived from All.
|
||||
[TestMethod]
|
||||
public void LanguageLookup_ContainsEveryLanguageEnumValue()
|
||||
{
|
||||
foreach (Language lang in Enum.GetValues<Language>())
|
||||
{
|
||||
Assert.IsTrue(
|
||||
CharacterMappings.LanguageLookup.ContainsKey(lang),
|
||||
$"Language.{lang} is missing from CharacterMappings.LanguageLookup.");
|
||||
}
|
||||
}
|
||||
|
||||
// Every entry in All must have a non-empty Identifier. A blank identifier would
|
||||
// produce a malformed resource key (e.g. "QuickAccent_SelectedLanguage_") that
|
||||
// silently resolves to an empty string in the Settings UI.
|
||||
[TestMethod]
|
||||
public void All_EveryEntry_HasNonEmptyIdentifier()
|
||||
{
|
||||
foreach (var entry in CharacterMappings.All)
|
||||
{
|
||||
Assert.IsFalse(
|
||||
string.IsNullOrWhiteSpace(entry.Identifier),
|
||||
$"Language.{entry.Id} has a null or whitespace Identifier.");
|
||||
}
|
||||
}
|
||||
|
||||
// Every entry in All must have a non-null Characters dictionary. A null would throw
|
||||
// at runtime inside GetCharacters.
|
||||
[TestMethod]
|
||||
public void All_EveryEntry_HasNonNullCharacters()
|
||||
{
|
||||
foreach (var entry in CharacterMappings.All)
|
||||
{
|
||||
Assert.IsNotNull(
|
||||
entry.Characters,
|
||||
$"Language.{entry.Id} has a null Characters dictionary.");
|
||||
}
|
||||
}
|
||||
|
||||
// Every LanguageGroup enum value must be used by at least one entry in All. This
|
||||
// guards against a new group being added to the enum but forgotten in the data, which
|
||||
// would make it impossible to test or exercise that group path.
|
||||
[TestMethod]
|
||||
public void All_EveryLanguageGroupValue_IsUsedAtLeastOnce()
|
||||
{
|
||||
var usedGroups = CharacterMappings.All.Select(e => e.Group).ToHashSet();
|
||||
foreach (LanguageGroup group in Enum.GetValues<LanguageGroup>())
|
||||
{
|
||||
// UserDefined is reserved for future use and may not yet be populated.
|
||||
if (group == LanguageGroup.UserDefined)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.IsTrue(
|
||||
usedGroups.Contains(group),
|
||||
$"LanguageGroup.{group} is defined in the enum but no entry in CharacterMappings.All uses it.");
|
||||
}
|
||||
}
|
||||
|
||||
// GetCharacters with an empty language array must return an empty array without
|
||||
// throwing.
|
||||
[TestMethod]
|
||||
public void GetCharacters_EmptyLanguages_ReturnsEmpty()
|
||||
{
|
||||
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, []);
|
||||
Assert.AreEqual(0, result.Length);
|
||||
}
|
||||
|
||||
// GetCharacters with all languages must return a non-empty result for a key that is
|
||||
// mapped in at least one language (VK_A is mapped in the majority of languages).
|
||||
[TestMethod]
|
||||
public void GetCharacters_AllLanguages_ReturnsNonEmptyForCommonKey()
|
||||
{
|
||||
var allLangs = Enum.GetValues<Language>();
|
||||
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, allLangs);
|
||||
Assert.IsTrue(result.Length > 0, "Expected at least one character for VK_A across all languages.");
|
||||
}
|
||||
|
||||
// GetCharacters must deduplicate characters that appear in multiple languages.
|
||||
// If two languages both map VK_A to the same character, it should appear only once.
|
||||
[TestMethod]
|
||||
public void GetCharacters_DeduplicatesCharactersAcrossLanguages()
|
||||
{
|
||||
var allLangs = Enum.GetValues<Language>();
|
||||
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, allLangs);
|
||||
var distinct = result.Distinct().ToArray();
|
||||
CollectionAssert.AreEquivalent(
|
||||
distinct,
|
||||
result,
|
||||
"GetCharacters returned duplicate characters. Results should be deduplicated.");
|
||||
}
|
||||
|
||||
// Calling GetCharacters twice with all languages should return the same results,
|
||||
// confirming the cache path is consistent.
|
||||
[TestMethod]
|
||||
public void GetCharacters_AllLanguagesCachedResult_IsConsistent()
|
||||
{
|
||||
var allLangs = Enum.GetValues<Language>();
|
||||
var first = CharacterMappings.GetCharacters(LetterKey.VK_E, allLangs);
|
||||
var second = CharacterMappings.GetCharacters(LetterKey.VK_E, allLangs);
|
||||
CollectionAssert.AreEqual(first, second, "Cached and non-cached results for VK_E differ.");
|
||||
}
|
||||
|
||||
// GetCharacters for a single language should return exactly that language's
|
||||
// characters for a key it maps. The test derives both the language and key from the
|
||||
// live data so it stays valid regardless of future mapping changes.
|
||||
[TestMethod]
|
||||
public void GetCharacters_SingleLanguage_ReturnsOnlyThatLanguagesCharacters()
|
||||
{
|
||||
var langInfo = CharacterMappings.All.First(l => l.Characters.Count > 0);
|
||||
var (key, expected) = langInfo.Characters.First();
|
||||
|
||||
var result = CharacterMappings.GetCharacters(key, [langInfo.Id]);
|
||||
|
||||
CollectionAssert.AreEquivalent(
|
||||
expected,
|
||||
result,
|
||||
$"GetCharacters for Language.{langInfo.Id} / LetterKey.{key} did not match the mapped characters.");
|
||||
}
|
||||
|
||||
// GetCharacters must throw KeyNotFoundException when passed a Language value that is
|
||||
// not in LanguageLookup (i.e. not in All). This is deliberate fail-fast behaviour:
|
||||
// an unknown language is a programming error, not a recoverable condition. The cast
|
||||
// produces a valid enum value that was never registered in All.
|
||||
[TestMethod]
|
||||
public void GetCharacters_UnknownLanguage_ThrowsKeyNotFoundException()
|
||||
{
|
||||
var unknown = (Language)(-1);
|
||||
Assert.ThrowsExactly<KeyNotFoundException>(
|
||||
() => CharacterMappings.GetCharacters(LetterKey.VK_A, [unknown]),
|
||||
"Expected KeyNotFoundException when a Language value absent from LanguageLookup is passed to GetCharacters.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetCharacters must return characters sorted strictly by GroupDisplayOrder and
|
||||
/// then DisplayOrder, regardless of the sequence of languages passed in.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void GetCharacters_SortsOutput_AccordingToDisplayOrder()
|
||||
{
|
||||
// Input in the wrong order.
|
||||
Language[] input = [Language.SPECIAL, Language.PI, Language.FR];
|
||||
|
||||
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, input);
|
||||
|
||||
// Derive correct order.
|
||||
var expectedOrder = CharacterMappings.All
|
||||
.Where(lang => input.Contains(lang.Id))
|
||||
.OrderBy(lang => CharacterMappings.GroupDisplayOrder.ToList().IndexOf(lang.Group))
|
||||
.ThenBy(lang => CharacterMappings.DisplayOrder.ToList().IndexOf(lang.Id))
|
||||
.SelectMany(lang => lang.Characters.TryGetValue(LetterKey.VK_A, out var chars) ? chars : [])
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
expectedOrder,
|
||||
result,
|
||||
"GetCharacters did not return characters in the expected order based on GroupDisplayOrder and DisplayOrder.");
|
||||
}
|
||||
|
||||
// Collect sorts by _languageOrder[m.Id], so every entry in All must appear in
|
||||
// DisplayOrder. Adding to All without updating DisplayOrder will throw
|
||||
// KeyNotFoundException at the first GetCharacters call that exercises that language.
|
||||
// This test verifies the invariant directly so the failure is caught at test time
|
||||
// rather than at runtime.
|
||||
[TestMethod]
|
||||
public void All_EveryEntry_ExistsInDisplayOrder()
|
||||
{
|
||||
var displayOrderSet = CharacterMappings.DisplayOrder.ToHashSet();
|
||||
foreach (var entry in CharacterMappings.All)
|
||||
{
|
||||
Assert.IsTrue(
|
||||
displayOrderSet.Contains(entry.Id),
|
||||
$"Language.{entry.Id} is in All but missing from DisplayOrder. Add it to DisplayOrder to prevent a KeyNotFoundException at runtime.");
|
||||
}
|
||||
}
|
||||
|
||||
// GetCharacters for a key that is not mapped in a given language should return empty.
|
||||
// The test finds a language and an absent key from the live data so it stays valid
|
||||
// regardless of future mapping changes.
|
||||
[TestMethod]
|
||||
public void GetCharacters_UnmappedKey_ReturnsEmpty()
|
||||
{
|
||||
var allKeys = Enum.GetValues<LetterKey>().ToHashSet();
|
||||
var langInfo = CharacterMappings.All.First(l => allKeys.Except(l.Characters.Keys).Any());
|
||||
var absentKey = allKeys.Except(langInfo.Characters.Keys).First();
|
||||
|
||||
var result = CharacterMappings.GetCharacters(absentKey, [langInfo.Id]);
|
||||
|
||||
Assert.AreEqual(
|
||||
0,
|
||||
result.Length,
|
||||
$"Expected empty result for Language.{langInfo.Id} / LetterKey.{absentKey}, which has no mapping.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spoken languages in DisplayOrder should be sorted alphabetically by their enum
|
||||
/// names to remain culturally neutral.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void DisplayOrder_SpokenLanguages_AreSortedAlphabeticallyByDisplayName()
|
||||
{
|
||||
var spokenLangs = CharacterMappings.DisplayOrder
|
||||
.Where(lang => CharacterMappings.LanguageLookup[lang].Group == LanguageGroup.Language)
|
||||
.Select(lang => lang.ToString())
|
||||
.ToList();
|
||||
|
||||
var sorted = spokenLangs.OrderBy(l => l, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
sorted,
|
||||
spokenLangs,
|
||||
"Spoken languages in DisplayOrder should be sorted alphabetically by their enum names.");
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerAccent.Common;
|
||||
using WinRtLetterKey = PowerToys.PowerAccentKeyboardService.LetterKey;
|
||||
|
||||
namespace PowerAccent.Common.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class LetterKeyTests
|
||||
{
|
||||
// Verifies that the managed LetterKey enum in PowerAccent.Common stays in sync with the
|
||||
// WinRT LetterKey enum defined in KeyboardListener.idl. The adapter in PowerAccent.Core
|
||||
// casts between them via their integer values, so any divergence would silently produce
|
||||
// wrong character mappings at runtime.
|
||||
[TestMethod]
|
||||
public void ManagedLetterKey_MatchesWinRtLetterKey_AllNamesPresent()
|
||||
{
|
||||
foreach (WinRtLetterKey winRtValue in Enum.GetValues<WinRtLetterKey>())
|
||||
{
|
||||
var name = winRtValue.ToString();
|
||||
Assert.IsTrue(
|
||||
Enum.TryParse<LetterKey>(name, out _),
|
||||
$"WinRT LetterKey.{name} has no corresponding value in the managed LetterKey enum. Update PowerAccent.Common/LetterKey.cs.");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ManagedLetterKey_MatchesWinRtLetterKey_ValuesMatch()
|
||||
{
|
||||
foreach (WinRtLetterKey winRtValue in Enum.GetValues<WinRtLetterKey>())
|
||||
{
|
||||
var name = winRtValue.ToString();
|
||||
if (Enum.TryParse<LetterKey>(name, out var managedValue))
|
||||
{
|
||||
Assert.AreEqual(
|
||||
(int)(object)winRtValue,
|
||||
(int)managedValue,
|
||||
$"LetterKey.{name} has value {(int)(object)winRtValue} in WinRT but {(int)managedValue} in the managed enum. Update PowerAccent.Common/LetterKey.cs.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>PowerToys.PowerAccent.Common.UnitTests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\PowerAccent.Common.UnitTests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.PowerAccentKeyboardService</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowerAccent.Common\PowerAccent.Common.csproj" />
|
||||
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,791 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace PowerAccent.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Single source of truth for all Quick Accent character data.
|
||||
/// <para>
|
||||
/// <see cref="All"/> is the canonical registry of every language: its identity, group,
|
||||
/// resource identifier, and character mappings. The Settings UI derives its language
|
||||
/// list from this collection.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="DisplayOrder"/> and <see cref="GroupDisplayOrder"/> control the order
|
||||
/// in which characters appear in the Quick Accent popup. These are intentionally
|
||||
/// separate from <see cref="All"/> so that popup ordering is explicit and not an
|
||||
/// accidental consequence of declaration order.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When adding a new language: add a <see cref="Language"/> enum value, a
|
||||
/// <see cref="LanguageInfo"/> entry to <see cref="All"/>, a position in
|
||||
/// <see cref="DisplayOrder"/>, and a resx string.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class CharacterMappings
|
||||
{
|
||||
/// <summary>
|
||||
/// The canonical registry of all languages. Each entry defines the language's
|
||||
/// identity, group, resource identifier, and character mappings.
|
||||
/// Declaration order here does not affect the popup or settings display order;
|
||||
/// see <see cref="DisplayOrder"/> and <see cref="GroupDisplayOrder"/> for that.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<LanguageInfo> All =
|
||||
[
|
||||
new(Language.SPECIAL, "Special", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_0] = ["₀", "⁰", "°", "↉", "₎", "⁾"],
|
||||
[LetterKey.VK_1] = ["₁", "¹", "½", "⅓", "¼", "⅕", "⅙", "⅐", "⅛", "⅑", "⅒"],
|
||||
[LetterKey.VK_2] = ["₂", "²", "⅔", "⅖"],
|
||||
[LetterKey.VK_3] = ["₃", "³", "¾", "⅗", "⅜"],
|
||||
[LetterKey.VK_4] = ["₄", "⁴", "⅘"],
|
||||
[LetterKey.VK_5] = ["₅", "⁵", "⅚", "⅝"],
|
||||
[LetterKey.VK_6] = ["₆", "⁶"],
|
||||
[LetterKey.VK_7] = ["₇", "⁷", "⅞"],
|
||||
[LetterKey.VK_8] = ["₈", "⁸", "∞"],
|
||||
[LetterKey.VK_9] = ["₉", "⁹", "₍", "⁽"],
|
||||
[LetterKey.VK_A] = ["ȧ", "ǽ", "∀", "ᵃ", "ₐ"],
|
||||
[LetterKey.VK_B] = ["ḃ", "ᵇ"],
|
||||
[LetterKey.VK_C] = ["ċ", "°C", "©", "ℂ", "∁", "ᶜ"],
|
||||
[LetterKey.VK_D] = ["ḍ", "ḋ", "∂", "ᵈ"],
|
||||
[LetterKey.VK_E] = ["∈", "∃", "∄", "∉", "ĕ", "ᵉ", "ₑ"],
|
||||
[LetterKey.VK_F] = ["ḟ", "°F", "ᶠ"],
|
||||
[LetterKey.VK_G] = ["ģ", "ǧ", "ġ", "ĝ", "ǥ", "ᵍ"],
|
||||
[LetterKey.VK_H] = ["ḣ", "ĥ", "ħ", "ʰ", "ₕ"],
|
||||
[LetterKey.VK_I] = ["ⁱ", "ᵢ"],
|
||||
[LetterKey.VK_J] = ["ĵ", "ʲ", "ⱼ"],
|
||||
[LetterKey.VK_K] = ["ķ", "ǩ", "ᵏ", "ₖ"],
|
||||
[LetterKey.VK_L] = ["ļ", "₺", "ˡ", "ₗ"], // ₺ is in VK_T for other languages, but not VK_L, so we add it here.
|
||||
[LetterKey.VK_M] = ["ṁ", "ᵐ", "ₘ"],
|
||||
[LetterKey.VK_N] = ["ņ", "ṅ", "ⁿ", "ℕ", "№", "ₙ"],
|
||||
[LetterKey.VK_O] = ["ȯ", "∅", "⌀", "ᵒ", "ₒ"],
|
||||
[LetterKey.VK_P] = ["ṗ", "℗", "∏", "¶", "ᵖ", "ₚ"],
|
||||
[LetterKey.VK_Q] = ["ℚ", "𐞥"],
|
||||
[LetterKey.VK_R] = ["ṙ", "®", "ℝ", "ʳ", "ᵣ"],
|
||||
[LetterKey.VK_S] = ["ṡ", "§", "∑", "∫", "ˢ", "ₛ"],
|
||||
[LetterKey.VK_T] = ["ţ", "ṫ", "ŧ", "™", "ᵗ", "ₜ"],
|
||||
[LetterKey.VK_U] = ["ŭ", "ᵘ", "ᵤ"],
|
||||
[LetterKey.VK_V] = ["V̇", "ᵛ", "ᵥ"],
|
||||
[LetterKey.VK_W] = ["ẇ", "ʷ"],
|
||||
[LetterKey.VK_X] = ["ẋ", "×", "ˣ", "ₓ"],
|
||||
[LetterKey.VK_Y] = ["ẏ", "ꝡ", "ʸ"],
|
||||
[LetterKey.VK_Z] = ["ʒ", "ǯ", "ℤ", "ᶻ"],
|
||||
[LetterKey.VK_COMMA] = ["∙", "₋", "⁻", "–", "√", "‟", "《", "》", "‛", "〈", "〉", "″", "‴", "⁗"], // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here.
|
||||
[LetterKey.VK_PERIOD] = ["…", "⁝", "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030B", "\u030C"],
|
||||
[LetterKey.VK_MINUS] = ["~", "‐", "‑", "‒", "–", "—", "―", "⁓", "−", "⸺", "⸻", "∓", "₋", "⁻"],
|
||||
[LetterKey.VK_SLASH_] = ["÷", "√"],
|
||||
[LetterKey.VK_DIVIDE_] = ["÷", "√"],
|
||||
[LetterKey.VK_MULTIPLY_] = ["×", "⋅", "ˣ", "ₓ"],
|
||||
[LetterKey.VK_PLUS] = ["≤", "≥", "≠", "≈", "≙", "⊕", "⊗", "±", "≅", "≡", "₊", "⁺", "₌", "⁼"],
|
||||
[LetterKey.VK_BACKSLASH] = ["`", "~"],
|
||||
}),
|
||||
|
||||
new(Language.BG, "Bulgarian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_I] = ["й"],
|
||||
}),
|
||||
|
||||
new(Language.CA, "Catalan", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à", "á"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["è", "é", "€"],
|
||||
[LetterKey.VK_I] = ["ì", "í", "ï"],
|
||||
[LetterKey.VK_N] = ["ñ"],
|
||||
[LetterKey.VK_O] = ["ò", "ó"],
|
||||
[LetterKey.VK_U] = ["ù", "ú", "ü"],
|
||||
[LetterKey.VK_L] = ["·"],
|
||||
[LetterKey.VK_COMMA] = ["¿", "?", "¡", "!", "«", "»", "“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.CRH, "Crimean", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["â"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_G] = ["ğ"],
|
||||
[LetterKey.VK_H] = ["₴"],
|
||||
[LetterKey.VK_I] = ["ı", "İ"],
|
||||
[LetterKey.VK_N] = ["ñ"],
|
||||
[LetterKey.VK_O] = ["ö"],
|
||||
[LetterKey.VK_S] = ["ş"],
|
||||
[LetterKey.VK_T] = ["₺"],
|
||||
[LetterKey.VK_U] = ["ü"],
|
||||
}),
|
||||
|
||||
// Currency symbols. This is a "special" language group as it's not a spoken
|
||||
// language, but rather a set of symbols used across languages.
|
||||
new(Language.CUR, "Currency", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_B] = ["฿", "в"],
|
||||
[LetterKey.VK_C] = ["¢", "₡", "č"],
|
||||
[LetterKey.VK_D] = ["₫"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_F] = ["ƒ"],
|
||||
[LetterKey.VK_H] = ["₴"],
|
||||
[LetterKey.VK_K] = ["₭"],
|
||||
[LetterKey.VK_L] = ["ł"],
|
||||
[LetterKey.VK_N] = ["л"],
|
||||
[LetterKey.VK_M] = ["₼"],
|
||||
[LetterKey.VK_P] = ["£", "₽"],
|
||||
[LetterKey.VK_R] = ["₹", "៛", "﷼"],
|
||||
[LetterKey.VK_S] = ["$", "₪"],
|
||||
[LetterKey.VK_T] = ["₮", "₺", "₸"],
|
||||
[LetterKey.VK_W] = ["₩"],
|
||||
[LetterKey.VK_Y] = ["¥"],
|
||||
[LetterKey.VK_Z] = ["z"],
|
||||
}),
|
||||
|
||||
new(Language.CY, "Welsh", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["â", "ä", "à", "á"],
|
||||
[LetterKey.VK_E] = ["ê", "ë", "è", "é"],
|
||||
[LetterKey.VK_I] = ["î", "ï", "ì", "í"],
|
||||
[LetterKey.VK_O] = ["ô", "ö", "ò", "ó"],
|
||||
[LetterKey.VK_P] = ["£"],
|
||||
[LetterKey.VK_U] = ["û", "ü", "ù", "ú"],
|
||||
[LetterKey.VK_Y] = ["ŷ", "ÿ", "ỳ", "ý"],
|
||||
[LetterKey.VK_W] = ["ŵ", "ẅ", "ẁ", "ẃ"],
|
||||
[LetterKey.VK_COMMA] = ["‘", "’", "“", "”"],
|
||||
}),
|
||||
|
||||
new(Language.CZ, "Czech", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á"],
|
||||
[LetterKey.VK_C] = ["č"],
|
||||
[LetterKey.VK_D] = ["ď"],
|
||||
[LetterKey.VK_E] = ["ě", "é"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_N] = ["ň"],
|
||||
[LetterKey.VK_O] = ["ó"],
|
||||
[LetterKey.VK_R] = ["ř"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_T] = ["ť"],
|
||||
[LetterKey.VK_U] = ["ů", "ú"],
|
||||
[LetterKey.VK_Y] = ["ý"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "‘", "»", "«", "›", "‹"],
|
||||
}),
|
||||
|
||||
new(Language.DK, "Danish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["å", "æ"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_O] = ["ø"],
|
||||
[LetterKey.VK_COMMA] = ["»", "«", "“", "”", "›", "‹", "‘", "’"],
|
||||
}),
|
||||
|
||||
// Gaelic (Irish).
|
||||
new(Language.GA, "Gaeilge", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á"],
|
||||
[LetterKey.VK_E] = ["é", "€"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_O] = ["ó"],
|
||||
[LetterKey.VK_U] = ["ú"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
// Gaelic (Scottish).
|
||||
new(Language.GD, "Gaidhlig", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à"],
|
||||
[LetterKey.VK_E] = ["è"],
|
||||
[LetterKey.VK_I] = ["ì"],
|
||||
[LetterKey.VK_O] = ["ò"],
|
||||
[LetterKey.VK_P] = ["£"],
|
||||
[LetterKey.VK_U] = ["ù"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.DE, "German", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ä"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_O] = ["ö"],
|
||||
[LetterKey.VK_S] = ["ß"],
|
||||
[LetterKey.VK_U] = ["ü"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "‘", "»", "«", "›", "‹"],
|
||||
}),
|
||||
|
||||
new(Language.EL, "Greek", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["α", "ά"],
|
||||
[LetterKey.VK_B] = ["β"],
|
||||
[LetterKey.VK_C] = ["χ"],
|
||||
[LetterKey.VK_D] = ["δ"],
|
||||
[LetterKey.VK_E] = ["ε", "έ", "η", "ή"],
|
||||
[LetterKey.VK_F] = ["φ"],
|
||||
[LetterKey.VK_G] = ["γ"],
|
||||
[LetterKey.VK_I] = ["ι", "ί"],
|
||||
[LetterKey.VK_K] = ["κ"],
|
||||
[LetterKey.VK_L] = ["λ"],
|
||||
[LetterKey.VK_M] = ["μ"],
|
||||
[LetterKey.VK_N] = ["ν"],
|
||||
[LetterKey.VK_O] = ["ο", "ό", "ω", "ώ"],
|
||||
[LetterKey.VK_P] = ["π", "φ", "ψ"],
|
||||
[LetterKey.VK_R] = ["ρ"],
|
||||
[LetterKey.VK_S] = ["σ", "ς"],
|
||||
[LetterKey.VK_T] = ["τ", "θ", "ϑ"],
|
||||
[LetterKey.VK_U] = ["υ", "ύ"],
|
||||
[LetterKey.VK_X] = ["ξ"],
|
||||
[LetterKey.VK_Y] = ["υ"],
|
||||
[LetterKey.VK_Z] = ["ζ"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "«", "»"],
|
||||
}),
|
||||
|
||||
new(Language.EST, "Estonian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ä"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_O] = ["ö", "õ"],
|
||||
[LetterKey.VK_U] = ["ü"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "«", "»"],
|
||||
}),
|
||||
|
||||
new(Language.EPO, "Esperanto", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_C] = ["ĉ"],
|
||||
[LetterKey.VK_G] = ["ĝ"],
|
||||
[LetterKey.VK_H] = ["ĥ"],
|
||||
[LetterKey.VK_J] = ["ĵ"],
|
||||
[LetterKey.VK_S] = ["ŝ"],
|
||||
[LetterKey.VK_U] = ["ŭ"],
|
||||
}),
|
||||
|
||||
new(Language.FI, "Finnish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ä", "å"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_O] = ["ö"],
|
||||
[LetterKey.VK_COMMA] = ["”", "’", "»"],
|
||||
}),
|
||||
|
||||
new(Language.FR, "French", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à", "â", "á", "ä", "ã", "æ"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["é", "è", "ê", "ë", "€"],
|
||||
[LetterKey.VK_I] = ["î", "ï", "í", "ì"],
|
||||
[LetterKey.VK_O] = ["ô", "ö", "ó", "ò", "õ", "œ"],
|
||||
[LetterKey.VK_U] = ["û", "ù", "ü", "ú"],
|
||||
[LetterKey.VK_Y] = ["ÿ", "ý"],
|
||||
[LetterKey.VK_COMMA] = ["«", "»", "‹", "›", "“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.GRC, "Greek_Polytonic", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["α", "ἀ", "ἁ", "ὰ", "ά", "ᾶ", "ᾱ", "ᾰ", "ἂ", "ἃ", "ἄ", "ἅ", "ἆ", "ἇ", "ᾳ", "ᾀ", "ᾁ", "ᾴ", "ᾲ", "ᾷ", "ᾄ", "ᾅ", "ᾂ", "ᾃ", "ᾆ", "ᾇ"],
|
||||
[LetterKey.VK_B] = ["β"],
|
||||
[LetterKey.VK_C] = ["χ", "ϲ"],
|
||||
[LetterKey.VK_D] = ["δ"],
|
||||
[LetterKey.VK_E] = ["ε", "ἐ", "ἑ", "ὲ", "έ", "ἒ", "ἓ", "ἔ", "ἕ"],
|
||||
[LetterKey.VK_F] = ["φ", "ϝ"],
|
||||
[LetterKey.VK_G] = ["γ"],
|
||||
[LetterKey.VK_H] = ["η", "ἠ", "ἡ", "ὴ", "ή", "ῆ", "ἢ", "ἣ", "ἤ", "ἥ", "ἦ", "ἧ", "ῃ", "ᾐ", "ᾑ", "ῄ", "ῂ", "ῇ", "ᾔ", "ᾕ", "ᾒ", "ᾓ", "ᾖ", "ᾗ"],
|
||||
[LetterKey.VK_I] = ["ι", "ἰ", "ἱ", "ὶ", "ί", "ῖ", "ῑ", "ῐ", "ἲ", "ἳ", "ἴ", "ἵ", "ἶ", "ἷ", "ϊ", "ΐ", "ῒ", "ῗ"],
|
||||
[LetterKey.VK_K] = ["κ"],
|
||||
[LetterKey.VK_L] = ["λ"],
|
||||
[LetterKey.VK_M] = ["μ"],
|
||||
[LetterKey.VK_N] = ["ν"],
|
||||
[LetterKey.VK_O] = ["ο", "ὀ", "ὁ", "ὸ", "ό", "ὂ", "ὃ", "ὄ", "ὅ"],
|
||||
[LetterKey.VK_P] = ["π", "φ", "ψ", "ρ"],
|
||||
[LetterKey.VK_Q] = ["ϙ", "ϟ"],
|
||||
[LetterKey.VK_R] = ["ρ", "ῤ", "ῥ"],
|
||||
[LetterKey.VK_S] = ["σ", "ς", "ϛ", "ϲ", "ϡ"],
|
||||
[LetterKey.VK_T] = ["τ", "θ", "ϑ"],
|
||||
[LetterKey.VK_U] = ["υ", "ὐ", "ὑ", "ὺ", "ύ", "ῦ", "ῡ", "ῠ", "ὒ", "ὓ", "ὔ", "ὕ", "ὖ", "ὗ", "ϋ", "ΰ", "ῢ", "ῧ"],
|
||||
[LetterKey.VK_V] = ["β", "ϝ"],
|
||||
[LetterKey.VK_W] = ["ω", "ὠ", "ὡ", "ὼ", "ώ", "ῶ", "ὢ", "ὣ", "ὤ", "ὥ", "ὦ", "ὧ", "ῳ", "ᾠ", "ᾡ", "ῴ", "ῲ", "ῷ", "ᾤ", "ᾥ", "ᾢ", "ᾣ", "ᾦ", "ᾧ"],
|
||||
[LetterKey.VK_X] = ["ξ", "χ"],
|
||||
[LetterKey.VK_Y] = ["υ", "ὐ", "ὑ", "ὺ", "ύ", "ῦ", "ῡ", "ῠ", "ὒ", "ὓ", "ὔ", "ὕ", "ὖ", "ὗ", "ϋ", "ΰ", "ῢ", "ῧ"],
|
||||
[LetterKey.VK_Z] = ["ζ"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’", ";", "`", "´"],
|
||||
[LetterKey.VK_PERIOD] = ["·"],
|
||||
}),
|
||||
|
||||
new(Language.HR, "Croatian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_C] = ["ć", "č"],
|
||||
[LetterKey.VK_D] = ["đ"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "»", "«"],
|
||||
}),
|
||||
|
||||
new(Language.HE, "Hebrew", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["שׂ", "שׁ", "\u05b0"],
|
||||
[LetterKey.VK_B] = ["׆"],
|
||||
[LetterKey.VK_E] = ["\u05b8", "\u05b3", "\u05bb"],
|
||||
[LetterKey.VK_G] = ["ױ"],
|
||||
[LetterKey.VK_H] = ["ײ", "ײַ", "ׯ", "\u05b4"],
|
||||
[LetterKey.VK_M] = ["\u05b5"],
|
||||
[LetterKey.VK_P] = ["\u05b7", "\u05b2"],
|
||||
[LetterKey.VK_S] = ["\u05bc"],
|
||||
[LetterKey.VK_T] = ["ﭏ"],
|
||||
[LetterKey.VK_U] = ["וֹ", "וּ", "װ", "\u05b9"],
|
||||
[LetterKey.VK_X] = ["\u05b6", "\u05b1"],
|
||||
[LetterKey.VK_Y] = ["ױ"],
|
||||
[LetterKey.VK_COMMA] = ["”", "’", "'", "״", "׳"],
|
||||
[LetterKey.VK_PERIOD] = ["\u05ab", "\u05bd", "\u05bf"],
|
||||
[LetterKey.VK_MINUS] = ["־"],
|
||||
}),
|
||||
|
||||
new(Language.HU, "Hungarian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á"],
|
||||
[LetterKey.VK_E] = ["é"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_O] = ["ó", "ő", "ö"],
|
||||
[LetterKey.VK_U] = ["ú", "ű", "ü"],
|
||||
[LetterKey.VK_Y] = ["ÿ", "ý"],
|
||||
[LetterKey.VK_COMMA] = ["„", "”", "»", "«"],
|
||||
}),
|
||||
|
||||
new(Language.IS, "Icelandic", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á", "æ"],
|
||||
[LetterKey.VK_D] = ["ð"],
|
||||
[LetterKey.VK_E] = ["é"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_O] = ["ó", "ö"],
|
||||
[LetterKey.VK_U] = ["ú"],
|
||||
[LetterKey.VK_Y] = ["ý"],
|
||||
[LetterKey.VK_T] = ["þ"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "‘"],
|
||||
}),
|
||||
|
||||
// International Phonetic Alphabet. This is a "special" language group as it's not
|
||||
// a spoken language, but rather a set of symbols used across languages.
|
||||
new(Language.IPA, "IPA", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ɐ", "ɑ", "ɒ", "ǎ"],
|
||||
[LetterKey.VK_B] = ["ʙ"],
|
||||
[LetterKey.VK_E] = ["ɘ", "ɵ", "ə", "ɛ", "ɜ", "ɞ"],
|
||||
[LetterKey.VK_F] = ["ɟ", "ɸ"],
|
||||
[LetterKey.VK_G] = ["ɢ", "ɣ"],
|
||||
[LetterKey.VK_H] = ["ɦ", "ʜ"],
|
||||
[LetterKey.VK_I] = ["ɨ", "ɪ"],
|
||||
[LetterKey.VK_J] = ["ʝ"],
|
||||
[LetterKey.VK_L] = ["ɬ", "ɮ", "ꞎ", "ɭ", "ʎ", "ʟ", "ɺ"],
|
||||
[LetterKey.VK_N] = ["ɳ", "ɲ", "ŋ", "ɴ"],
|
||||
[LetterKey.VK_O] = ["ɤ", "ɔ", "ɶ", "ǒ"],
|
||||
[LetterKey.VK_R] = ["ʁ", "ɹ", "ɻ", "ɾ", "ɽ", "ʀ"],
|
||||
[LetterKey.VK_S] = ["ʃ", "ʂ", "ɕ"],
|
||||
[LetterKey.VK_U] = ["ʉ", "ʊ", "ǔ"],
|
||||
[LetterKey.VK_V] = ["ʋ", "ⱱ", "ʌ"],
|
||||
[LetterKey.VK_W] = ["ɰ", "ɯ"],
|
||||
[LetterKey.VK_Y] = ["ʏ"],
|
||||
[LetterKey.VK_Z] = ["ʒ", "ʐ", "ʑ"],
|
||||
[LetterKey.VK_COMMA] = ["ʡ", "ʔ", "ʕ", "ʢ"],
|
||||
}),
|
||||
|
||||
new(Language.IT, "Italian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à"],
|
||||
[LetterKey.VK_E] = ["è", "é", "ə", "€"],
|
||||
[LetterKey.VK_I] = ["ì", "í"],
|
||||
[LetterKey.VK_O] = ["ò", "ó"],
|
||||
[LetterKey.VK_U] = ["ù", "ú"],
|
||||
[LetterKey.VK_COMMA] = ["«", "»", "“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.KU, "Kurdish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["ê", "€"],
|
||||
[LetterKey.VK_I] = ["î"],
|
||||
[LetterKey.VK_O] = ["ö", "ô"],
|
||||
[LetterKey.VK_L] = ["ł"],
|
||||
[LetterKey.VK_N] = ["ň"],
|
||||
[LetterKey.VK_R] = ["ř"],
|
||||
[LetterKey.VK_S] = ["ş"],
|
||||
[LetterKey.VK_U] = ["û", "ü"],
|
||||
[LetterKey.VK_COMMA] = ["«", "»", "“", "”"],
|
||||
}),
|
||||
|
||||
new(Language.LT, "Lithuanian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ą"],
|
||||
[LetterKey.VK_C] = ["č"],
|
||||
[LetterKey.VK_E] = ["ę", "ė", "€"],
|
||||
[LetterKey.VK_I] = ["į"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_U] = ["ų", "ū"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "‘"],
|
||||
}),
|
||||
|
||||
new(Language.MK, "Macedonian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_E] = ["ѐ"],
|
||||
[LetterKey.VK_I] = ["ѝ"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "’", "‘"],
|
||||
}),
|
||||
|
||||
new(Language.MT, "Maltese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à"],
|
||||
[LetterKey.VK_C] = ["ċ"],
|
||||
[LetterKey.VK_E] = ["è", "€"],
|
||||
[LetterKey.VK_G] = ["ġ"],
|
||||
[LetterKey.VK_H] = ["ħ"],
|
||||
[LetterKey.VK_I] = ["ì"],
|
||||
[LetterKey.VK_O] = ["ò"],
|
||||
[LetterKey.VK_U] = ["ù"],
|
||||
[LetterKey.VK_Z] = ["ż"],
|
||||
}),
|
||||
|
||||
new(Language.MI, "Maori", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ā"],
|
||||
[LetterKey.VK_E] = ["ē"],
|
||||
[LetterKey.VK_I] = ["ī"],
|
||||
[LetterKey.VK_O] = ["ō"],
|
||||
[LetterKey.VK_S] = ["$"],
|
||||
[LetterKey.VK_U] = ["ū"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.NL, "Dutch", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á", "à", "ä"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["é", "è", "ë", "ê", "€"],
|
||||
[LetterKey.VK_I] = ["í", "ï", "î"],
|
||||
[LetterKey.VK_N] = ["ñ"],
|
||||
[LetterKey.VK_O] = ["ó", "ö", "ô"],
|
||||
[LetterKey.VK_U] = ["ú", "ü", "û"],
|
||||
[LetterKey.VK_COMMA] = ["“", "„", "”", "‘", ",", "’"],
|
||||
}),
|
||||
|
||||
new(Language.NO, "Norwegian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["å", "æ"],
|
||||
[LetterKey.VK_E] = ["€", "é"],
|
||||
[LetterKey.VK_O] = ["ø"],
|
||||
[LetterKey.VK_S] = ["$"],
|
||||
[LetterKey.VK_COMMA] = ["«", "»", ",", "‘", "’", "„", "“"],
|
||||
}),
|
||||
|
||||
new(Language.PI, "Pinyin", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_1] = ["\u0304", "ˉ"],
|
||||
[LetterKey.VK_2] = ["\u0301", "ˊ"],
|
||||
[LetterKey.VK_3] = ["\u030c", "ˇ"],
|
||||
[LetterKey.VK_4] = ["\u0300", "ˋ"],
|
||||
[LetterKey.VK_5] = ["·"],
|
||||
[LetterKey.VK_A] = ["ā", "á", "ǎ", "à", "ɑ", "ɑ\u0304", "ɑ\u0301", "ɑ\u030c", "ɑ\u0300"],
|
||||
[LetterKey.VK_C] = ["ĉ"],
|
||||
[LetterKey.VK_E] = ["ē", "é", "ě", "è", "ê", "ê\u0304", "ế", "ê\u030c", "ề"],
|
||||
[LetterKey.VK_I] = ["ī", "í", "ǐ", "ì"],
|
||||
[LetterKey.VK_M] = ["m\u0304", "ḿ", "m\u030c", "m\u0300"],
|
||||
[LetterKey.VK_N] = ["n\u0304", "ń", "ň", "ǹ", "ŋ", "ŋ\u0304", "ŋ\u0301", "ŋ\u030c", "ŋ\u0300"],
|
||||
[LetterKey.VK_O] = ["ō", "ó", "ǒ", "ò"],
|
||||
[LetterKey.VK_S] = ["ŝ"],
|
||||
[LetterKey.VK_U] = ["ū", "ú", "ǔ", "ù", "ü", "ǖ", "ǘ", "ǚ", "ǜ"],
|
||||
[LetterKey.VK_V] = ["ü", "ǖ", "ǘ", "ǚ", "ǜ"],
|
||||
[LetterKey.VK_Y] = ["¥"],
|
||||
[LetterKey.VK_Z] = ["ẑ"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’", "「", "」", "『", "』"],
|
||||
}),
|
||||
|
||||
// Proto-Indo-European. This is a "special" language group as it's not a spoken
|
||||
// language, but rather a reconstructed ancestor of many languages.
|
||||
new(Language.PIE, "Proto_Indo_European", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ā"],
|
||||
[LetterKey.VK_E] = ["ē"],
|
||||
[LetterKey.VK_O] = ["ō"],
|
||||
[LetterKey.VK_K] = ["ḱ"],
|
||||
[LetterKey.VK_G] = ["ǵ"],
|
||||
[LetterKey.VK_R] = ["r̥"],
|
||||
[LetterKey.VK_L] = ["l̥"],
|
||||
[LetterKey.VK_M] = ["m̥"],
|
||||
[LetterKey.VK_N] = ["n̥"],
|
||||
}),
|
||||
|
||||
new(Language.PL, "Polish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ą"],
|
||||
[LetterKey.VK_C] = ["ć"],
|
||||
[LetterKey.VK_E] = ["ę", "€"],
|
||||
[LetterKey.VK_L] = ["ł"],
|
||||
[LetterKey.VK_N] = ["ń"],
|
||||
[LetterKey.VK_O] = ["ó"],
|
||||
[LetterKey.VK_S] = ["ś"],
|
||||
[LetterKey.VK_Z] = ["ż", "ź"],
|
||||
[LetterKey.VK_COMMA] = ["„", "”", "‘", "’", "»", "«"],
|
||||
}),
|
||||
|
||||
new(Language.PT, "Portuguese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á", "à", "â", "ã", "ª"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["é", "ê", "€"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_O] = ["ô", "ó", "õ", "º"],
|
||||
[LetterKey.VK_S] = ["$"],
|
||||
[LetterKey.VK_U] = ["ú"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’", "«", "»"],
|
||||
}),
|
||||
|
||||
new(Language.RO, "Romanian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["ă", "â"],
|
||||
[LetterKey.VK_I] = ["î"],
|
||||
[LetterKey.VK_S] = ["ș"],
|
||||
[LetterKey.VK_T] = ["ț"],
|
||||
[LetterKey.VK_COMMA] = ["„", "”", "«", "»"],
|
||||
}),
|
||||
|
||||
// Middle Eastern Romanization. This is a "special" language group as it's not a
|
||||
// spoken language, but rather a set of characters used to romanize various Middle
|
||||
// Eastern languages.
|
||||
new(Language.ROM, "Romanization", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á", "â", "ă", "ā"],
|
||||
[LetterKey.VK_B] = ["ḇ"],
|
||||
[LetterKey.VK_C] = ["č", "ç"],
|
||||
[LetterKey.VK_D] = ["ḑ", "ḍ", "ḏ", "ḏ\u0323"],
|
||||
[LetterKey.VK_E] = ["ê", "ě", "ĕ", "ē", "é", "ə"],
|
||||
[LetterKey.VK_G] = ["ġ", "ǧ", "ğ", "ḡ", "g\u0303", "g\u0331"],
|
||||
[LetterKey.VK_H] = ["ḧ", "ḩ", "ḥ", "ḫ", "h\u0331"],
|
||||
[LetterKey.VK_I] = ["í", "ı", "î", "ī", "ı\u0307\u0304"],
|
||||
[LetterKey.VK_J] = ["ǰ", "j\u0331"],
|
||||
[LetterKey.VK_K] = ["ḳ", "ḵ"],
|
||||
[LetterKey.VK_L] = ["ł"],
|
||||
[LetterKey.VK_N] = ["ⁿ", "ñ"],
|
||||
[LetterKey.VK_O] = ["ó", "ô", "ö", "ŏ", "ō", "ȫ"],
|
||||
[LetterKey.VK_P] = ["p\u0304"],
|
||||
[LetterKey.VK_R] = ["ṙ", "ṛ"],
|
||||
[LetterKey.VK_S] = ["ś", "š", "ş", "ṣ", "s\u0331", "ṣ\u0304"],
|
||||
[LetterKey.VK_T] = ["ẗ", "ţ", "ṭ", "ṯ"],
|
||||
[LetterKey.VK_U] = ["ú", "û", "ü", "ū", "ǖ"],
|
||||
[LetterKey.VK_V] = ["v\u0307", "ṿ", "ᵛ"],
|
||||
[LetterKey.VK_Y] = ["̀y"],
|
||||
[LetterKey.VK_Z] = ["ż", "ž", "z\u0304", "z\u0327", "ẓ", "z\u0324", "ẕ"],
|
||||
[LetterKey.VK_PERIOD] = ["’", "ʾ", "ʿ", "′", "…"],
|
||||
}),
|
||||
|
||||
new(Language.SK, "Slovak", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á", "ä"],
|
||||
[LetterKey.VK_C] = ["č"],
|
||||
[LetterKey.VK_D] = ["ď"],
|
||||
[LetterKey.VK_E] = ["é", "€"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_L] = ["ľ", "ĺ"],
|
||||
[LetterKey.VK_N] = ["ň"],
|
||||
[LetterKey.VK_O] = ["ó", "ô"],
|
||||
[LetterKey.VK_R] = ["ŕ"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_T] = ["ť"],
|
||||
[LetterKey.VK_U] = ["ú"],
|
||||
[LetterKey.VK_Y] = ["ý"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "‘", "»", "«", "›", "‹"],
|
||||
}),
|
||||
|
||||
new(Language.SL, "Slovenian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_C] = ["č", "ć"],
|
||||
[LetterKey.VK_E] = ["€"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "»", "«"],
|
||||
}),
|
||||
|
||||
new(Language.SP, "Spanish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["á"],
|
||||
[LetterKey.VK_E] = ["é", "€"],
|
||||
[LetterKey.VK_H] = ["ḥ"],
|
||||
[LetterKey.VK_I] = ["í"],
|
||||
[LetterKey.VK_L] = ["ḷ"],
|
||||
[LetterKey.VK_N] = ["ñ"],
|
||||
[LetterKey.VK_O] = ["ó"],
|
||||
[LetterKey.VK_U] = ["ú", "ü"],
|
||||
[LetterKey.VK_COMMA] = ["¿", "?", "¡", "!", "«", "»", "“", "”", "‘", "’"],
|
||||
}),
|
||||
|
||||
new(Language.SR, "Serbian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_C] = ["ć", "č"],
|
||||
[LetterKey.VK_D] = ["đ"],
|
||||
[LetterKey.VK_S] = ["š"],
|
||||
[LetterKey.VK_Z] = ["ž"],
|
||||
[LetterKey.VK_COMMA] = ["„", "“", "‚", "’", "»", "«", "›", "‹"],
|
||||
}),
|
||||
|
||||
new(Language.SR_CYRL, "Serbian_Cyrillic", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_D] = ["ђ", "џ"],
|
||||
[LetterKey.VK_L] = ["љ"],
|
||||
[LetterKey.VK_N] = ["њ"],
|
||||
[LetterKey.VK_C] = ["ћ"],
|
||||
}),
|
||||
|
||||
new(Language.SV, "Swedish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["å", "ä"],
|
||||
[LetterKey.VK_E] = ["é"],
|
||||
[LetterKey.VK_O] = ["ö"],
|
||||
[LetterKey.VK_COMMA] = ["”", "’", "»", "«"],
|
||||
}),
|
||||
|
||||
new(Language.TK, "Turkish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["â"],
|
||||
[LetterKey.VK_C] = ["ç"],
|
||||
[LetterKey.VK_E] = ["ë", "€"],
|
||||
[LetterKey.VK_G] = ["ğ"],
|
||||
[LetterKey.VK_I] = ["ı", "İ", "î",],
|
||||
[LetterKey.VK_O] = ["ö", "ô"],
|
||||
[LetterKey.VK_S] = ["ş"],
|
||||
[LetterKey.VK_T] = ["₺"],
|
||||
[LetterKey.VK_U] = ["ü", "û"],
|
||||
[LetterKey.VK_COMMA] = ["“", "”", "‘", "’", "«", "»", "‹", "›"],
|
||||
}),
|
||||
|
||||
new(Language.VI, "Vietnamese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
|
||||
{
|
||||
[LetterKey.VK_A] = ["à", "ả", "ã", "á", "ạ", "ă", "ằ", "ẳ", "ẵ", "ắ", "ặ", "â", "ầ", "ẩ", "ẫ", "ấ", "ậ"],
|
||||
[LetterKey.VK_D] = ["đ"],
|
||||
[LetterKey.VK_E] = ["è", "ẻ", "ẽ", "é", "ẹ", "ê", "ề", "ể", "ễ", "ế", "ệ"],
|
||||
[LetterKey.VK_I] = ["ì", "ỉ", "ĩ", "í", "ị"],
|
||||
[LetterKey.VK_O] = ["ò", "ỏ", "õ", "ó", "ọ", "ô", "ồ", "ổ", "ỗ", "ố", "ộ", "ơ", "ờ", "ở", "ỡ", "ớ", "ợ"],
|
||||
[LetterKey.VK_U] = ["ù", "ủ", "ũ", "ú", "ụ", "ư", "ừ", "ử", "ữ", "ứ", "ự"],
|
||||
[LetterKey.VK_Y] = ["ỳ", "ỷ", "ỹ", "ý", "ỵ"],
|
||||
}),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// O(1) lookup from <see cref="Language"/> to its <see cref="LanguageInfo"/>.
|
||||
/// Use this instead of searching <see cref="All"/> when you have a language identity.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<Language, LanguageInfo> LanguageLookup =
|
||||
All.ToDictionary(x => x.Id);
|
||||
|
||||
/// <summary>
|
||||
/// The order in which language groups appear in the Quick Accent popup.
|
||||
/// Groups listed first have their characters shown first.
|
||||
/// This is intentionally separate from the <see cref="LanguageGroup"/> enum order.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<LanguageGroup> GroupDisplayOrder =
|
||||
[
|
||||
LanguageGroup.UserDefined,
|
||||
LanguageGroup.Language,
|
||||
LanguageGroup.Special,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The order in which individual languages appear within their group in the Quick
|
||||
/// Accent popup. Position in this list is the display order; position in
|
||||
/// <see cref="All"/> is irrelevant for popup ordering.
|
||||
/// Entries are sorted alphabetically by <see cref="Language"/> enum name.
|
||||
/// When adding a new language, insert it in alphabetical order.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<Language> DisplayOrder =
|
||||
[
|
||||
|
||||
// Spoken languages.
|
||||
Language.BG,
|
||||
Language.CA,
|
||||
Language.CRH,
|
||||
Language.CY,
|
||||
Language.CZ,
|
||||
Language.DE,
|
||||
Language.DK,
|
||||
Language.EL,
|
||||
Language.EPO,
|
||||
Language.EST,
|
||||
Language.FI,
|
||||
Language.FR,
|
||||
Language.GA,
|
||||
Language.GD,
|
||||
Language.HE,
|
||||
Language.HR,
|
||||
Language.HU,
|
||||
Language.IS,
|
||||
Language.IT,
|
||||
Language.KU,
|
||||
Language.LT,
|
||||
Language.MI,
|
||||
Language.MK,
|
||||
Language.MT,
|
||||
Language.NL,
|
||||
Language.NO,
|
||||
Language.PI,
|
||||
Language.PL,
|
||||
Language.PT,
|
||||
Language.RO,
|
||||
Language.SK,
|
||||
Language.SL,
|
||||
Language.SP,
|
||||
Language.SR,
|
||||
Language.SR_CYRL,
|
||||
Language.SV,
|
||||
Language.TK,
|
||||
Language.VI,
|
||||
|
||||
// Symbols, non-spoken languages, and non-language-specific characters.
|
||||
Language.CUR,
|
||||
Language.GRC,
|
||||
Language.IPA,
|
||||
Language.PIE,
|
||||
Language.ROM,
|
||||
Language.SPECIAL,
|
||||
];
|
||||
|
||||
// O(1) sort-key lookups derived from the display order lists above.
|
||||
private static readonly Dictionary<LanguageGroup, int> _groupOrder =
|
||||
GroupDisplayOrder.Select((g, i) => (g, i)).ToDictionary(x => x.g, x => x.i);
|
||||
|
||||
private static readonly Dictionary<Language, int> _languageOrder =
|
||||
DisplayOrder.Select((l, i) => (l, i)).ToDictionary(x => x.l, x => x.i);
|
||||
|
||||
private static readonly ConcurrentDictionary<LetterKey, string[]> _allLanguagesCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the deduplicated set of characters for the given key across the specified
|
||||
/// languages, ordered by <see cref="GroupDisplayOrder"/> then <see cref="DisplayOrder"/>.
|
||||
/// </summary>
|
||||
public static string[] GetCharacters(LetterKey letter, Language[] langs)
|
||||
{
|
||||
if (langs.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (langs.Length == All.Count)
|
||||
{
|
||||
return _allLanguagesCache.GetOrAdd(letter, key => Collect(key, All));
|
||||
}
|
||||
|
||||
return Collect(letter, langs.Select(lang => LanguageLookup[lang]));
|
||||
}
|
||||
|
||||
private static string[] Collect(LetterKey letter, IEnumerable<LanguageInfo> maps)
|
||||
{
|
||||
var result = new List<string>();
|
||||
foreach (var map in maps
|
||||
.OrderBy(m => _groupOrder[m.Group])
|
||||
.ThenBy(m => _languageOrder[m.Id]))
|
||||
{
|
||||
if (map.Characters.TryGetValue(letter, out var chars))
|
||||
{
|
||||
result.AddRange(chars);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. result.Distinct()];
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerAccent.Common;
|
||||
|
||||
public enum Language
|
||||
{
|
||||
SPECIAL,
|
||||
BG,
|
||||
CA,
|
||||
CRH,
|
||||
CUR,
|
||||
CY,
|
||||
CZ,
|
||||
DK,
|
||||
GA,
|
||||
GD,
|
||||
DE,
|
||||
EL,
|
||||
EST,
|
||||
EPO,
|
||||
FI,
|
||||
FR,
|
||||
GRC,
|
||||
HR,
|
||||
HE,
|
||||
HU,
|
||||
IS,
|
||||
IPA,
|
||||
IT,
|
||||
KU,
|
||||
LT,
|
||||
MK,
|
||||
MT,
|
||||
MI,
|
||||
NL,
|
||||
NO,
|
||||
PI,
|
||||
PIE,
|
||||
PL,
|
||||
PT,
|
||||
RO,
|
||||
ROM,
|
||||
SK,
|
||||
SL,
|
||||
SP,
|
||||
SR,
|
||||
SR_CYRL,
|
||||
SV,
|
||||
TK,
|
||||
VI,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerAccent.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Describes which category a language belongs to in the Quick Accent settings UI.
|
||||
/// </summary>
|
||||
public enum LanguageGroup
|
||||
{
|
||||
/// <summary>Standard spoken languages.</summary>
|
||||
Language,
|
||||
|
||||
/// <summary>Special character sets (e.g. currencies, IPA, romanization).</summary>
|
||||
Special,
|
||||
|
||||
/// <summary>User-defined custom character sets.</summary>
|
||||
UserDefined,
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerAccent.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a single language entry: its enum identity, the resource key identifier
|
||||
/// used to look up its localized display name, which group it belongs to, and its
|
||||
/// character mappings.
|
||||
/// </summary>
|
||||
/// <param name="Id">The <see cref="Language"/> enum value for this entry.</param>
|
||||
/// <param name="Identifier">
|
||||
/// The stable string identifier used to construct the settings resource key
|
||||
/// (e.g. <c>"Bulgarian"</c> -> <c>QuickAccent_SelectedLanguage_Bulgarian</c>).
|
||||
/// </param>
|
||||
/// <param name="Group">Which <see cref="LanguageGroup"/> category this entry belongs to.
|
||||
/// </param>
|
||||
/// <param name="Characters">The character mappings for this language.</param>
|
||||
public sealed record LanguageInfo(
|
||||
Language Id,
|
||||
string Identifier,
|
||||
LanguageGroup Group,
|
||||
IReadOnlyDictionary<LetterKey, string[]> Characters);
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerAccent.Common;
|
||||
|
||||
// Mirrors the LetterKey enum defined in PowerAccentKeyboardService\KeyboardListener.idl.
|
||||
// The numeric values must stay in sync with the IDL definition.
|
||||
// This managed copy exists so that language mapping data in CharacterMappings.cs can be shared
|
||||
// with projects (e.g. Settings UI) that do not reference the WinRT keyboard service.
|
||||
public enum LetterKey
|
||||
{
|
||||
None = 0x00,
|
||||
VK_0 = 0x30,
|
||||
VK_1 = 0x31,
|
||||
VK_2 = 0x32,
|
||||
VK_3 = 0x33,
|
||||
VK_4 = 0x34,
|
||||
VK_5 = 0x35,
|
||||
VK_6 = 0x36,
|
||||
VK_7 = 0x37,
|
||||
VK_8 = 0x38,
|
||||
VK_9 = 0x39,
|
||||
VK_A = 0x41,
|
||||
VK_B = 0x42,
|
||||
VK_C = 0x43,
|
||||
VK_D = 0x44,
|
||||
VK_E = 0x45,
|
||||
VK_F = 0x46,
|
||||
VK_G = 0x47,
|
||||
VK_H = 0x48,
|
||||
VK_I = 0x49,
|
||||
VK_J = 0x4A,
|
||||
VK_K = 0x4B,
|
||||
VK_L = 0x4C,
|
||||
VK_M = 0x4D,
|
||||
VK_N = 0x4E,
|
||||
VK_O = 0x4F,
|
||||
VK_P = 0x50,
|
||||
VK_Q = 0x51,
|
||||
VK_R = 0x52,
|
||||
VK_S = 0x53,
|
||||
VK_T = 0x54,
|
||||
VK_U = 0x55,
|
||||
VK_V = 0x56,
|
||||
VK_W = 0x57,
|
||||
VK_X = 0x58,
|
||||
VK_Y = 0x59,
|
||||
VK_Z = 0x5A,
|
||||
VK_PLUS = 0xBB,
|
||||
VK_COMMA = 0xBC,
|
||||
VK_PERIOD = 0xBE,
|
||||
VK_MINUS = 0xBD,
|
||||
VK_MULTIPLY_ = 0x6A,
|
||||
VK_SLASH_ = 0xBF,
|
||||
VK_DIVIDE_ = 0x6F,
|
||||
VK_BACKSLASH = 0xDC,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Currently hard-coded, as this project does not target WinRT.
|
||||
To be removed after non-WinRT information is moved from
|
||||
Common.Dotnet.CsWinRT.props. -->
|
||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
// Character mapping data has moved to PowerAccent.Common.CharacterMappings.
|
||||
// This file provides a thin adapter so that callers in PowerAccent.Core can pass the
|
||||
// WinRT LetterKey (from PowerAccentKeyboardService) without needing to know about the
|
||||
// managed copy defined in PowerAccent.Common.
|
||||
using CommonLanguage = global::PowerAccent.Common.Language;
|
||||
using CommonLetterKey = global::PowerAccent.Common.LetterKey;
|
||||
using CommonMappings = global::PowerAccent.Common.CharacterMappings;
|
||||
using WinRtLetterKey = PowerToys.PowerAccentKeyboardService.LetterKey;
|
||||
|
||||
namespace PowerAccent.Core;
|
||||
|
||||
internal static class CharacterMappings
|
||||
{
|
||||
public static string[] GetCharacters(WinRtLetterKey letter, CommonLanguage[] langs)
|
||||
{
|
||||
// The managed and WinRT LetterKey enums share identical numeric values, so a
|
||||
// direct cast via int is safe. If the IDL values ever change, the unit tests
|
||||
// in PowerAccent.Common will catch the mismatch.
|
||||
var managedKey = (CommonLetterKey)(int)letter;
|
||||
return CommonMappings.GetCharacters(managedKey, langs);
|
||||
}
|
||||
}
|
||||
1030
src/modules/poweraccent/PowerAccent.Core/Languages.cs
Normal file
1030
src/modules/poweraccent/PowerAccent.Core/Languages.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,6 @@
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\PowerAccent.Common\PowerAccent.Common.csproj" />
|
||||
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -89,7 +89,7 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
_keyboardListener.SetIsLanguageLetterDelegate(new PowerToys.PowerAccentKeyboardService.IsLanguageLetter((LetterKey letterKey, out bool result) =>
|
||||
{
|
||||
result = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang).Length > 0;
|
||||
result = Languages.GetDefaultLetterKey(letterKey, _settingService.SelectedLang).Length > 0;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public partial class PowerAccent : IDisposable
|
||||
|
||||
private string[] GetCharacters(LetterKey letterKey)
|
||||
{
|
||||
var characters = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang);
|
||||
var characters = Languages.GetDefaultLetterKey(letterKey, _settingService.SelectedLang);
|
||||
if (_settingService.SortByUsageFrequency)
|
||||
{
|
||||
characters = characters.OrderByDescending(character => _usageInfo.GetUsageFrequency(character))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -11,7 +11,6 @@ using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
using PowerAccent.Core.SerializationContext;
|
||||
using PowerToys.PowerAccentKeyboardService;
|
||||
using Language = global::PowerAccent.Common.Language;
|
||||
|
||||
namespace PowerAccent.Core.Services;
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "KeyboardListener.g.h"
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <spdlog/stopwatch.h>
|
||||
|
||||
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BuiltInMonitorBlacklistTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Entries_LoadsWithoutThrowing()
|
||||
{
|
||||
var entries = BuiltInMonitorBlacklist.Entries;
|
||||
|
||||
Assert.IsNotNull(entries);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Entries_AreNormalizedToUpperCase()
|
||||
{
|
||||
foreach (var entry in BuiltInMonitorBlacklist.Entries)
|
||||
{
|
||||
Assert.AreEqual(
|
||||
entry.EdidId,
|
||||
entry.EdidId.ToUpperInvariant(),
|
||||
$"Entry '{entry.EdidId}' is not normalized to uppercase.");
|
||||
Assert.AreEqual(
|
||||
entry.EdidId.Trim(),
|
||||
entry.EdidId,
|
||||
$"Entry '{entry.EdidId}' has untrimmed whitespace.");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Entries_ContainNoEmptyEdidIds()
|
||||
{
|
||||
Assert.IsFalse(
|
||||
BuiltInMonitorBlacklist.Entries.Any(e => string.IsNullOrWhiteSpace(e.EdidId)),
|
||||
"Built-in list should never contain blank EdidId entries.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Entries_AreCached()
|
||||
{
|
||||
var first = BuiltInMonitorBlacklist.Entries;
|
||||
var second = BuiltInMonitorBlacklist.Entries;
|
||||
|
||||
Assert.AreSame(first, second, "Entries should be returned from a cached Lazy<>.");
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Services;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class MonitorBlacklistServiceTests
|
||||
{
|
||||
private const string SamplePathDel = @"\\?\DISPLAY#DELD1A8#5&abc123&0&UID12345#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
|
||||
private const string SamplePathBoe = @"\\?\DISPLAY#BOE0900#4&xyz&0&UID0";
|
||||
|
||||
[TestMethod]
|
||||
public void IsBlocked_EmptyBuiltIn_ReturnsFalse()
|
||||
{
|
||||
// Built-in list ships empty in this release, so the service should never block.
|
||||
var service = new MonitorBlacklistService();
|
||||
|
||||
Assert.IsFalse(service.IsBlocked(SamplePathDel));
|
||||
Assert.IsFalse(service.IsBlocked(SamplePathBoe));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsBlocked_EmptyOrUnknownMonitorId_ReturnsFalse()
|
||||
{
|
||||
var service = new MonitorBlacklistService();
|
||||
|
||||
Assert.IsFalse(service.IsBlocked(string.Empty));
|
||||
Assert.IsFalse(service.IsBlocked(null!));
|
||||
Assert.IsFalse(service.IsBlocked(@"\\?\DISPLAY"));
|
||||
Assert.IsFalse(service.IsBlocked(@"garbage no hashes here"));
|
||||
}
|
||||
}
|
||||
@@ -703,16 +703,6 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
#endif
|
||||
|
||||
// Log identity of the monitor we are about to touch via DDC/CI BEFORE the
|
||||
// first syscall. If the call triggers a kernel stack-cookie overrun inside
|
||||
// win32kfull (see GH #47556 / #47968), this is the last log line that
|
||||
// survives — it has to carry enough to identify the offending hardware:
|
||||
// EdidId for blacklist matching, plus the human-readable name and full
|
||||
// DevicePath.
|
||||
var edidId = MonitorIdentity.EdidIdFromMonitorId(info.DevicePath);
|
||||
Logger.LogInfo(
|
||||
$"DDC: probing capabilities [EdidId={edidId}] [FriendlyName='{info.FriendlyName}'] [DevicePath={info.DevicePath}]");
|
||||
|
||||
// Async caps fetch (retry + max-compat probe). Awaits Task.Delay between
|
||||
// retries instead of blocking the threadpool.
|
||||
var (capsString, caps) = await FetchCapabilitiesWithFallbackAsync(
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Models;
|
||||
|
||||
namespace PowerDisplay.Common.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Decides whether a monitor identified by its <c>Monitor.Id</c> (or raw DevicePath)
|
||||
/// should be filtered out of PowerDisplay's discovery. Matches on EdidId only —
|
||||
/// model-level granularity, so a single entry covers every physical port and every
|
||||
/// machine with the same monitor model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the built-in list shipped with PowerToys is consulted; user-customized
|
||||
/// blacklists were considered but cut due to UI cost. EdidIds are normalized
|
||||
/// (trimmed, upper-cased) on construction; comparisons use
|
||||
/// <see cref="StringComparer.OrdinalIgnoreCase"/> as defense in depth.
|
||||
/// </remarks>
|
||||
public sealed class MonitorBlacklistService
|
||||
{
|
||||
private readonly HashSet<string> _blockedEdidIds;
|
||||
|
||||
public MonitorBlacklistService()
|
||||
{
|
||||
_blockedEdidIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in BuiltInMonitorBlacklist.Entries)
|
||||
{
|
||||
AddNormalized(entry.EdidId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="monitorId"/> (a <c>Monitor.Id</c> or raw Windows
|
||||
/// DevicePath) has an EdidId in the built-in blacklist. Monitors whose EdidId cannot
|
||||
/// be extracted (empty path, malformed) are never blocked — we only filter what we
|
||||
/// can positively identify.
|
||||
/// </summary>
|
||||
public bool IsBlocked(string monitorId)
|
||||
{
|
||||
var edid = MonitorIdentity.EdidIdFromMonitorId(monitorId);
|
||||
return !string.IsNullOrEmpty(edid) && _blockedEdidIds.Contains(edid);
|
||||
}
|
||||
|
||||
private void AddNormalized(string? edidId)
|
||||
{
|
||||
var trimmed = edidId?.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
_blockedEdidIds.Add(trimmed.ToUpperInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PowerDisplay.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the built-in monitor blacklist shipped with PowerToys.
|
||||
/// The data is an embedded JSON resource in this assembly; the file is read once
|
||||
/// on first access and cached for the lifetime of the process.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Loader failures are non-fatal: on any exception (missing resource, malformed
|
||||
/// JSON, etc.) the loader returns an empty list. This keeps PowerDisplay running
|
||||
/// even if a malformed release ships, and avoids logging dependencies inside the
|
||||
/// AOT-compatible PowerDisplay.Models assembly.
|
||||
/// </remarks>
|
||||
public static class BuiltInMonitorBlacklist
|
||||
{
|
||||
private const string ResourceName = "PowerDisplay.Models.BuiltInMonitorBlacklist.json";
|
||||
|
||||
private static readonly Lazy<IReadOnlyList<MonitorBlacklistEntry>> _entries
|
||||
= new(LoadFromResource);
|
||||
|
||||
public static IReadOnlyList<MonitorBlacklistEntry> Entries => _entries.Value;
|
||||
|
||||
private static IReadOnlyList<MonitorBlacklistEntry> LoadFromResource()
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = typeof(BuiltInMonitorBlacklist).Assembly;
|
||||
using var stream = assembly.GetManifestResourceStream(ResourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
return Array.Empty<MonitorBlacklistEntry>();
|
||||
}
|
||||
|
||||
var file = JsonSerializer.Deserialize(
|
||||
stream,
|
||||
MonitorBlacklistSerializationContext.Default.BuiltInMonitorBlacklistFile);
|
||||
|
||||
if (file?.Entries == null)
|
||||
{
|
||||
return Array.Empty<MonitorBlacklistEntry>();
|
||||
}
|
||||
|
||||
// Only the v1 schema is understood by this build. Future versions
|
||||
// ship a refreshed binary that updates this check.
|
||||
if (file.Version != 1)
|
||||
{
|
||||
return Array.Empty<MonitorBlacklistEntry>();
|
||||
}
|
||||
|
||||
return file.Entries
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.EdidId))
|
||||
.Select(e => new MonitorBlacklistEntry
|
||||
{
|
||||
EdidId = e.EdidId.Trim().ToUpperInvariant(),
|
||||
Comments = e.Comments ?? string.Empty,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<MonitorBlacklistEntry>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"edidId": "LTM2C02",
|
||||
"comments": "See https://github.com/microsoft/PowerToys/issues/47556"
|
||||
},
|
||||
{
|
||||
"edidId": "GSM7714",
|
||||
"comments": "See https://github.com/microsoft/PowerToys/issues/47968"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON file shape for <see cref="BuiltInMonitorBlacklist"/>.
|
||||
/// The <see cref="Version"/> field is a forward-compatibility marker; this
|
||||
/// release only understands version 1.
|
||||
/// </summary>
|
||||
public class BuiltInMonitorBlacklistFile
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public int Version { get; set; }
|
||||
|
||||
[JsonPropertyName("entries")]
|
||||
public List<MonitorBlacklistEntry> Entries { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// One entry in a PowerDisplay monitor blacklist. Used both for the built-in
|
||||
/// list shipped with PowerToys (loaded via <see cref="BuiltInMonitorBlacklist"/>)
|
||||
/// and for the user-editable custom list persisted on <c>PowerDisplayProperties</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><see cref="EdidId"/> is the 7–8 character PnP hardware identifier extracted
|
||||
/// from a <c>Monitor.Id</c> by <c>MonitorIdentity.EdidIdFromMonitorId</c> (e.g.
|
||||
/// <c>"DELD1A8"</c>, <c>"BOE0900"</c>). It is normalized to uppercase and trimmed
|
||||
/// on write; matching is case-insensitive as a defense-in-depth measure.</para>
|
||||
/// <para><see cref="Comments"/> is free text rendered as-is. The built-in JSON ships
|
||||
/// English-only comments; user input is not localized.</para>
|
||||
/// </remarks>
|
||||
public class MonitorBlacklistEntry
|
||||
{
|
||||
[JsonPropertyName("edidId")]
|
||||
public string EdidId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("comments")]
|
||||
public string Comments { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON serialization context for monitor blacklist types.
|
||||
/// Provides source-generated serialization for Native AOT compatibility.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
IncludeFields = true)]
|
||||
[JsonSerializable(typeof(MonitorBlacklistEntry))]
|
||||
[JsonSerializable(typeof(List<MonitorBlacklistEntry>))]
|
||||
[JsonSerializable(typeof(BuiltInMonitorBlacklistFile))]
|
||||
public partial class MonitorBlacklistSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,4 @@
|
||||
<PropertyGroup>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="BuiltInMonitorBlacklist.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -31,9 +31,6 @@ namespace PowerDisplay.Helpers
|
||||
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||
private readonly DisplayRotationService _rotationService = new();
|
||||
|
||||
// Built-in entries are loaded automatically by the service constructor.
|
||||
private readonly MonitorBlacklistService _blacklistService = new();
|
||||
|
||||
// Controllers stored by type for O(1) lookup based on CommunicationMethod
|
||||
private DdcCiController? _ddcController;
|
||||
private WmiController? _wmiController;
|
||||
@@ -131,34 +128,6 @@ namespace PowerDisplay.Helpers
|
||||
{
|
||||
var inventory = DisplayConfigInventory.GetAllMonitorDisplayInfo();
|
||||
|
||||
// Filter blacklisted monitors out of the inventory before any controller
|
||||
// is dispatched. Matching uses MonitorIdentity.EdidIdFromMonitorId on each
|
||||
// entry's DevicePath, so blocked monitors are not opened, probed, or queried
|
||||
// — the whole point of the blacklist over the per-monitor IsHidden flag.
|
||||
var beforeCount = inventory.Count;
|
||||
var filteredInventory = new Dictionary<string, MonitorDisplayInfo>(
|
||||
inventory.Count, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in inventory)
|
||||
{
|
||||
if (_blacklistService.IsBlocked(kvp.Value.DevicePath))
|
||||
{
|
||||
var edidId = MonitorIdentity.EdidIdFromMonitorId(kvp.Value.DevicePath);
|
||||
Logger.LogInfo(
|
||||
$"[MonitorBlacklist] Skipping '{kvp.Value.FriendlyName}' (EdidId '{edidId}', path '{kvp.Value.DevicePath}') — EdidId is on the blacklist");
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredInventory.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
if (filteredInventory.Count < beforeCount)
|
||||
{
|
||||
Logger.LogInfo(
|
||||
$"[MonitorBlacklist] Filtered out {beforeCount - filteredInventory.Count} monitor(s); {filteredInventory.Count} remain");
|
||||
}
|
||||
|
||||
inventory = filteredInventory;
|
||||
|
||||
if (inventory.Count == 0)
|
||||
{
|
||||
Logger.LogWarning("[MonitorManager] QueryDisplayConfig returned no displays — discovery aborted");
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.Foundation" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
|
||||
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
|
||||
<PackageReference Include="boost" GeneratePathProperty="true" />
|
||||
@@ -225,12 +224,4 @@
|
||||
<PRIResource Include="@(_WildCardPRIResource)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<!-- Deduplicate WindowsAppRuntimeAutoInitializer.cpp (added twice via transitive imports causing MSB8027/LNK4042). Same fix as runner.vcxproj. -->
|
||||
<Target Name="FixWinAppSDKAutoInitializer" BeforeTargets="ClCompile" AfterTargets="WindowsAppRuntimeAutoInitializer">
|
||||
<ItemGroup>
|
||||
<ClCompile Remove="@(ClCompile)" Condition="'%(Filename)' == 'WindowsAppRuntimeAutoInitializer'" />
|
||||
<ClCompile Include="$(PkgMicrosoft_WindowsAppSDK_Foundation)\include\WindowsAppRuntimeAutoInitializer.cpp">
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Target></Project>
|
||||
</Project>
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace cmdArg
|
||||
// restarting it from there, so it doesn't interfere with the installation process.
|
||||
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE1 = L"-update_now";
|
||||
// Stage 2 consists of starting the installer and optionally launching newly installed PowerToys binary.
|
||||
// That's indicated by the following 2 flags.
|
||||
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE2 = L"-update_now_stage_2";
|
||||
const inline wchar_t* UPDATE_STAGE2_RESTART_PT = L"restart";
|
||||
const inline wchar_t* UPDATE_STAGE2_DONT_START_PT = L"dont_start";
|
||||
|
||||
const inline wchar_t* UPDATE_REPORT_SUCCESS = L"-report_update_success";
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
QuickAccessShortcut = new HotkeySettings();
|
||||
IsElevated = false;
|
||||
ShowNewUpdatesToastNotification = true;
|
||||
AutoDownloadUpdates = true;
|
||||
AutoDownloadUpdates = false;
|
||||
EnableExperimentation = true;
|
||||
DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
Theme = "system";
|
||||
|
||||
@@ -56,7 +56,6 @@ public class SetSettingCommandTests
|
||||
}
|
||||
|
||||
[DataRow(typeof(GeneralSettings), "Enabled.MouseWithoutBorders", "true")]
|
||||
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "false")]
|
||||
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "true")]
|
||||
[TestMethod]
|
||||
public void SetGeneralSetting(Type moduleSettingsType, string settingName, string newValueStr)
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
<ProjectReference Include="..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
|
||||
<ProjectReference Include="..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\modules\poweraccent\PowerAccent.Common\PowerAccent.Common.csproj" />
|
||||
<ProjectReference Include="..\..\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj" />
|
||||
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
|
||||
@@ -1174,6 +1174,12 @@ opera.exe</value>
|
||||
<data name="ShortcutGuide_WindowPosition_Right.Content" xml:space="preserve">
|
||||
<value>Right</value>
|
||||
</data>
|
||||
<data name="ShortcutGuide_WindowPosition_Left.Content" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
</data>
|
||||
<data name="ShortcutGuide_WindowPosition_Right.Content" xml:space="preserve">
|
||||
<value>Right</value>
|
||||
</data>
|
||||
<data name="ShortcutGuide_DisabledApps.Header" xml:space="preserve">
|
||||
<value>Exclude apps</value>
|
||||
</data>
|
||||
@@ -2056,6 +2062,9 @@ The Microsoft Windows Operating System
|
||||
Windows Explorer
|
||||
Notepad
|
||||
Microsoft PowerToys
|
||||
Paint
|
||||
Microsoft Office Apps
|
||||
And more...!
|
||||
|
||||
The shortcuts always correspond to the most current application/Windows version.</value>
|
||||
</data>
|
||||
@@ -5136,7 +5145,7 @@ The break timer font matches the text font.</value>
|
||||
<data name="UpdateAvailableInfoBar.Title" xml:space="preserve">
|
||||
<value>Update available</value>
|
||||
</data>
|
||||
<data name="GeneralVersion.Text" xml:space="preserve">
|
||||
<data name="GeneralVersion.Text" xml:space="preserve">
|
||||
<value>Version</value>
|
||||
</data>
|
||||
<data name="LearnWhatsNew.Text" xml:space="preserve">
|
||||
|
||||
@@ -12,7 +12,6 @@ using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using PowerAccent.Common;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
@@ -24,31 +23,56 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
|
||||
/// <summary>
|
||||
/// Maps each currently supported <see cref="LanguageGroup"/> to its resx
|
||||
/// resource key so that group header strings can be looked up by the Settings UI.
|
||||
/// Only groups that already have corresponding Settings UI resources should be
|
||||
/// listed here.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<LanguageGroup, string> _groupResourceKeys = new()
|
||||
{
|
||||
[LanguageGroup.Language] = "QuickAccent_Group_Language",
|
||||
[LanguageGroup.Special] = "QuickAccent_Group_Special",
|
||||
};
|
||||
private const string SpecialGroup = "QuickAccent_Group_Special";
|
||||
private const string LanguageGroup = "QuickAccent_Group_Language";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flat list of all available languages, derived from
|
||||
/// <see cref="CharacterMappings.All"/>. In the Settings UI, this list is sorted
|
||||
/// alphabetically by the localized display name and arranged into groups based on
|
||||
/// the <see cref="LanguageGroup"/>. Populated by <see cref="InitializeLanguages"/>.
|
||||
/// </summary>
|
||||
public List<PowerAccentLanguageModel> Languages { get; private set; }
|
||||
public List<PowerAccentLanguageModel> Languages { get; } = [
|
||||
new PowerAccentLanguageModel("SPECIAL", "QuickAccent_SelectedLanguage_Special", SpecialGroup),
|
||||
new PowerAccentLanguageModel("BG", "QuickAccent_SelectedLanguage_Bulgarian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("CA", "QuickAccent_SelectedLanguage_Catalan", LanguageGroup),
|
||||
new PowerAccentLanguageModel("CRH", "QuickAccent_SelectedLanguage_Crimean", LanguageGroup),
|
||||
new PowerAccentLanguageModel("CUR", "QuickAccent_SelectedLanguage_Currency", SpecialGroup),
|
||||
new PowerAccentLanguageModel("HR", "QuickAccent_SelectedLanguage_Croatian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("CZ", "QuickAccent_SelectedLanguage_Czech", LanguageGroup),
|
||||
new PowerAccentLanguageModel("DK", "QuickAccent_SelectedLanguage_Danish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("GA", "QuickAccent_SelectedLanguage_Gaeilge", LanguageGroup),
|
||||
new PowerAccentLanguageModel("GD", "QuickAccent_SelectedLanguage_Gaidhlig", LanguageGroup),
|
||||
new PowerAccentLanguageModel("NL", "QuickAccent_SelectedLanguage_Dutch", LanguageGroup),
|
||||
new PowerAccentLanguageModel("EL", "QuickAccent_SelectedLanguage_Greek", LanguageGroup),
|
||||
new PowerAccentLanguageModel("GRC", "QuickAccent_SelectedLanguage_Greek_Polytonic", LanguageGroup),
|
||||
new PowerAccentLanguageModel("EST", "QuickAccent_SelectedLanguage_Estonian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("EPO", "QuickAccent_SelectedLanguage_Esperanto", LanguageGroup),
|
||||
new PowerAccentLanguageModel("FI", "QuickAccent_SelectedLanguage_Finnish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("FR", "QuickAccent_SelectedLanguage_French", LanguageGroup),
|
||||
new PowerAccentLanguageModel("DE", "QuickAccent_SelectedLanguage_German", LanguageGroup),
|
||||
new PowerAccentLanguageModel("HE", "QuickAccent_SelectedLanguage_Hebrew", LanguageGroup),
|
||||
new PowerAccentLanguageModel("HU", "QuickAccent_SelectedLanguage_Hungarian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("IS", "QuickAccent_SelectedLanguage_Icelandic", LanguageGroup),
|
||||
new PowerAccentLanguageModel("IPA", "QuickAccent_SelectedLanguage_IPA", SpecialGroup),
|
||||
new PowerAccentLanguageModel("IT", "QuickAccent_SelectedLanguage_Italian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("KU", "QuickAccent_SelectedLanguage_Kurdish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("LT", "QuickAccent_SelectedLanguage_Lithuanian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("MK", "QuickAccent_SelectedLanguage_Macedonian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("MT", "QuickAccent_SelectedLanguage_Maltese", LanguageGroup),
|
||||
new PowerAccentLanguageModel("MI", "QuickAccent_SelectedLanguage_Maori", LanguageGroup),
|
||||
new PowerAccentLanguageModel("NO", "QuickAccent_SelectedLanguage_Norwegian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("PI", "QuickAccent_SelectedLanguage_Pinyin", LanguageGroup),
|
||||
new PowerAccentLanguageModel("PIE", "QuickAccent_SelectedLanguage_Proto_Indo_European", LanguageGroup),
|
||||
new PowerAccentLanguageModel("PL", "QuickAccent_SelectedLanguage_Polish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("PT", "QuickAccent_SelectedLanguage_Portuguese", LanguageGroup),
|
||||
new PowerAccentLanguageModel("RO", "QuickAccent_SelectedLanguage_Romanian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("ROM", "QuickAccent_SelectedLanguage_Romanization", SpecialGroup),
|
||||
new PowerAccentLanguageModel("SK", "QuickAccent_SelectedLanguage_Slovak", LanguageGroup),
|
||||
new PowerAccentLanguageModel("SL", "QuickAccent_SelectedLanguage_Slovenian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("SP", "QuickAccent_SelectedLanguage_Spanish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("SR", "QuickAccent_SelectedLanguage_Serbian", LanguageGroup),
|
||||
new PowerAccentLanguageModel("SR_CYRL", "QuickAccent_SelectedLanguage_Serbian_Cyrillic", LanguageGroup),
|
||||
new PowerAccentLanguageModel("SV", "QuickAccent_SelectedLanguage_Swedish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("TK", "QuickAccent_SelectedLanguage_Turkish", LanguageGroup),
|
||||
new PowerAccentLanguageModel("VI", "QuickAccent_SelectedLanguage_Vietnamese", LanguageGroup),
|
||||
new PowerAccentLanguageModel("CY", "QuickAccent_SelectedLanguage_Welsh", LanguageGroup),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the languages arranged into display groups, in the order defined by
|
||||
/// <see cref="CharacterMappings.GroupDisplayOrder"/>. Bound to the Settings UI
|
||||
/// list.
|
||||
/// </summary>
|
||||
public PowerAccentLanguageGroupModel[] LanguageGroups { get; private set; }
|
||||
|
||||
private readonly string[] _toolbarOptions =
|
||||
@@ -133,50 +157,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Settings UI language models. This resolves localized display names
|
||||
/// for each language, sorts by name within each group, and arranges groups in the
|
||||
/// order defined by <see cref="CharacterMappings.GroupDisplayOrder"/>. The
|
||||
/// resulting list of languages and groups is stored in the
|
||||
/// <see cref="Languages"/> and <see cref="LanguageGroups"/> properties, which are
|
||||
/// bound to the Settings UI.
|
||||
/// Adds Localized Language Name, sorts by it and splits languages into two groups.
|
||||
/// </summary>
|
||||
private void InitializeLanguages()
|
||||
{
|
||||
// Build the flat list and resolve localized display names.
|
||||
Languages = CharacterMappings.All
|
||||
.Where(lang => _groupResourceKeys.ContainsKey(lang.Group))
|
||||
.Select(lang =>
|
||||
foreach (var item in Languages)
|
||||
{
|
||||
string languageResourceId = $"QuickAccent_SelectedLanguage_{lang.Identifier}";
|
||||
item.Language = ResourceLoaderInstance.ResourceLoader.GetString(item.LanguageResourceID);
|
||||
}
|
||||
|
||||
var model = new PowerAccentLanguageModel(
|
||||
lang.Id.ToString(),
|
||||
languageResourceId,
|
||||
_groupResourceKeys[lang.Group]);
|
||||
|
||||
model.Language = ResourceLoaderInstance.ResourceLoader.GetString(languageResourceId);
|
||||
return model;
|
||||
}).ToList();
|
||||
|
||||
// Sort the flat list alphabetically by the localized display name.
|
||||
Languages.Sort((x, y) => string.Compare(x.Language, y.Language, StringComparison.Ordinal));
|
||||
|
||||
// Group them in the explicit order defined by the core library. Note:
|
||||
// PowerAccentLanguageModel does not hold a direct dependency on the
|
||||
// LanguageGroup enum. Instead, we use the stable GroupResourceID as a
|
||||
// decoupled key to map the core groups to the Settings UI models.
|
||||
LanguageGroups = CharacterMappings.GroupDisplayOrder
|
||||
.Where(group => _groupResourceKeys.ContainsKey(group))
|
||||
.Select(group =>
|
||||
{
|
||||
string groupResourceId = _groupResourceKeys[group];
|
||||
var groupedLanguages = Languages.Where(lang => lang.GroupResourceID == groupResourceId).ToList();
|
||||
|
||||
return groupedLanguages.Count > 0
|
||||
? new PowerAccentLanguageGroupModel(groupedLanguages, ResourceLoaderInstance.ResourceLoader.GetString(groupResourceId))
|
||||
: null; // Skip groups with no languages.
|
||||
})
|
||||
.OfType<PowerAccentLanguageGroupModel>()
|
||||
LanguageGroups = Languages
|
||||
.GroupBy(language => language.GroupResourceID)
|
||||
.Select(grp => new PowerAccentLanguageGroupModel(grp.ToList(), ResourceLoaderInstance.ResourceLoader.GetString(grp.Key)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user