New module: AltWindowCycle (#48281)

## Summary of the Pull Request

Introduces a new utility: AltWindowCycle to quickly switch between
windows from the same process using Alt + `.

In release notes give @wzhudev coauthor credits as he also had an
earlier PR

It works like Alt + Tab, but scoped to the app you’re already in.
Perfect for juggling multiple browser windows, terminals, or editor
instances.


https://github.com/user-attachments/assets/cd42f6af-fa5d-4f08-8f68-3c4e75c16d94

<img width="1835" height="971" alt="image"
src="https://github.com/user-attachments/assets/adea59cb-6c8d-4b44-87e2-0a792c4c0b4f"
/>

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

This PR adds AltWindowCycle (in-proc module + Settings integration),
then addresses follow-up check-spelling feedback without changing
runtime behavior:
- allow-list update for `ROOTOWNER`
- comment text adjustment for forbidden-pattern compliance
- local identifier rename (`wpx` → `whitePx`) for spelling compliance

## Validation Steps Performed

- Verified `ROOTOWNER` is present in
`.github/actions/spell-check/allow/code.txt`
- Verified `wpx` is removed and updated occurrences in
`src/modules/AltWindowCycle/AltWindowCycle.cpp`
- Ran targeted diff/verification for both updated files
- Ran final validation (code review + CodeQL trivial-change path)
- Ran secret scan for changed files

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Clint Rutkas <crutkas@users.noreply.github.com>
Copilot-Session: dd5080ea-5001-4efb-87f8-1e7218e10a4e
This commit is contained in:
Clint Rutkas
2026-07-30 22:42:05 -07:00
committed by GitHub
parent 331f88a1a0
commit bb99c30edc
60 changed files with 3974 additions and 1 deletions

View File

@@ -330,6 +330,7 @@ xes
PACKAGEVERSIONNUMBER
APPXMANIFESTVERSION
PROGMAN
ROOTOWNER
# MRU lists
CACHEWRITE

View File

@@ -10,6 +10,7 @@ ACCESSDENIED
ACCESSTOKEN
acfs
ACIE
ACRYLICBLURBEHIND
acrt
ACTIVATEAPP
ACTIVATEOPTIONS
@@ -93,6 +94,7 @@ ASSOCSTR
ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS
atl
AWC
ATRIOX
aumid
AUO
@@ -465,6 +467,7 @@ dwm
dwmapi
DWMCOLORIZATIONCOLORCHANGED
DWMCOMPOSITIONCHANGED
DWMSBT
DWMNCRENDERINGCHANGED
Dwmp
DWMSENDICONICLIVEPREVIEWBITMAP
@@ -1391,6 +1394,7 @@ pnid
PNMLINK
Poc
Podcasts
PARGB
POINTERID
POINTERUPDATE
Pokedex
@@ -1857,6 +1861,7 @@ SYSLIB
sysmenu
systemai
SYSTEMAPPS
SYSTEMBACKDROP
SYSTEMMODAL
systemroot
SYSTEMTIME
@@ -1931,6 +1936,7 @@ tracerpt
trackbar
trafficmanager
transicc
TRANSIENTWINDOW
transitioning
TRAYMOUSEMESSAGE
triaging
@@ -2075,7 +2081,7 @@ WANTNUKEWARNING
WANTPALM
WASDK
wbem
Wca
wca
WCE
wcex
WCRAPI
@@ -2103,6 +2109,7 @@ winappcli
winappsdk
windir
WINDOWCREATED
WINDOWCOMPOSITIONATTRIBDATA
WINDOWDESTROYED
windowedge
WINDOWINFO

View File

@@ -33,6 +33,7 @@
"PowerToys.AlwaysOnTop.exe",
"PowerToys.AlwaysOnTopModuleInterface.dll",
"PowerToys.AltWindowCycle.dll",
"PowerToys.CmdNotFoundModuleInterface.dll",

View File

@@ -158,6 +158,10 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/AltWindowCycle/">
<Project Path="src/modules/AltWindowCycle/AltWindowCycle.vcxproj" Id="4d1d41c7-e22b-4a8b-8805-660a100e1d79" />
<Project Path="src/modules/AltWindowCycle/UnitTests/AltWindowCycleUnitTests.vcxproj" Id="9ebe5c3a-4a37-477e-9176-07c7cf0eba6e" />
</Folder>
<Folder Name="/modules/AlwaysOnTop/">
<Project Path="src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj" Id="1dc3be92-ce89-43fb-8110-9c043a2fe7a2" />
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -8,6 +8,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAlwaysOnTopEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredAltWindowCycleEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAltWindowCycleEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredAwakeEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAwakeEnabledValue());

View File

@@ -8,6 +8,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
GPOWrapper() = default;
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
static GpoRuleConfigured GetConfiguredAltWindowCycleEnabledValue();
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();

View File

@@ -12,6 +12,7 @@ namespace PowerToys
};
[default_interface] static runtimeclass GPOWrapper {
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
static GpoRuleConfigured GetConfiguredAltWindowCycleEnabledValue();
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();

View File

@@ -7,6 +7,7 @@ namespace ManagedCommon
public enum ModuleType
{
AdvancedPaste,
AltWindowCycle,
AlwaysOnTop,
Awake,
ColorPicker,

View File

@@ -72,6 +72,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS = L"ConfigureEnabledUtilityFileExplorerQOIThumbnails";
const std::wstring POLICY_CONFIGURE_ENABLED_NEWPLUS = L"ConfigureEnabledUtilityNewPlus";
const std::wstring POLICY_CONFIGURE_ENABLED_WORKSPACES = L"ConfigureEnabledUtilityWorkspaces";
const std::wstring POLICY_CONFIGURE_ENABLED_ALT_WINDOW_CYCLE = L"ConfigureEnabledUtilityAltWindowCycle";
// The registry value names for PowerToys installer and update policies.
const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled";
@@ -288,6 +289,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ALWAYS_ON_TOP);
}
inline gpo_rule_configured_t getConfiguredAltWindowCycleEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ALT_WINDOW_CYCLE);
}
inline gpo_rule_configured_t getConfiguredAwakeEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_AWAKE);

View File

@@ -91,6 +91,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityAltWindowCycle" class="Both" displayName="$(string.ConfigureEnabledUtilityAltWindowCycle)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityAltWindowCycle">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_99_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityAwake" class="Both" displayName="$(string.ConfigureEnabledUtilityAwake)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityAwake">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />

View File

@@ -244,6 +244,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureAllUtilityGlobalEnabledState">Configure global utility enabled state</string>
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
<string id="ConfigureEnabledUtilityAlwaysOnTop">Always On Top: Configure enabled state</string>
<string id="ConfigureEnabledUtilityAltWindowCycle">Window Hopper: Configure enabled state</string>
<string id="ConfigureEnabledUtilityAwake">Awake: Configure enabled state</string>
<string id="ConfigureEnabledUtilityColorPicker">Color Picker: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCmdNotFound">Command Not Found: Configure enabled state</string>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
#pragma once
// Starts the dedicated UI thread that owns all overlay windows and the Switcher
// state machine. Must be called from the PowerToys module enable() path.
// Safe to call multiple times (idempotent).
bool InitializeAltWindowCycle(HINSTANCE hinst);
// Stops the UI thread and destroys all overlay resources. Safe to call when not
// initialized (idempotent).
//
// The runner drives enable()/disable() on the same thread that services the
// centralized low-level keyboard hook, so disable() must not stall it: pass
// `blockUntilExit = false` there and let the UI thread finish on its own. Pass
// `blockUntilExit = true` only from destroy(), where the runner unloads the DLL
// afterwards and the thread must be gone first.
void ShutdownAltWindowCycle(bool blockUntilExit);
// Called from on_hotkey() on the runner thread. Posts to the UI thread without
// enumerating windows. `holdModifiers` is an AltWindowCycleLogic modifier mask
// that controls which modifier release commits the visible cycle.
bool HandleAltWindowCycleHotkey(bool forward, unsigned int holdModifiers);

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{4d1d41c7-e22b-4a8b-8805-660a100e1d79}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>AltWindowCycle</RootNamespace>
<ProjectName>AltWindowCycle</ProjectName>
</PropertyGroup>
<Import Project="$(RepoRoot)deps\spdlog.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\</OutDir>
<TargetName>PowerToys.AltWindowCycle</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>dwmapi.lib;gdi32.lib;gdiplus.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>dwmapi.lib;gdi32.lib;gdiplus.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(RepoRoot)src\;$(RepoRoot)src\modules;$(RepoRoot)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="AltWindowCycle.h" />
<ClInclude Include="AltWindowCycleLogic.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="AltWindowCycle.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="AltWindowCycle.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="$(RepoRoot)src\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="AltWindowCycle.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AltWindowCycle.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AltWindowCycleLogic.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="AltWindowCycle.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,295 @@
#pragma once
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <algorithm>
#include <string>
#include <vector>
#include <wchar.h>
namespace AltWindowCycleLogic
{
constexpr int DefaultMaxColumns = 6;
constexpr unsigned int ModifierAlt = 1u << 0;
constexpr unsigned int ModifierCtrl = 1u << 1;
constexpr unsigned int ModifierShift = 1u << 2;
constexpr unsigned int ModifierWin = 1u << 3;
constexpr unsigned int AllModifiers = ModifierAlt | ModifierCtrl | ModifierShift | ModifierWin;
struct OverlayLayout
{
double scale = 1.0;
int pad = 0;
int gap = 0;
int tileW = 0;
int tileH = 0;
int headerH = 0;
int previewH = 0;
int inner = 0;
int radius = 0;
int cardTrimBottom = 0;
int iconSize = 0;
int cols = 1;
int rows = 0;
int pageSize = 0;
int panelX = 0;
int panelY = 0;
int panelW = 0;
int panelH = 0;
};
enum class FirstHotkeyAction
{
Ignore,
ShowOverlay,
};
struct FirstHotkeyResult
{
int selected = 0;
FirstHotkeyAction action = FirstHotkeyAction::Ignore;
};
constexpr int ScaledValue(double scale, int value)
{
return static_cast<int>(value * scale + 0.5);
}
constexpr unsigned int StableHoldModifiers(unsigned int configuredModifiers)
{
const unsigned int sanitized = configuredModifiers & AllModifiers;
const unsigned int nonShiftModifiers = sanitized & ~ModifierShift;
return nonShiftModifiers != 0 ? nonShiftModifiers : sanitized;
}
constexpr bool AreRequiredModifiersDown(unsigned int requiredModifiers, unsigned int downModifiers)
{
return requiredModifiers != 0 && (downModifiers & requiredModifiers) == requiredModifiers;
}
inline int WrapIndex(int index, int count)
{
if (count <= 0)
{
return 0;
}
return ((index % count) + count) % count;
}
inline FirstHotkeyResult BeginCycle(int currentIndex, int windowCount, bool forward)
{
if (windowCount < 2)
{
return {};
}
const int normalizedCurrentIndex = currentIndex < 0 ? 0 : currentIndex;
return {
WrapIndex(forward ? normalizedCurrentIndex + 1 : normalizedCurrentIndex - 1, windowCount),
FirstHotkeyAction::ShowOverlay
};
}
inline OverlayLayout ComputeOverlayLayout(const RECT& work, int windowCount, double scale, int maxColumns = DefaultMaxColumns)
{
OverlayLayout layout;
layout.scale = scale;
layout.pad = ScaledValue(scale, 32);
layout.gap = ScaledValue(scale, 26);
layout.tileW = ScaledValue(scale, 270);
layout.headerH = ScaledValue(scale, 48);
layout.previewH = ScaledValue(scale, 142);
layout.inner = ScaledValue(scale, 6);
layout.radius = ScaledValue(scale, 10);
layout.cardTrimBottom = 0;
layout.iconSize = ScaledValue(scale, 16);
layout.tileH = layout.headerH + layout.inner + layout.previewH + layout.inner;
const int workW = work.right - work.left;
const int workH = work.bottom - work.top;
const int safeWindowCount = (std::max)(0, windowCount);
const int safeMaxColumns = (std::max)(1, maxColumns);
const int columnsFromWork = (std::max)(1, (workW - 2 * layout.pad + layout.gap) / (layout.tileW + layout.gap));
layout.cols = (std::min)(safeWindowCount, (std::min)(safeMaxColumns, columnsFromWork));
if (layout.cols < 1)
{
layout.cols = 1;
}
const int totalRows = (safeWindowCount + layout.cols - 1) / layout.cols;
const int rowsFromWork = (std::max)(1, (workH - 2 * layout.pad + layout.gap) / (layout.tileH + layout.gap));
layout.rows = (std::min)(totalRows, rowsFromWork);
layout.pageSize = (std::min)(safeWindowCount, layout.cols * layout.rows);
layout.panelW = 2 * layout.pad + layout.cols * layout.tileW + (layout.cols - 1) * layout.gap;
layout.panelH = 2 * layout.pad + layout.rows * layout.tileH + (std::max)(0, layout.rows - 1) * layout.gap;
layout.panelX = work.left + (workW - layout.panelW) / 2;
layout.panelY = work.top + (workH - layout.panelH) / 2;
if (layout.panelX < work.left)
{
layout.panelX = work.left;
}
if (layout.panelY < work.top)
{
layout.panelY = work.top;
}
return layout;
}
inline int PageStartForSelection(int selectedIndex, int windowCount, int pageSize)
{
if (windowCount <= 0 || pageSize <= 0)
{
return 0;
}
const int normalizedIndex = (std::min)((std::max)(0, selectedIndex), windowCount - 1);
return (normalizedIndex / pageSize) * pageSize;
}
// Number of fixed-size pages the cycle set is split across. 0 when there is
// nothing to page; 1 means every window fits on screen at once.
inline int PageCount(int windowCount, int pageSize)
{
if (windowCount <= 0 || pageSize <= 0)
{
return 0;
}
return (windowCount + pageSize - 1) / pageSize;
}
inline RECT TileRect(const OverlayLayout& layout, int index)
{
const int col = index % layout.cols;
const int row = index / layout.cols;
const int left = layout.pad + col * (layout.tileW + layout.gap);
const int top = layout.pad + row * (layout.tileH + layout.gap);
return { left, top, left + layout.tileW, top + layout.tileH - layout.cardTrimBottom };
}
inline RECT PreviewRect(const OverlayLayout& layout, const RECT& tile)
{
// Sits directly below the header band, inset by the 1px card stroke on the
// left/right/bottom so the card border stays visible around the image.
const int stroke = ScaledValue(layout.scale, 1);
return {
tile.left + stroke,
tile.top + layout.headerH,
tile.right - stroke,
tile.bottom - stroke
};
}
inline RECT HeaderRect(const OverlayLayout& layout, const RECT& tile)
{
const int margin = ScaledValue(layout.scale, 12);
return { tile.left + margin, tile.top, tile.right - margin, tile.top + layout.headerH };
}
inline RECT CoverSource(const RECT& dest, const RECT& avail)
{
const int aw = avail.right - avail.left;
const int ah = avail.bottom - avail.top;
const int dw = dest.right - dest.left;
const int dh = dest.bottom - dest.top;
if (aw <= 0 || ah <= 0 || dw <= 0 || dh <= 0)
{
return avail;
}
const double destA = static_cast<double>(dw) / dh;
const double srcA = static_cast<double>(aw) / ah;
if (srcA > destA)
{
const int cw = (std::max)(1, static_cast<int>(ah * destA + 0.5));
const int x = avail.left + (aw - cw) / 2;
return { x, avail.top, x + cw, avail.bottom };
}
const int ch = (std::max)(1, static_cast<int>(aw / destA + 0.5));
const int y = avail.top + (ah - ch) / 2;
return { avail.left, y, avail.right, y + ch };
}
// =================== Same-process window selection ===================
//
// The live module queries Win32 for each window's state; these helpers own the
// pure decision + ordering logic so the Alt-Tab cycle set can be unit-tested
// without a live desktop.
struct WindowEligibility
{
bool isVisible = false;
bool isCloaked = false;
// True when the window is the representative window for its owner chain, i.e.
// the GetLastActivePopup walk from its root owner lands back on the window
// itself. Owned secondary windows (whose owner is visible) are not.
bool isAltTabRepresentative = false;
bool isToolWindow = false;
bool isAppWindow = false;
};
// Mirrors the classic "IsAltTabWindow" predicate: a window participates in the
// cycle only when it is visible, not cloaked, the representative window of its
// owner chain, and either not a tool window or explicitly opted in as an app window.
constexpr bool IsAltTabEligible(const WindowEligibility& window)
{
return window.isVisible &&
!window.isCloaked &&
window.isAltTabRepresentative &&
(!window.isToolWindow || window.isAppWindow);
}
struct CandidateWindow
{
unsigned long long id = 0;
WindowEligibility eligibility;
// Resolved owning-process image path (UWP windows resolved to the real app,
// not ApplicationFrameHost). Empty when it could not be determined.
std::wstring processKey;
};
// Case-insensitive process-key match, mirroring the live _wcsicmp grouping. An
// empty candidate key never matches.
inline bool ProcessKeyEquals(const std::wstring& candidateKey, const std::wstring& foregroundKey)
{
return !candidateKey.empty() && _wcsicmp(candidateKey.c_str(), foregroundKey.c_str()) == 0;
}
// Given the foreground app's process key and the windows enumerated in Z-order
// (top-most first == MRU), return the ids that make up the Alt-Tab cycle set for
// that app, preserving enumeration order. Ineligible windows (invisible, cloaked,
// owned, or tool-only) and windows from other processes are dropped. Returns empty
// when the foreground key is unknown.
inline std::vector<unsigned long long> SelectCycleWindows(
const std::wstring& foregroundProcessKey,
const std::vector<CandidateWindow>& enumeratedInZOrder)
{
std::vector<unsigned long long> result;
if (foregroundProcessKey.empty())
{
return result;
}
for (const auto& candidate : enumeratedInZOrder)
{
if (!IsAltTabEligible(candidate.eligibility))
{
continue;
}
if (ProcessKeyEquals(candidate.processKey, foregroundProcessKey))
{
result.push_back(candidate.id);
}
}
return result;
}
}

View File

@@ -0,0 +1,424 @@
#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)
#include "..\AltWindowCycleLogic.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace AltWindowCycleUnitTests
{
namespace
{
void AssertRectEqual(const RECT& expected, const RECT& actual)
{
Assert::AreEqual(expected.left, actual.left, L"left");
Assert::AreEqual(expected.top, actual.top, L"top");
Assert::AreEqual(expected.right, actual.right, L"right");
Assert::AreEqual(expected.bottom, actual.bottom, L"bottom");
}
AltWindowCycleLogic::CandidateWindow MakeCandidate(
unsigned long long id,
const std::wstring& processKey,
bool visible = true,
bool cloaked = false,
bool representative = true,
bool toolWindow = false,
bool appWindow = false)
{
AltWindowCycleLogic::CandidateWindow candidate;
candidate.id = id;
candidate.processKey = processKey;
candidate.eligibility.isVisible = visible;
candidate.eligibility.isCloaked = cloaked;
candidate.eligibility.isAltTabRepresentative = representative;
candidate.eligibility.isToolWindow = toolWindow;
candidate.eligibility.isAppWindow = appWindow;
return candidate;
}
void AssertIds(std::initializer_list<unsigned long long> expected, const std::vector<unsigned long long>& actual)
{
Assert::AreEqual(static_cast<int>(expected.size()), static_cast<int>(actual.size()), L"cycle set size");
size_t i = 0;
for (const unsigned long long id : expected)
{
Assert::IsTrue(id == actual[i], L"cycle set id/order mismatch");
++i;
}
}
}
TEST_CLASS(AltWindowCycleLogicTests)
{
public:
TEST_METHOD(ComputeOverlayLayoutUsesExpectedUnscaledGeometry)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 1.0);
Assert::AreEqual(32, layout.pad);
Assert::AreEqual(26, layout.gap);
Assert::AreEqual(270, layout.tileW);
Assert::AreEqual(48, layout.headerH);
Assert::AreEqual(142, layout.previewH);
Assert::AreEqual(6, layout.inner);
Assert::AreEqual(10, layout.radius);
Assert::AreEqual(16, layout.iconSize);
Assert::AreEqual(202, layout.tileH);
Assert::AreEqual(4, layout.cols);
Assert::AreEqual(1, layout.rows);
Assert::AreEqual(4, layout.pageSize);
Assert::AreEqual(1222, layout.panelW);
Assert::AreEqual(266, layout.panelH);
Assert::AreEqual(349, layout.panelX);
Assert::AreEqual(407, layout.panelY);
}
TEST_METHOD(ComputeOverlayLayoutUsesRoundedScaledGeometry)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 6, 1.5);
Assert::AreEqual(48, layout.pad);
Assert::AreEqual(39, layout.gap);
Assert::AreEqual(405, layout.tileW);
Assert::AreEqual(72, layout.headerH);
Assert::AreEqual(213, layout.previewH);
Assert::AreEqual(9, layout.inner);
Assert::AreEqual(15, layout.radius);
Assert::AreEqual(24, layout.iconSize);
Assert::AreEqual(303, layout.tileH);
Assert::AreEqual(4, layout.cols);
Assert::AreEqual(2, layout.rows);
Assert::AreEqual(6, layout.pageSize);
Assert::AreEqual(1833, layout.panelW);
Assert::AreEqual(741, layout.panelH);
Assert::AreEqual(43, layout.panelX);
Assert::AreEqual(169, layout.panelY);
}
TEST_METHOD(ComputeOverlayLayoutHandlesEmptyWindowCount)
{
const RECT work = { 10, 20, 810, 620 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 0, 1.0);
Assert::AreEqual(1, layout.cols);
Assert::AreEqual(0, layout.rows);
Assert::AreEqual(0, layout.pageSize);
}
TEST_METHOD(TilePreviewAndHeaderRectsUsePanelRelativeInsetViewport)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 1.0);
const RECT tile = AltWindowCycleLogic::TileRect(layout, 0);
AssertRectEqual({ 32, 32, 302, 234 }, tile);
const RECT preview = AltWindowCycleLogic::PreviewRect(layout, tile);
AssertRectEqual({ 33, 80, 301, 233 }, preview);
const RECT header = AltWindowCycleLogic::HeaderRect(layout, tile);
AssertRectEqual({ 44, 32, 290, 80 }, header);
}
TEST_METHOD(CoverSourceCropsWideSourceToDestinationAspectRatio)
{
const RECT dest = { 0, 0, 100, 100 };
const RECT avail = { 0, 0, 400, 200 };
const RECT source = AltWindowCycleLogic::CoverSource(dest, avail);
AssertRectEqual({ 100, 0, 300, 200 }, source);
}
TEST_METHOD(CoverSourceCropsTallSourceToDestinationAspectRatio)
{
const RECT dest = { 0, 0, 100, 100 };
const RECT avail = { 0, 0, 200, 400 };
const RECT source = AltWindowCycleLogic::CoverSource(dest, avail);
AssertRectEqual({ 0, 100, 200, 300 }, source);
}
TEST_METHOD(CoverSourceReturnsAvailableRegionForInvalidInputs)
{
const RECT dest = { 0, 0, 0, 100 };
const RECT avail = { 1, 2, 3, 4 };
const RECT source = AltWindowCycleLogic::CoverSource(dest, avail);
AssertRectEqual(avail, source);
}
TEST_METHOD(WrapIndexWrapsForwardAndBackward)
{
Assert::AreEqual(0, AltWindowCycleLogic::WrapIndex(3, 3));
Assert::AreEqual(2, AltWindowCycleLogic::WrapIndex(-1, 3));
Assert::AreEqual(1, AltWindowCycleLogic::WrapIndex(4, 3));
Assert::AreEqual(0, AltWindowCycleLogic::WrapIndex(4, 0));
}
TEST_METHOD(StableHoldModifiersPreferNonShiftModifiers)
{
Assert::AreEqual(AltWindowCycleLogic::ModifierAlt, AltWindowCycleLogic::StableHoldModifiers(AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierShift));
Assert::AreEqual(AltWindowCycleLogic::ModifierCtrl, AltWindowCycleLogic::StableHoldModifiers(AltWindowCycleLogic::ModifierCtrl | AltWindowCycleLogic::ModifierShift));
Assert::AreEqual(AltWindowCycleLogic::ModifierWin, AltWindowCycleLogic::StableHoldModifiers(AltWindowCycleLogic::ModifierWin));
Assert::AreEqual(AltWindowCycleLogic::ModifierShift, AltWindowCycleLogic::StableHoldModifiers(AltWindowCycleLogic::ModifierShift));
Assert::AreEqual(0u, AltWindowCycleLogic::StableHoldModifiers(0));
}
TEST_METHOD(AreRequiredModifiersDownRequiresEveryHeldModifier)
{
Assert::IsFalse(AltWindowCycleLogic::AreRequiredModifiersDown(0, AltWindowCycleLogic::ModifierAlt));
Assert::IsFalse(AltWindowCycleLogic::AreRequiredModifiersDown(AltWindowCycleLogic::ModifierAlt, 0));
Assert::IsFalse(AltWindowCycleLogic::AreRequiredModifiersDown(AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierCtrl, AltWindowCycleLogic::ModifierAlt));
Assert::IsTrue(AltWindowCycleLogic::AreRequiredModifiersDown(AltWindowCycleLogic::ModifierAlt, AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierShift));
Assert::IsTrue(AltWindowCycleLogic::AreRequiredModifiersDown(AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierCtrl, AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierCtrl | AltWindowCycleLogic::ModifierShift));
}
TEST_METHOD(BeginCycleSelectsNextOrPreviousAndShowsOverlay)
{
const auto forward = AltWindowCycleLogic::BeginCycle(0, 3, true);
Assert::AreEqual(1, forward.selected);
Assert::IsTrue(forward.action == AltWindowCycleLogic::FirstHotkeyAction::ShowOverlay);
const auto backward = AltWindowCycleLogic::BeginCycle(0, 3, false);
Assert::AreEqual(2, backward.selected);
Assert::IsTrue(backward.action == AltWindowCycleLogic::FirstHotkeyAction::ShowOverlay);
const auto ignored = AltWindowCycleLogic::BeginCycle(0, 1, true);
Assert::AreEqual(0, ignored.selected);
Assert::IsTrue(ignored.action == AltWindowCycleLogic::FirstHotkeyAction::Ignore);
}
// =================== Same-process window selection ===================
TEST_METHOD(IsAltTabEligibleAcceptsVisibleRepresentativeNonToolWindow)
{
Assert::IsTrue(AltWindowCycleLogic::IsAltTabEligible({ true, false, true, false }));
}
TEST_METHOD(IsAltTabEligibleRejectsEachDisqualifyingCondition)
{
// Invisible.
Assert::IsFalse(AltWindowCycleLogic::IsAltTabEligible({ false, false, true, false }));
// Cloaked (e.g. a background virtual-desktop window).
Assert::IsFalse(AltWindowCycleLogic::IsAltTabEligible({ true, true, true, false }));
// Owned / not the representative window of its owner chain.
Assert::IsFalse(AltWindowCycleLogic::IsAltTabEligible({ true, false, false, false }));
// Tool window.
Assert::IsFalse(AltWindowCycleLogic::IsAltTabEligible({ true, false, true, true }));
}
TEST_METHOD(IsAltTabEligibleAcceptsToolWindowOptedInAsAppWindow)
{
Assert::IsTrue(AltWindowCycleLogic::IsAltTabEligible({ true, false, true, true, true }));
}
TEST_METHOD(ProcessKeyEqualsIsCaseInsensitiveAndRejectsEmptyCandidate)
{
Assert::IsTrue(AltWindowCycleLogic::ProcessKeyEquals(L"C:\\Apps\\Foo.exe", L"c:\\apps\\foo.EXE"));
Assert::IsFalse(AltWindowCycleLogic::ProcessKeyEquals(L"C:\\Apps\\Foo.exe", L"C:\\Apps\\Bar.exe"));
Assert::IsFalse(AltWindowCycleLogic::ProcessKeyEquals(L"", L"C:\\Apps\\Foo.exe"));
}
TEST_METHOD(SelectCycleWindowsKeepsOnlyForegroundProcessInEnumerationOrder)
{
const std::wstring fg = L"C:\\Apps\\Editor.exe";
const std::vector<AltWindowCycleLogic::CandidateWindow> candidates = {
MakeCandidate(10, L"C:\\Apps\\Editor.exe"),
MakeCandidate(20, L"C:\\Apps\\Browser.exe"),
MakeCandidate(30, L"c:\\apps\\editor.EXE"), // same app, different casing
MakeCandidate(40, L"C:\\Apps\\Browser.exe"),
MakeCandidate(50, L"C:\\Apps\\Editor.exe"),
};
AssertIds({ 10, 30, 50 }, AltWindowCycleLogic::SelectCycleWindows(fg, candidates));
}
TEST_METHOD(SelectCycleWindowsExcludesIneligibleWindowsOfTheSameApp)
{
const std::wstring fg = L"C:\\Apps\\Editor.exe";
const std::vector<AltWindowCycleLogic::CandidateWindow> candidates = {
MakeCandidate(1, fg), // eligible
MakeCandidate(2, fg, /*visible*/ false),
MakeCandidate(3, fg, /*visible*/ true, /*cloaked*/ true),
MakeCandidate(4, fg, /*visible*/ true, /*cloaked*/ false, /*representative*/ false),
MakeCandidate(5, fg, /*visible*/ true, /*cloaked*/ false, /*representative*/ true, /*tool*/ true),
MakeCandidate(6, fg), // eligible
};
AssertIds({ 1, 6 }, AltWindowCycleLogic::SelectCycleWindows(fg, candidates));
}
TEST_METHOD(SelectCycleWindowsReturnsEmptyWhenForegroundKeyUnknown)
{
const std::vector<AltWindowCycleLogic::CandidateWindow> candidates = {
MakeCandidate(1, L"C:\\Apps\\Editor.exe"),
};
Assert::AreEqual(0u, static_cast<unsigned int>(AltWindowCycleLogic::SelectCycleWindows(L"", candidates).size()));
}
TEST_METHOD(SelectCycleWindowsHandlesZeroAndSingleCandidate)
{
const std::wstring fg = L"C:\\Apps\\Editor.exe";
Assert::AreEqual(0u, static_cast<unsigned int>(AltWindowCycleLogic::SelectCycleWindows(fg, {}).size()));
const std::vector<AltWindowCycleLogic::CandidateWindow> single = { MakeCandidate(99, fg) };
AssertIds({ 99 }, AltWindowCycleLogic::SelectCycleWindows(fg, single));
}
TEST_METHOD(SelectCycleWindowsIncludesEveryWindowOfATiedProcess)
{
// Several windows of the same app (the real grouping key is the resolved
// process image path, so multiple windows / duplicate pids all cycle).
const std::wstring fg = L"C:\\Apps\\Editor.exe";
const std::vector<AltWindowCycleLogic::CandidateWindow> candidates = {
MakeCandidate(7, fg),
MakeCandidate(8, fg),
MakeCandidate(9, fg),
};
AssertIds({ 7, 8, 9 }, AltWindowCycleLogic::SelectCycleWindows(fg, candidates));
}
TEST_METHOD(SelectCycleWindowsDropsCandidatesWithNoResolvedProcess)
{
const std::wstring fg = L"C:\\Apps\\Editor.exe";
const std::vector<AltWindowCycleLogic::CandidateWindow> candidates = {
MakeCandidate(1, fg),
MakeCandidate(2, L""), // eligible flags but process could not be resolved
MakeCandidate(3, fg),
};
AssertIds({ 1, 3 }, AltWindowCycleLogic::SelectCycleWindows(fg, candidates));
}
// =================== Layout / index edge cases ===================
TEST_METHOD(ComputeOverlayLayoutClampsColumnsToWindowCount)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 1, 1.0);
Assert::AreEqual(1, layout.cols);
Assert::AreEqual(1, layout.rows);
}
TEST_METHOD(ComputeOverlayLayoutRespectsMaxColumns)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 10, 1.0, 3);
Assert::AreEqual(3, layout.cols);
Assert::AreEqual(4, layout.rows); // ceil(10 / 3)
}
TEST_METHOD(ComputeOverlayLayoutClampsColumnsAndRowsToNarrowWorkArea)
{
const RECT work = { 0, 0, 640, 480 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 8, 1.0);
// (640 - 64 + 26) / (270 + 26) = 2 columns fit the work area.
Assert::AreEqual(2, layout.cols);
Assert::AreEqual(1, layout.rows);
Assert::AreEqual(2, layout.pageSize);
Assert::IsTrue(layout.panelH <= work.bottom - work.top);
}
TEST_METHOD(ComputeOverlayLayoutScalesPaddingAcrossDpi)
{
const RECT work = { 0, 0, 1920, 1080 };
Assert::AreEqual(32, AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 1.0).pad);
Assert::AreEqual(40, AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 1.25).pad);
Assert::AreEqual(48, AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 1.5).pad);
Assert::AreEqual(64, AltWindowCycleLogic::ComputeOverlayLayout(work, 4, 2.0).pad);
}
TEST_METHOD(ComputeOverlayLayoutPaginatesRowsThatDoNotFitWorkArea)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 13, 1.5);
Assert::AreEqual(4, layout.cols);
Assert::AreEqual(2, layout.rows);
Assert::AreEqual(8, layout.pageSize);
Assert::IsTrue(layout.panelH <= work.bottom - work.top);
}
TEST_METHOD(PageStartForSelectionKeepsSelectedWindowOnPage)
{
Assert::AreEqual(0, AltWindowCycleLogic::PageStartForSelection(0, 13, 8));
Assert::AreEqual(0, AltWindowCycleLogic::PageStartForSelection(7, 13, 8));
Assert::AreEqual(8, AltWindowCycleLogic::PageStartForSelection(8, 13, 8));
Assert::AreEqual(8, AltWindowCycleLogic::PageStartForSelection(12, 13, 8));
Assert::AreEqual(0, AltWindowCycleLogic::PageStartForSelection(-1, 13, 8));
Assert::AreEqual(8, AltWindowCycleLogic::PageStartForSelection(13, 13, 8));
Assert::AreEqual(0, AltWindowCycleLogic::PageStartForSelection(0, 0, 0));
}
TEST_METHOD(PageCountReportsPagesForPaginationAffordance)
{
Assert::AreEqual(1, AltWindowCycleLogic::PageCount(1, 8));
Assert::AreEqual(1, AltWindowCycleLogic::PageCount(8, 8));
Assert::AreEqual(2, AltWindowCycleLogic::PageCount(9, 8));
Assert::AreEqual(2, AltWindowCycleLogic::PageCount(13, 8));
Assert::AreEqual(3, AltWindowCycleLogic::PageCount(17, 8));
Assert::AreEqual(0, AltWindowCycleLogic::PageCount(0, 8));
Assert::AreEqual(0, AltWindowCycleLogic::PageCount(13, 0));
}
TEST_METHOD(TileRectAdvancesAcrossColumnsAndRows)
{
const RECT work = { 0, 0, 1920, 1080 };
const auto layout = AltWindowCycleLogic::ComputeOverlayLayout(work, 8, 1.0);
Assert::AreEqual(6, layout.cols); // sanity: second row exists
// Second column, first row.
AssertRectEqual({ 328, 32, 598, 234 }, AltWindowCycleLogic::TileRect(layout, 1));
// First column, second row.
AssertRectEqual({ 32, 260, 302, 462 }, AltWindowCycleLogic::TileRect(layout, 6));
}
TEST_METHOD(WrapIndexNormalizesNegativeAndOversizedIndices)
{
Assert::AreEqual(2, AltWindowCycleLogic::WrapIndex(-4, 3));
Assert::AreEqual(1, AltWindowCycleLogic::WrapIndex(7, 3));
Assert::AreEqual(0, AltWindowCycleLogic::WrapIndex(0, 5));
Assert::AreEqual(0, AltWindowCycleLogic::WrapIndex(-3, 3));
}
TEST_METHOD(BeginCycleClampsNegativeCurrentIndex)
{
const auto forward = AltWindowCycleLogic::BeginCycle(-1, 3, true);
Assert::AreEqual(1, forward.selected);
Assert::IsTrue(forward.action == AltWindowCycleLogic::FirstHotkeyAction::ShowOverlay);
const auto backward = AltWindowCycleLogic::BeginCycle(-5, 3, false);
Assert::AreEqual(2, backward.selected);
}
TEST_METHOD(StableHoldModifiersStripsUnknownBitsAndPrefersNonShift)
{
const unsigned int expected =
AltWindowCycleLogic::ModifierAlt | AltWindowCycleLogic::ModifierCtrl | AltWindowCycleLogic::ModifierWin;
Assert::AreEqual(expected, AltWindowCycleLogic::StableHoldModifiers(0xFFu));
}
};
}

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<ProjectGuid>{9EBE5C3A-4A37-477E-9176-07C7CF0EBA6E}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>AltWindowCycleUnitTests</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>AltWindowCycle.UnitTests</ProjectName>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\tests\AltWindowCycle\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..;$(RepoRoot)src\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AltWindowCycleLogicTests.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\AltWindowCycleLogic.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{1345DADF-2D71-454C-A6D9-B9698E920E1A}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{8439D5F4-E4A2-4A72-A6D7-579E2A8D1260}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="AltWindowCycleLogicTests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\AltWindowCycleLogic.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,305 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/gpo.h>
#include "AltWindowCycle.h"
#include "AltWindowCycleLogic.h"
#include "trace.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_NEXT_WINDOW_SHORTCUT[] = L"next_window_shortcut";
const wchar_t JSON_KEY_PREVIOUS_WINDOW_SHORTCUT[] = L"previous_window_shortcut";
// ` ~ key (VK_OEM_3) on US layouts.
const unsigned char DEFAULT_BACKTICK_VK = 0xC0;
// The configurable hotkeys stay in Settings UI order. Escape is handled by a
// scoped low-level hook inside the module while the overlay is visible, so it
// deliberately does not appear here.
enum HotkeyId : size_t
{
HotkeyNext = 0,
HotkeyPrevious,
HotkeyCount
};
static unsigned int ModifierMaskFromHotkey(const PowertoyModuleIface::Hotkey& hotkey)
{
unsigned int modifiers = 0;
if (hotkey.alt)
{
modifiers |= AltWindowCycleLogic::ModifierAlt;
}
if (hotkey.ctrl)
{
modifiers |= AltWindowCycleLogic::ModifierCtrl;
}
if (hotkey.shift)
{
modifiers |= AltWindowCycleLogic::ModifierShift;
}
if (hotkey.win)
{
modifiers |= AltWindowCycleLogic::ModifierWin;
}
return AltWindowCycleLogic::StableHoldModifiers(modifiers);
}
// The overlay commits when the held modifier is released, so a shortcut with no
// modifier at all would flash the overlay and immediately dismiss it. Settings UI
// won't produce one, but a hand-edited settings.json can.
static bool HasModifier(const PowertoyModuleIface::Hotkey& hotkey)
{
return hotkey.win || hotkey.ctrl || hotkey.shift || hotkey.alt;
}
}
// Implement the PowerToy Module Interface and all the required methods.
class AltWindowCycle : public PowertoyModuleIface
{
private:
bool m_enabled = false;
// Cycle to the next window of the focused app.
PowertoyModuleIface::Hotkey m_nextHotkey;
// Cycle to the previous window of the focused app.
PowertoyModuleIface::Hotkey m_previousHotkey;
void init_settings();
void parse_settings(PowerToysSettings::PowerToyValues& settings);
static void parse_hotkey(const winrt::Windows::Data::Json::JsonObject& properties,
const wchar_t* key,
PowertoyModuleIface::Hotkey& hotkey)
{
try
{
auto jsonHotkeyObject = properties.GetNamedObject(key);
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
Logger::error("Failed to initialize AltWindowCycle shortcut from settings");
}
}
public:
AltWindowCycle()
{
LoggerHelpers::init_logger(L"AltWindowCycle", L"ModuleInterface", "AltWindowCycle");
init_settings();
}
virtual void destroy() override
{
// The runner unloads this DLL right after destroy() returns, so the UI
// thread must be fully gone before the module code disappears.
ShutdownAltWindowCycle(true); // idempotent
delete this;
}
virtual const wchar_t* get_name() override
{
return L"AltWindowCycle";
}
virtual const wchar_t* get_key() override
{
return L"AltWindowCycle";
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredAltWindowCycleEnabledValue();
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(L"Cycle between the windows of the currently focused application.");
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_settings(values);
values.save_to_settings_file();
}
catch (std::exception&)
{
// Improper JSON.
}
}
virtual void enable() override
{
m_enabled = InitializeAltWindowCycle(reinterpret_cast<HINSTANCE>(&__ImageBase));
if (m_enabled)
{
Trace::EnableAltWindowCycle(true);
}
}
virtual void disable() override
{
m_enabled = false;
Trace::EnableAltWindowCycle(false);
// disable() runs on the runner thread that services the centralized
// low-level keyboard hook, so it must never block on the UI thread.
ShutdownAltWindowCycle(false);
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual bool is_enabled_by_default() const override
{
return false;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (hotkeys && buffer_size >= HotkeyCount)
{
hotkeys[HotkeyNext] = m_nextHotkey;
hotkeys[HotkeyPrevious] = m_previousHotkey;
}
return HotkeyCount;
}
virtual bool on_hotkey(size_t hotkeyId) override
{
if (!m_enabled)
{
return false;
}
if (hotkeyId == HotkeyNext)
{
Trace::CycleWindow(true);
return HandleAltWindowCycleHotkey(true, ModifierMaskFromHotkey(m_nextHotkey));
}
if (hotkeyId == HotkeyPrevious)
{
Trace::CycleWindow(false);
return HandleAltWindowCycleHotkey(false, ModifierMaskFromHotkey(m_previousHotkey));
}
return false;
}
};
void AltWindowCycle::init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(AltWindowCycle::get_key());
parse_settings(settings);
}
catch (std::exception&)
{
// Error while loading from the settings file. Let default values stay as they are.
}
}
void AltWindowCycle::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
// Reset to defaults before parsing so removed/invalid values fall back cleanly.
m_nextHotkey = PowertoyModuleIface::Hotkey{};
m_previousHotkey = PowertoyModuleIface::Hotkey{};
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size() && settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (properties.HasKey(JSON_KEY_NEXT_WINDOW_SHORTCUT))
{
parse_hotkey(properties, JSON_KEY_NEXT_WINDOW_SHORTCUT, m_nextHotkey);
}
if (properties.HasKey(JSON_KEY_PREVIOUS_WINDOW_SHORTCUT))
{
parse_hotkey(properties, JSON_KEY_PREVIOUS_WINDOW_SHORTCUT, m_previousHotkey);
}
}
else
{
Logger::info("AltWindowCycle settings are empty");
}
// Default: Alt+` cycles to the next window of the focused app.
if (!m_nextHotkey.key || !HasModifier(m_nextHotkey))
{
m_nextHotkey.win = false;
m_nextHotkey.ctrl = false;
m_nextHotkey.shift = false;
m_nextHotkey.alt = true;
m_nextHotkey.key = DEFAULT_BACKTICK_VK;
}
// Default: Shift+Alt+` cycles to the previous window of the focused app.
if (!m_previousHotkey.key || !HasModifier(m_previousHotkey))
{
m_previousHotkey.win = false;
m_previousHotkey.ctrl = false;
m_previousHotkey.shift = true;
m_previousHotkey.alt = true;
m_previousHotkey.key = DEFAULT_BACKTICK_VK;
}
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new AltWindowCycle();
}

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <dwmapi.h>
#include <algorithm>
#include <string>
#include <vector>
#include <winrt/Windows.Data.Json.h>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/SettingsAPI/settings_helpers.h>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by AltWindowCycle.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Window Hopper"
#define INTERNAL_NAME "AltWindowCycle"
#define ORIGINAL_FILENAME "PowerToys.AltWindowCycle.dll"
// Non-localizable
//////////////////////////////

View File

@@ -0,0 +1,33 @@
#include "pch.h"
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
// Log if the user has AltWindowCycle enabled or disabled
void Trace::EnableAltWindowCycle(const bool enabled) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"AltWindowCycle_EnableAltWindowCycle",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
// Log that the user invoked the module to cycle a window
void Trace::CycleWindow(const bool forward) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"AltWindowCycle_CycleWindow",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(forward, "Forward"));
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
// Log if the user has AltWindowCycle enabled or disabled
static void EnableAltWindowCycle(const bool enabled) noexcept;
// Log that the user invoked the module to cycle a window
static void CycleWindow(const bool forward) noexcept;
};

View File

@@ -288,6 +288,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"PowerToys.LightSwitchModuleInterface.dll",
L"PowerToys.PowerDisplayModuleInterface.dll",
L"PowerToys.GrabAndMoveModuleInterface.dll",
L"PowerToys.AltWindowCycle.dll",
};
for (auto moduleSubdir : knownModules)

View File

@@ -25,6 +25,7 @@ internal static class ModuleGpoHelper
ModuleType.FancyZones => GPOWrapper.GetConfiguredFancyZonesEnabledValue(),
ModuleType.FileLocksmith => GPOWrapper.GetConfiguredFileLocksmithEnabledValue(),
ModuleType.FindMyMouse => GPOWrapper.GetConfiguredFindMyMouseEnabledValue(),
ModuleType.AltWindowCycle => GPOWrapper.GetConfiguredAltWindowCycleEnabledValue(),
ModuleType.Hosts => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue(),
ModuleType.ImageResizer => GPOWrapper.GetConfiguredImageResizerEnabledValue(),
ModuleType.KeyboardManager => GPOWrapper.GetConfiguredKeyboardManagerEnabledValue(),

View File

@@ -0,0 +1,47 @@
// 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;
using Settings.UI.Library.Attributes;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class AltWindowCycleProperties
{
// Alt+` cycles to the next window of the focused app.
[JsonIgnore]
[CmdConfigureIgnore]
public HotkeySettings DefaultNextWindowShortcut => new HotkeySettings(false, false, true, false, 0xC0);
// Shift+Alt+` cycles to the previous window of the focused app.
[JsonIgnore]
[CmdConfigureIgnore]
public HotkeySettings DefaultPreviousWindowShortcut => new HotkeySettings(false, false, true, true, 0xC0);
private HotkeySettings _nextWindowShortcut;
private HotkeySettings _previousWindowShortcut;
[JsonPropertyName("next_window_shortcut")]
public HotkeySettings NextWindowShortcut
{
get => _nextWindowShortcut ?? DefaultNextWindowShortcut;
set => _nextWindowShortcut = value;
}
[JsonPropertyName("previous_window_shortcut")]
public HotkeySettings PreviousWindowShortcut
{
get => _previousWindowShortcut ?? DefaultPreviousWindowShortcut;
set => _previousWindowShortcut = value;
}
public AltWindowCycleProperties()
{
NextWindowShortcut = DefaultNextWindowShortcut;
PreviousWindowShortcut = DefaultPreviousWindowShortcut;
}
}
}

View File

@@ -0,0 +1,57 @@
// 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;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class AltWindowCycleSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "AltWindowCycle";
[JsonPropertyName("properties")]
public AltWindowCycleProperties Properties { get; set; }
public AltWindowCycleSettings()
{
Name = ModuleName;
Properties = new AltWindowCycleProperties();
Version = "1.0";
}
public string GetModuleName()
{
return Name;
}
public ModuleType GetModuleType() => ModuleType.AltWindowCycle;
public HotkeyAccessor[] GetAllHotkeyAccessors()
{
var hotkeyAccessors = new List<HotkeyAccessor>
{
new HotkeyAccessor(
() => Properties.NextWindowShortcut,
value => Properties.NextWindowShortcut = value ?? Properties.DefaultNextWindowShortcut,
"AltWindowCycle_NextWindowShortcut"),
new HotkeyAccessor(
() => Properties.PreviousWindowShortcut,
value => Properties.PreviousWindowShortcut = value ?? Properties.DefaultPreviousWindowShortcut,
"AltWindowCycle_PreviousWindowShortcut"),
};
return hotkeyAccessors.ToArray();
}
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
return false;
}
}
}

View File

@@ -218,6 +218,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool altWindowCycle; // defaulting to off
[JsonPropertyName("AltWindowCycle")]
public bool AltWindowCycle
{
get => altWindowCycle;
set
{
if (altWindowCycle != value)
{
LogTelemetryEvent(value);
altWindowCycle = value;
}
}
}
private bool mouseHighlighter = true;
[JsonPropertyName("MouseHighlighter")]

View File

@@ -30,6 +30,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
return moduleType switch
{
ModuleType.AdvancedPaste => "ms-appx:///Assets/Settings/Icons/AdvancedPaste.png",
ModuleType.AltWindowCycle => "ms-appx:///Assets/Settings/Icons/WindowHopper.png",
ModuleType.Workspaces => "ms-appx:///Assets/Settings/Icons/Workspaces.png",
ModuleType.PowerOCR => "ms-appx:///Assets/Settings/Icons/TextExtractor.png",
ModuleType.PowerAccent => "ms-appx:///Assets/Settings/Icons/QuickAccent.png",
@@ -56,6 +57,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
ModuleType.FancyZones => generalSettingsConfig.Enabled.FancyZones,
ModuleType.FileLocksmith => generalSettingsConfig.Enabled.FileLocksmith,
ModuleType.FindMyMouse => generalSettingsConfig.Enabled.FindMyMouse,
ModuleType.AltWindowCycle => generalSettingsConfig.Enabled.AltWindowCycle,
ModuleType.Hosts => generalSettingsConfig.Enabled.Hosts,
ModuleType.ImageResizer => generalSettingsConfig.Enabled.ImageResizer,
ModuleType.KeyboardManager => generalSettingsConfig.Enabled.KeyboardManager,
@@ -97,6 +99,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
case ModuleType.FancyZones: generalSettingsConfig.Enabled.FancyZones = isEnabled; break;
case ModuleType.FileLocksmith: generalSettingsConfig.Enabled.FileLocksmith = isEnabled; break;
case ModuleType.FindMyMouse: generalSettingsConfig.Enabled.FindMyMouse = isEnabled; break;
case ModuleType.AltWindowCycle: generalSettingsConfig.Enabled.AltWindowCycle = isEnabled; break;
case ModuleType.Hosts: generalSettingsConfig.Enabled.Hosts = isEnabled; break;
case ModuleType.ImageResizer: generalSettingsConfig.Enabled.ImageResizer = isEnabled; break;
case ModuleType.KeyboardManager: generalSettingsConfig.Enabled.KeyboardManager = isEnabled; break;
@@ -141,6 +144,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
ModuleType.FancyZones => FancyZonesSettings.ModuleName,
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
ModuleType.AltWindowCycle => AltWindowCycleSettings.ModuleName,
ModuleType.Hosts => HostsSettings.ModuleName,
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,

View File

@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonSerializable(typeof(GeneralSettings))]
[JsonSerializable(typeof(OutGoingGeneralSettings))]
[JsonSerializable(typeof(AdvancedPasteSettings))]
[JsonSerializable(typeof(AltWindowCycleSettings))]
[JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(AwakeSettings))]
[JsonSerializable(typeof(CmdNotFoundSettings))]

View File

@@ -0,0 +1,116 @@
// 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;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CommonLibTest
{
[TestClass]
public class AltWindowCycleSettingsTests
{
// VK_OEM_3 (the backtick / grave-accent key) that both default shortcuts bind to.
private const int OemTilde = 0xC0;
[TestMethod]
public void Defaults_ShouldMatchDocumentedHotkeys()
{
var settings = new AltWindowCycleSettings();
Assert.AreEqual(AltWindowCycleSettings.ModuleName, settings.Name);
Assert.AreEqual("1.0", settings.Version);
Assert.IsNotNull(settings.Properties);
// Next window: Alt+`
AssertHotkey(settings.Properties.NextWindowShortcut, win: false, ctrl: false, alt: true, shift: false, code: OemTilde);
// Previous window: Shift+Alt+`
AssertHotkey(settings.Properties.PreviousWindowShortcut, win: false, ctrl: false, alt: true, shift: true, code: OemTilde);
}
[TestMethod]
public void ToJsonString_ShouldContainExpectedKeys()
{
var settings = new AltWindowCycleSettings();
var json = settings.ToJsonString();
StringAssert.Contains(json, AltWindowCycleSettings.ModuleName);
StringAssert.Contains(json, "\"properties\"");
StringAssert.Contains(json, "next_window_shortcut");
StringAssert.Contains(json, "previous_window_shortcut");
}
[TestMethod]
public void RoundTrip_WithDefaults_ShouldPreserveAllValues()
{
var original = new AltWindowCycleSettings();
var deserialized = JsonSerializer.Deserialize<AltWindowCycleSettings>(original.ToJsonString());
Assert.IsNotNull(deserialized);
Assert.AreEqual(original.Name, deserialized.Name);
Assert.AreEqual(original.Version, deserialized.Version);
Assert.IsNotNull(deserialized.Properties);
AssertHotkeyEqual(original.Properties.NextWindowShortcut, deserialized.Properties.NextWindowShortcut);
AssertHotkeyEqual(original.Properties.PreviousWindowShortcut, deserialized.Properties.PreviousWindowShortcut);
}
[TestMethod]
public void RoundTrip_WithCustomHotkeys_ShouldPreserveOverrides()
{
var original = new AltWindowCycleSettings();
original.Properties.NextWindowShortcut = new HotkeySettings(true, false, false, false, 0x4E); // Win+N
original.Properties.PreviousWindowShortcut = new HotkeySettings(true, false, false, true, 0x50); // Win+Shift+P
var deserialized = JsonSerializer.Deserialize<AltWindowCycleSettings>(original.ToJsonString());
Assert.IsNotNull(deserialized);
AssertHotkey(deserialized.Properties.NextWindowShortcut, win: true, ctrl: false, alt: false, shift: false, code: 0x4E);
AssertHotkey(deserialized.Properties.PreviousWindowShortcut, win: true, ctrl: false, alt: false, shift: true, code: 0x50);
}
[TestMethod]
public void Deserialize_WithNullHotkeys_ShouldUseDefaults()
{
const string Json = "{\"properties\":{\"next_window_shortcut\":null,\"previous_window_shortcut\":null}}";
var deserialized = JsonSerializer.Deserialize<AltWindowCycleSettings>(Json);
Assert.IsNotNull(deserialized);
AssertHotkey(deserialized.Properties.NextWindowShortcut, win: false, ctrl: false, alt: true, shift: false, code: OemTilde);
AssertHotkey(deserialized.Properties.PreviousWindowShortcut, win: false, ctrl: false, alt: true, shift: true, code: OemTilde);
}
[TestMethod]
public void ShouldBeRegisteredInSerializationContext()
{
var options = new JsonSerializerOptions
{
TypeInfoResolver = SettingsSerializationContext.Default,
};
var typeInfo = options.TypeInfoResolver?.GetTypeInfo(typeof(AltWindowCycleSettings), options);
Assert.IsNotNull(typeInfo, "AltWindowCycleSettings must be registered in SettingsSerializationContext for Native AOT serialization.");
}
private static void AssertHotkey(HotkeySettings hotkey, bool win, bool ctrl, bool alt, bool shift, int code)
{
Assert.IsNotNull(hotkey);
Assert.AreEqual(win, hotkey.Win, "Win modifier mismatch.");
Assert.AreEqual(ctrl, hotkey.Ctrl, "Ctrl modifier mismatch.");
Assert.AreEqual(alt, hotkey.Alt, "Alt modifier mismatch.");
Assert.AreEqual(shift, hotkey.Shift, "Shift modifier mismatch.");
Assert.AreEqual(code, hotkey.Code, "Key code mismatch.");
}
private static void AssertHotkeyEqual(HotkeySettings expected, HotkeySettings actual)
{
AssertHotkey(actual, expected.Win, expected.Ctrl, expected.Alt, expected.Shift, expected.Code);
}
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO.Abstractions;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.UnitTests.Mocks;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace ViewModelTests
{
[TestClass]
public class AltWindowCycle
{
private Mock<SettingsUtils> mockGeneralSettingsUtils;
private Mock<SettingsUtils> mockAltWindowCycleSettingsUtils;
[TestInitialize]
public void SetUpStubSettingUtils()
{
mockGeneralSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<GeneralSettings>();
mockAltWindowCycleSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<AltWindowCycleSettings>();
}
private AltWindowCycleViewModel CreateViewModel(Mock<SettingsUtils> settingsUtilsMock, Func<string, int> ipcCallback = null)
{
ipcCallback ??= msg => 0;
return new AltWindowCycleViewModel(
settingsUtilsMock.Object,
SettingsRepository<GeneralSettings>.GetInstance(mockGeneralSettingsUtils.Object),
SettingsRepository<AltWindowCycleSettings>.GetInstance(mockAltWindowCycleSettingsUtils.Object),
ipcCallback);
}
[TestMethod]
public void Constructor_ShouldLoadDefaultShortcutsFromSettings()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var viewModel = CreateViewModel(settingsUtilsMock);
Assert.IsNotNull(viewModel.NextWindowShortcut);
Assert.IsTrue(viewModel.NextWindowShortcut.Alt);
Assert.IsFalse(viewModel.NextWindowShortcut.Shift);
Assert.AreEqual(0xC0, viewModel.NextWindowShortcut.Code);
Assert.IsNotNull(viewModel.PreviousWindowShortcut);
Assert.IsTrue(viewModel.PreviousWindowShortcut.Alt);
Assert.IsTrue(viewModel.PreviousWindowShortcut.Shift);
Assert.AreEqual(0xC0, viewModel.PreviousWindowShortcut.Code);
}
[TestMethod]
public void IsEnabled_WhenSet_ShouldSendEnabledGeneralSettings()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var ipcInvoked = false;
Func<string, int> sendMockIPCConfigMSG = msg =>
{
OutGoingGeneralSettings snd = JsonSerializer.Deserialize<OutGoingGeneralSettings>(msg);
Assert.IsTrue(snd.GeneralSettings.Enabled.AltWindowCycle);
ipcInvoked = true;
return 0;
};
var viewModel = CreateViewModel(settingsUtilsMock, sendMockIPCConfigMSG);
viewModel.IsEnabled = true;
Assert.IsTrue(ipcInvoked, "Enabling the module should send an OutGoingGeneralSettings IPC message.");
Assert.IsTrue(viewModel.IsEnabled);
}
[TestMethod]
public void GpoNotConfigured_ShouldNotReportPolicyManaged()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var viewModel = CreateViewModel(settingsUtilsMock);
// With no enterprise policy applied in the test environment, the module must be
// user-controllable (not reported as GPO-managed).
Assert.IsFalse(viewModel.IsEnabledGpoConfigured);
}
[TestMethod]
public void NextWindowShortcut_WhenChanged_ShouldPersistSettings()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var viewModel = CreateViewModel(settingsUtilsMock);
var newShortcut = new HotkeySettings(true, true, false, false, 0x4E); // Win+Ctrl+N
viewModel.NextWindowShortcut = newShortcut;
Assert.AreSame(newShortcut, viewModel.NextWindowShortcut);
settingsUtilsMock.Verify(
x => x.SaveSettings(
It.Is<string>(json => json.Contains("next_window_shortcut")),
It.Is<string>(name => name == AltWindowCycleSettings.ModuleName),
It.IsAny<string>()),
Times.AtLeastOnce);
}
[TestMethod]
public void PreviousWindowShortcut_WhenChanged_ShouldPersistSettings()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var viewModel = CreateViewModel(settingsUtilsMock);
var newShortcut = new HotkeySettings(true, true, false, true, 0x50); // Win+Ctrl+Shift+P
viewModel.PreviousWindowShortcut = newShortcut;
Assert.AreSame(newShortcut, viewModel.PreviousWindowShortcut);
settingsUtilsMock.Verify(
x => x.SaveSettings(
It.Is<string>(json => json.Contains("previous_window_shortcut")),
It.Is<string>(name => name == AltWindowCycleSettings.ModuleName),
It.IsAny<string>()),
Times.AtLeastOnce);
}
[TestMethod]
public void NextWindowShortcut_WhenSetToNull_ShouldFallBackToDefault()
{
var settingsUtilsMock = new Mock<SettingsUtils>(new FileSystem(), null);
var viewModel = CreateViewModel(settingsUtilsMock);
viewModel.NextWindowShortcut = null;
Assert.IsNotNull(viewModel.NextWindowShortcut);
Assert.IsTrue(viewModel.NextWindowShortcut.Alt);
Assert.IsFalse(viewModel.NextWindowShortcut.Shift);
Assert.AreEqual(0xC0, viewModel.NextWindowShortcut.Code);
}
}
}

View File

@@ -0,0 +1,29 @@
// 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.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ViewModelTests
{
[TestClass]
public class OobeShellViewModelTests
{
[TestMethod]
public void ModulesPlacesAltWindowCycleImmediatelyBeforeWorkspaces()
{
var viewModel = new OobeShellViewModel();
var moduleNames = viewModel.Modules.Select(module => module.ModuleName).ToList();
int altWindowCycleIndex = moduleNames.IndexOf(PowerToysModules.AltWindowCycle.ToString());
int workspacesIndex = moduleNames.IndexOf(PowerToysModules.Workspaces.ToString());
Assert.IsTrue(altWindowCycleIndex >= 0);
Assert.AreEqual(altWindowCycleIndex + 1, workspacesIndex);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -27,6 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.FancyZones: return GPOWrapper.GetConfiguredFancyZonesEnabledValue();
case ModuleType.FileLocksmith: return GPOWrapper.GetConfiguredFileLocksmithEnabledValue();
case ModuleType.FindMyMouse: return GPOWrapper.GetConfiguredFindMyMouseEnabledValue();
case ModuleType.AltWindowCycle: return GPOWrapper.GetConfiguredAltWindowCycleEnabledValue();
case ModuleType.Hosts: return GPOWrapper.GetConfiguredHostsFileEditorEnabledValue();
case ModuleType.ImageResizer: return GPOWrapper.GetConfiguredImageResizerEnabledValue();
case ModuleType.KeyboardManager: return GPOWrapper.GetConfiguredKeyboardManagerEnabledValue();
@@ -68,6 +69,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
ModuleType.FancyZones => typeof(FancyZonesPage),
ModuleType.FileLocksmith => typeof(FileLocksmithPage),
ModuleType.FindMyMouse => typeof(MouseUtilsPage),
ModuleType.AltWindowCycle => typeof(AltWindowCyclePage),
ModuleType.GeneralSettings => typeof(GeneralPage),
ModuleType.Hosts => typeof(HostsPage),
ModuleType.ImageResizer => typeof(ImageResizerPage),

View File

@@ -37,5 +37,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
GrabAndMove,
NewPlus,
ZoomIt,
AltWindowCycle,
}
}

View File

@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.ViewModel
(PowerToysModules.TextExtractor, false),
(PowerToysModules.MeasureTool, false),
(PowerToysModules.Hosts, false),
(PowerToysModules.AltWindowCycle, false),
(PowerToysModules.Workspaces, false),
(PowerToysModules.GrabAndMove, false),
(PowerToysModules.RegistryPreview, false),

View File

@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(AltWindowCycleSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(LightSwitchSettings))]

View File

@@ -416,6 +416,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "Dashboard": return typeof(DashboardPage);
case "Overview": return typeof(GeneralPage);
case "AdvancedPaste": return typeof(AdvancedPastePage);
case "AltWindowCycle": return typeof(AltWindowCyclePage);
case "AlwaysOnTop": return typeof(AlwaysOnTopPage);
case "Awake": return typeof(AwakePage);
case "CmdNotFound": return typeof(CmdNotFoundPage);

View File

@@ -0,0 +1,34 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeAltWindowCycle"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl
x:Uid="Oobe_AltWindowCycle"
HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/WindowHopper.png"
HeroImageHeight="192">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<ptcontrols:ShortcutWithTextLabelControl x:Name="NextWindowHotkeyControl" x:Uid="Oobe_AltWindowCycle_HowToUse_Next" />
<ptcontrols:ShortcutWithTextLabelControl x:Name="PreviousWindowHotkeyControl" x:Uid="Oobe_AltWindowCycle_HowToUse_Previous" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tkcontrols:MarkdownTextBlock x:Uid="Oobe_AltWindowCycle_TipsAndTricks" />
<Button
x:Uid="OOBE_Settings"
AutomationProperties.AutomationId="AltWindowCycleOpenSettingsButton"
Click="SettingsLaunchButton_Click" />
</StackPanel>
</controls:OOBEPageControl.PageContent>
</controls:OOBEPageControl>
</Page>

View File

@@ -0,0 +1,49 @@
// 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.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeAltWindowCycle : Page
{
public OobePowerToysModule ViewModel { get; }
public OobeAltWindowCycle()
{
this.InitializeComponent();
ViewModel = App.OobeShellViewModel.GetModule(PowerToysModules.AltWindowCycle);
DataContext = ViewModel;
}
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (OobeWindow.OpenMainWindowCallback != null)
{
OobeWindow.OpenMainWindowCallback(typeof(AltWindowCyclePage));
}
ViewModel.LogOpeningSettingsEvent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
AltWindowCycleSettings settings = SettingsRepository<AltWindowCycleSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
NextWindowHotkeyControl.Keys = settings.Properties.NextWindowShortcut.GetKeysList();
PreviousWindowHotkeyControl.Keys = settings.Properties.PreviousWindowShortcut.GetKeysList();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
}
}
}

View File

@@ -166,6 +166,11 @@
x:Uid="Shell_TextExtractor"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/TextExtractor.png}"
Tag="TextExtractor" />
<NavigationViewItem
x:Uid="Shell_AltWindowCycle"
AutomationProperties.AutomationId="AltWindowCycleOobeNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowHopper.png}"
Tag="AltWindowCycle" />
<NavigationViewItem
x:Uid="Shell_Workspaces"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Workspaces.png}"

View File

@@ -110,6 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI
case "RegistryPreview": NavigationFrame.Navigate(typeof(OobeRegistryPreview)); break;
case "Peek": NavigationFrame.Navigate(typeof(OobePeek)); break;
case "NewPlus": NavigationFrame.Navigate(typeof(OobeNewPlus)); break;
case "AltWindowCycle": NavigationFrame.Navigate(typeof(OobeAltWindowCycle)); break;
case "Workspaces": NavigationFrame.Navigate(typeof(OobeWorkspaces)); break;
case "GrabAndMove": NavigationFrame.Navigate(typeof(OobeGrabAndMove)); break;
case "ZoomIt": NavigationFrame.Navigate(typeof(OobeZoomIt)); break;

View File

@@ -0,0 +1,39 @@
<local:NavigablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.AltWindowCyclePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources />
<controls:SettingsPageControl
x:Uid="AltWindowCycle"
IsTabStop="False"
ModuleImageSource="ms-appx:///Assets/Settings/Modules/WindowHopper.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
Name="AltWindowCycleEnableToggleControlHeaderText"
x:Uid="AltWindowCycle_EnableToggleControl_HeaderText"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowHopper.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="AltWindowCycle_Activation_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="AltWindowCycle_NextWindowShortcut" HeaderIcon="{ui:FontIcon Glyph=&#xE72A;}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.NextWindowShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="AltWindowCycle_PreviousWindowShortcut" HeaderIcon="{ui:FontIcon Glyph=&#xE72B;}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.PreviousWindowShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
</controls:SettingsPageControl>
</local:NavigablePage>

View File

@@ -0,0 +1,31 @@
// 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.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AltWindowCyclePage : NavigablePage, IRefreshablePage
{
private AltWindowCycleViewModel ViewModel { get; set; }
public AltWindowCyclePage()
{
var settingsUtils = SettingsUtils.Default;
ViewModel = new AltWindowCycleViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<AltWindowCycleSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
Loaded += (s, e) => ViewModel.OnPageLoaded();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
}
}

View File

@@ -288,6 +288,12 @@
helpers:NavHelper.NavigateTo="views:GrabAndMovePage"
AutomationProperties.AutomationId="GrabAndMoveNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}" />
<NavigationViewItem
x:Name="AltWindowCycleNavigationItem"
x:Uid="Shell_AltWindowCycle"
helpers:NavHelper.NavigateTo="views:AltWindowCyclePage"
AutomationProperties.AutomationId="AltWindowCycleNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowHopper.png}" />
<NavigationViewItem
x:Name="WorkspacesNavigationItem"
x:Uid="Shell_Workspaces"

View File

@@ -3022,6 +3022,54 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Oobe_CropAndLock_HowToUse_Screenshot.Text" xml:space="preserve">
<value>to crop an application's window into a screenshot window. The screenshot won't update with the original window's content.</value>
</data>
<data name="AltWindowCycle.ModuleDescription" xml:space="preserve">
<value>Window Hopper lets you cycle between windows of the currently focused application.</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="AltWindowCycle.ModuleTitle" xml:space="preserve">
<value>Window Hopper</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="AltWindowCycle_Activation_GroupSettings.Header" xml:space="preserve">
<value>Activation</value>
</data>
<data name="AltWindowCycle_EnableToggleControl_HeaderText.Header" xml:space="preserve">
<value>Enable Window Hopper</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="AltWindowCycle_NextWindowShortcut.Description" xml:space="preserve">
<value>Cycle to the next window of the focused app</value>
</data>
<data name="AltWindowCycle_NextWindowShortcut.Header" xml:space="preserve">
<value>Next window</value>
</data>
<data name="AltWindowCycle_PreviousWindowShortcut.Description" xml:space="preserve">
<value>Cycle to the previous window of the focused app</value>
</data>
<data name="AltWindowCycle_PreviousWindowShortcut.Header" xml:space="preserve">
<value>Previous window</value>
</data>
<data name="Shell_AltWindowCycle.Content" xml:space="preserve">
<value>Window Hopper</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="Oobe_AltWindowCycle.Title" xml:space="preserve">
<value>Window Hopper</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="Oobe_AltWindowCycle.Description" xml:space="preserve">
<value>Window Hopper lets you cycle between windows of the currently focused application.</value>
<comment>"Window Hopper" is the name of the utility</comment>
</data>
<data name="Oobe_AltWindowCycle_HowToUse_Next.Text" xml:space="preserve">
<value>to cycle to the next window of the focused app.</value>
</data>
<data name="Oobe_AltWindowCycle_HowToUse_Previous.Text" xml:space="preserve">
<value>to cycle to the previous window of the focused app.</value>
</data>
<data name="Oobe_AltWindowCycle_TipsAndTricks.Text" xml:space="preserve">
<value>Use the next and previous shortcuts to move through only the windows that belong to your currently focused app.</value>
</data>
<data name="AlwaysOnTop.ModuleDescription" xml:space="preserve">
<value>Always On Top is a quick and easy way to pin windows on top.</value>
</data>

View File

@@ -0,0 +1,174 @@
// 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.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class AltWindowCycleViewModel : PageViewModelBase
{
protected override string ModuleName => AltWindowCycleSettings.ModuleName;
private SettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; }
private AltWindowCycleSettings Settings { get; set; }
private Func<string, int> SendConfigMSG { get; }
public AltWindowCycleViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<AltWindowCycleSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
ArgumentNullException.ThrowIfNull(settingsUtils);
SettingsUtils = settingsUtils;
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
InitializeEnabledValue();
// To obtain the settings configurations of AltWindowCycle.
ArgumentNullException.ThrowIfNull(moduleSettingsRepository);
Settings = moduleSettingsRepository.SettingsConfig;
_nextWindowShortcut = Settings.Properties.NextWindowShortcut;
_previousWindowShortcut = Settings.Properties.PreviousWindowShortcut;
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
}
private void InitializeEnabledValue()
{
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAltWindowCycleEnabledValue();
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isEnabled = GeneralSettingsConfig.Enabled.AltWindowCycle;
}
}
public override Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{
[ModuleName] = [NextWindowShortcut, PreviousWindowShortcut],
};
return hotkeysDict;
}
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (value != _isEnabled)
{
_isEnabled = value;
// Set the status in the general settings configuration
GeneralSettingsConfig.Enabled.AltWindowCycle = value;
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(snd.ToString());
OnPropertyChanged(nameof(IsEnabled));
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
}
public HotkeySettings NextWindowShortcut
{
get => _nextWindowShortcut;
set
{
if (value != _nextWindowShortcut)
{
_nextWindowShortcut = value ?? Settings.Properties.DefaultNextWindowShortcut;
Settings.Properties.NextWindowShortcut = _nextWindowShortcut;
NotifyPropertyChanged();
SendSettingsConfigMessage();
}
}
}
public HotkeySettings PreviousWindowShortcut
{
get => _previousWindowShortcut;
set
{
if (value != _previousWindowShortcut)
{
_previousWindowShortcut = value ?? Settings.Properties.DefaultPreviousWindowShortcut;
Settings.Properties.PreviousWindowShortcut = _previousWindowShortcut;
NotifyPropertyChanged();
SendSettingsConfigMessage();
}
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
SettingsUtils.SaveSettings(Settings.ToJsonString(), AltWindowCycleSettings.ModuleName);
}
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsEnabled));
}
private void SendSettingsConfigMessage()
{
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
AltWindowCycleSettings.ModuleName,
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.AltWindowCycleSettings)));
}
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isEnabled;
private HotkeySettings _nextWindowShortcut;
private HotkeySettings _previousWindowShortcut;
}
}

View File

@@ -493,6 +493,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
ModuleType.EnvironmentVariables => GetModuleItemsEnvironmentVariables(),
ModuleType.FancyZones => GetModuleItemsFancyZones(),
ModuleType.FindMyMouse => GetModuleItemsFindMyMouse(),
ModuleType.AltWindowCycle => GetModuleItemsAltWindowCycle(),
ModuleType.Hosts => GetModuleItemsHosts(),
ModuleType.KeyboardManager => GetModuleItemsKeyboardManager(),
ModuleType.LightSwitch => GetModuleItemsLightSwitch(),
@@ -626,6 +627,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return new ObservableCollection<DashboardModuleItem>(list);
}
private ObservableCollection<DashboardModuleItem> GetModuleItemsAltWindowCycle()
{
ISettingsRepository<AltWindowCycleSettings> moduleSettingsRepository = SettingsRepository<AltWindowCycleSettings>.GetInstance(SettingsUtils.Default);
var settings = moduleSettingsRepository.SettingsConfig;
var list = new List<DashboardModuleItem>
{
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AltWindowCycle_NextWindowShortcut/Header"), Shortcut = settings.Properties.NextWindowShortcut.GetKeysList() },
new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AltWindowCycle_PreviousWindowShortcut/Header"), Shortcut = settings.Properties.PreviousWindowShortcut.GetKeysList() },
};
return new ObservableCollection<DashboardModuleItem>(list);
}
private ObservableCollection<DashboardModuleItem> GetModuleItemsHosts()
{
var list = new List<DashboardModuleItem>

View File

@@ -334,6 +334,7 @@ function Test-CoreFiles {
# Module executables and libraries
'PowerToys.AlwaysOnTop.exe',
'PowerToys.AlwaysOnTopModuleInterface.dll',
'PowerToys.AltWindowCycle.dll',
'PowerToys.CmdNotFoundModuleInterface.dll',
'PowerToys.ColorPicker.dll',
'PowerToys.ColorPickerUI.dll',