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
1
.github/actions/spell-check/allow/code.txt
vendored
@@ -330,6 +330,7 @@ xes
|
|||||||
PACKAGEVERSIONNUMBER
|
PACKAGEVERSIONNUMBER
|
||||||
APPXMANIFESTVERSION
|
APPXMANIFESTVERSION
|
||||||
PROGMAN
|
PROGMAN
|
||||||
|
ROOTOWNER
|
||||||
|
|
||||||
# MRU lists
|
# MRU lists
|
||||||
CACHEWRITE
|
CACHEWRITE
|
||||||
|
|||||||
9
.github/actions/spell-check/expect.txt
vendored
@@ -10,6 +10,7 @@ ACCESSDENIED
|
|||||||
ACCESSTOKEN
|
ACCESSTOKEN
|
||||||
acfs
|
acfs
|
||||||
ACIE
|
ACIE
|
||||||
|
ACRYLICBLURBEHIND
|
||||||
acrt
|
acrt
|
||||||
ACTIVATEAPP
|
ACTIVATEAPP
|
||||||
ACTIVATEOPTIONS
|
ACTIVATEOPTIONS
|
||||||
@@ -93,6 +94,7 @@ ASSOCSTR
|
|||||||
ASYNCWINDOWPLACEMENT
|
ASYNCWINDOWPLACEMENT
|
||||||
ASYNCWINDOWPOS
|
ASYNCWINDOWPOS
|
||||||
atl
|
atl
|
||||||
|
AWC
|
||||||
ATRIOX
|
ATRIOX
|
||||||
aumid
|
aumid
|
||||||
AUO
|
AUO
|
||||||
@@ -465,6 +467,7 @@ dwm
|
|||||||
dwmapi
|
dwmapi
|
||||||
DWMCOLORIZATIONCOLORCHANGED
|
DWMCOLORIZATIONCOLORCHANGED
|
||||||
DWMCOMPOSITIONCHANGED
|
DWMCOMPOSITIONCHANGED
|
||||||
|
DWMSBT
|
||||||
DWMNCRENDERINGCHANGED
|
DWMNCRENDERINGCHANGED
|
||||||
Dwmp
|
Dwmp
|
||||||
DWMSENDICONICLIVEPREVIEWBITMAP
|
DWMSENDICONICLIVEPREVIEWBITMAP
|
||||||
@@ -1391,6 +1394,7 @@ pnid
|
|||||||
PNMLINK
|
PNMLINK
|
||||||
Poc
|
Poc
|
||||||
Podcasts
|
Podcasts
|
||||||
|
PARGB
|
||||||
POINTERID
|
POINTERID
|
||||||
POINTERUPDATE
|
POINTERUPDATE
|
||||||
Pokedex
|
Pokedex
|
||||||
@@ -1857,6 +1861,7 @@ SYSLIB
|
|||||||
sysmenu
|
sysmenu
|
||||||
systemai
|
systemai
|
||||||
SYSTEMAPPS
|
SYSTEMAPPS
|
||||||
|
SYSTEMBACKDROP
|
||||||
SYSTEMMODAL
|
SYSTEMMODAL
|
||||||
systemroot
|
systemroot
|
||||||
SYSTEMTIME
|
SYSTEMTIME
|
||||||
@@ -1931,6 +1936,7 @@ tracerpt
|
|||||||
trackbar
|
trackbar
|
||||||
trafficmanager
|
trafficmanager
|
||||||
transicc
|
transicc
|
||||||
|
TRANSIENTWINDOW
|
||||||
transitioning
|
transitioning
|
||||||
TRAYMOUSEMESSAGE
|
TRAYMOUSEMESSAGE
|
||||||
triaging
|
triaging
|
||||||
@@ -2075,7 +2081,7 @@ WANTNUKEWARNING
|
|||||||
WANTPALM
|
WANTPALM
|
||||||
WASDK
|
WASDK
|
||||||
wbem
|
wbem
|
||||||
Wca
|
wca
|
||||||
WCE
|
WCE
|
||||||
wcex
|
wcex
|
||||||
WCRAPI
|
WCRAPI
|
||||||
@@ -2103,6 +2109,7 @@ winappcli
|
|||||||
winappsdk
|
winappsdk
|
||||||
windir
|
windir
|
||||||
WINDOWCREATED
|
WINDOWCREATED
|
||||||
|
WINDOWCOMPOSITIONATTRIBDATA
|
||||||
WINDOWDESTROYED
|
WINDOWDESTROYED
|
||||||
windowedge
|
windowedge
|
||||||
WINDOWINFO
|
WINDOWINFO
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
"PowerToys.AlwaysOnTop.exe",
|
"PowerToys.AlwaysOnTop.exe",
|
||||||
"PowerToys.AlwaysOnTopModuleInterface.dll",
|
"PowerToys.AlwaysOnTopModuleInterface.dll",
|
||||||
|
"PowerToys.AltWindowCycle.dll",
|
||||||
|
|
||||||
"PowerToys.CmdNotFoundModuleInterface.dll",
|
"PowerToys.CmdNotFoundModuleInterface.dll",
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,10 @@
|
|||||||
<Platform Solution="*|x64" Project="x64" />
|
<Platform Solution="*|x64" Project="x64" />
|
||||||
</Project>
|
</Project>
|
||||||
</Folder>
|
</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/">
|
<Folder Name="/modules/AlwaysOnTop/">
|
||||||
<Project Path="src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj" Id="1dc3be92-ce89-43fb-8110-9c043a2fe7a2" />
|
<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" />
|
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />
|
||||||
|
|||||||
BIN
doc/images/icons/WindowHopper.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
doc/images/overview/Original/WindowHopper.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
doc/images/overview/WindowHopper_large.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
doc/images/overview/WindowHopper_small.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
@@ -8,6 +8,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
|||||||
{
|
{
|
||||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAlwaysOnTopEnabledValue());
|
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAlwaysOnTopEnabledValue());
|
||||||
}
|
}
|
||||||
|
GpoRuleConfigured GPOWrapper::GetConfiguredAltWindowCycleEnabledValue()
|
||||||
|
{
|
||||||
|
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAltWindowCycleEnabledValue());
|
||||||
|
}
|
||||||
GpoRuleConfigured GPOWrapper::GetConfiguredAwakeEnabledValue()
|
GpoRuleConfigured GPOWrapper::GetConfiguredAwakeEnabledValue()
|
||||||
{
|
{
|
||||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAwakeEnabledValue());
|
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredAwakeEnabledValue());
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
|||||||
{
|
{
|
||||||
GPOWrapper() = default;
|
GPOWrapper() = default;
|
||||||
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
|
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
|
||||||
|
static GpoRuleConfigured GetConfiguredAltWindowCycleEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
|
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
|
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
|
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace PowerToys
|
|||||||
};
|
};
|
||||||
[default_interface] static runtimeclass GPOWrapper {
|
[default_interface] static runtimeclass GPOWrapper {
|
||||||
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
|
static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue();
|
||||||
|
static GpoRuleConfigured GetConfiguredAltWindowCycleEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
|
static GpoRuleConfigured GetConfiguredAwakeEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
|
static GpoRuleConfigured GetConfiguredCmdNotFoundEnabledValue();
|
||||||
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
|
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace ManagedCommon
|
|||||||
public enum ModuleType
|
public enum ModuleType
|
||||||
{
|
{
|
||||||
AdvancedPaste,
|
AdvancedPaste,
|
||||||
|
AltWindowCycle,
|
||||||
AlwaysOnTop,
|
AlwaysOnTop,
|
||||||
Awake,
|
Awake,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ namespace powertoys_gpo
|
|||||||
const std::wstring POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS = L"ConfigureEnabledUtilityFileExplorerQOIThumbnails";
|
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_NEWPLUS = L"ConfigureEnabledUtilityNewPlus";
|
||||||
const std::wstring POLICY_CONFIGURE_ENABLED_WORKSPACES = L"ConfigureEnabledUtilityWorkspaces";
|
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.
|
// The registry value names for PowerToys installer and update policies.
|
||||||
const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled";
|
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);
|
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()
|
inline gpo_rule_configured_t getConfiguredAwakeEnabledValue()
|
||||||
{
|
{
|
||||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_AWAKE);
|
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_AWAKE);
|
||||||
|
|||||||
@@ -91,6 +91,16 @@
|
|||||||
<decimal value="0" />
|
<decimal value="0" />
|
||||||
</disabledValue>
|
</disabledValue>
|
||||||
</policy>
|
</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">
|
<policy name="ConfigureEnabledUtilityAwake" class="Both" displayName="$(string.ConfigureEnabledUtilityAwake)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityAwake">
|
||||||
<parentCategory ref="PowerToys" />
|
<parentCategory ref="PowerToys" />
|
||||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />
|
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />
|
||||||
|
|||||||
@@ -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="ConfigureAllUtilityGlobalEnabledState">Configure global utility enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityAlwaysOnTop">Always On Top: 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="ConfigureEnabledUtilityAwake">Awake: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityColorPicker">Color Picker: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityColorPicker">Color Picker: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityCmdNotFound">Command Not Found: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityCmdNotFound">Command Not Found: Configure enabled state</string>
|
||||||
|
|||||||
1694
src/modules/AltWindowCycle/AltWindowCycle.cpp
Normal file
21
src/modules/AltWindowCycle/AltWindowCycle.h
Normal 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);
|
||||||
40
src/modules/AltWindowCycle/AltWindowCycle.rc
Normal 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
|
||||||
130
src/modules/AltWindowCycle/AltWindowCycle.vcxproj
Normal 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>
|
||||||
56
src/modules/AltWindowCycle/AltWindowCycle.vcxproj.filters
Normal 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>
|
||||||
295
src/modules/AltWindowCycle/AltWindowCycleLogic.h
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
305
src/modules/AltWindowCycle/dllmain.cpp
Normal 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();
|
||||||
|
}
|
||||||
4
src/modules/AltWindowCycle/packages.config
Normal 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>
|
||||||
1
src/modules/AltWindowCycle/pch.cpp
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "pch.h"
|
||||||
15
src/modules/AltWindowCycle/pch.h
Normal 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>
|
||||||
13
src/modules/AltWindowCycle/resource.h
Normal 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
|
||||||
|
//////////////////////////////
|
||||||
33
src/modules/AltWindowCycle/trace.cpp
Normal 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"));
|
||||||
|
}
|
||||||
13
src/modules/AltWindowCycle/trace.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -288,6 +288,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
|||||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||||
L"PowerToys.PowerDisplayModuleInterface.dll",
|
L"PowerToys.PowerDisplayModuleInterface.dll",
|
||||||
L"PowerToys.GrabAndMoveModuleInterface.dll",
|
L"PowerToys.GrabAndMoveModuleInterface.dll",
|
||||||
|
L"PowerToys.AltWindowCycle.dll",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto moduleSubdir : knownModules)
|
for (auto moduleSubdir : knownModules)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ internal static class ModuleGpoHelper
|
|||||||
ModuleType.FancyZones => GPOWrapper.GetConfiguredFancyZonesEnabledValue(),
|
ModuleType.FancyZones => GPOWrapper.GetConfiguredFancyZonesEnabledValue(),
|
||||||
ModuleType.FileLocksmith => GPOWrapper.GetConfiguredFileLocksmithEnabledValue(),
|
ModuleType.FileLocksmith => GPOWrapper.GetConfiguredFileLocksmithEnabledValue(),
|
||||||
ModuleType.FindMyMouse => GPOWrapper.GetConfiguredFindMyMouseEnabledValue(),
|
ModuleType.FindMyMouse => GPOWrapper.GetConfiguredFindMyMouseEnabledValue(),
|
||||||
|
ModuleType.AltWindowCycle => GPOWrapper.GetConfiguredAltWindowCycleEnabledValue(),
|
||||||
ModuleType.Hosts => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue(),
|
ModuleType.Hosts => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue(),
|
||||||
ModuleType.ImageResizer => GPOWrapper.GetConfiguredImageResizerEnabledValue(),
|
ModuleType.ImageResizer => GPOWrapper.GetConfiguredImageResizerEnabledValue(),
|
||||||
ModuleType.KeyboardManager => GPOWrapper.GetConfiguredKeyboardManagerEnabledValue(),
|
ModuleType.KeyboardManager => GPOWrapper.GetConfiguredKeyboardManagerEnabledValue(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
private bool mouseHighlighter = true;
|
||||||
|
|
||||||
[JsonPropertyName("MouseHighlighter")]
|
[JsonPropertyName("MouseHighlighter")]
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
|||||||
return moduleType switch
|
return moduleType switch
|
||||||
{
|
{
|
||||||
ModuleType.AdvancedPaste => "ms-appx:///Assets/Settings/Icons/AdvancedPaste.png",
|
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.Workspaces => "ms-appx:///Assets/Settings/Icons/Workspaces.png",
|
||||||
ModuleType.PowerOCR => "ms-appx:///Assets/Settings/Icons/TextExtractor.png",
|
ModuleType.PowerOCR => "ms-appx:///Assets/Settings/Icons/TextExtractor.png",
|
||||||
ModuleType.PowerAccent => "ms-appx:///Assets/Settings/Icons/QuickAccent.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.FancyZones => generalSettingsConfig.Enabled.FancyZones,
|
||||||
ModuleType.FileLocksmith => generalSettingsConfig.Enabled.FileLocksmith,
|
ModuleType.FileLocksmith => generalSettingsConfig.Enabled.FileLocksmith,
|
||||||
ModuleType.FindMyMouse => generalSettingsConfig.Enabled.FindMyMouse,
|
ModuleType.FindMyMouse => generalSettingsConfig.Enabled.FindMyMouse,
|
||||||
|
ModuleType.AltWindowCycle => generalSettingsConfig.Enabled.AltWindowCycle,
|
||||||
ModuleType.Hosts => generalSettingsConfig.Enabled.Hosts,
|
ModuleType.Hosts => generalSettingsConfig.Enabled.Hosts,
|
||||||
ModuleType.ImageResizer => generalSettingsConfig.Enabled.ImageResizer,
|
ModuleType.ImageResizer => generalSettingsConfig.Enabled.ImageResizer,
|
||||||
ModuleType.KeyboardManager => generalSettingsConfig.Enabled.KeyboardManager,
|
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.FancyZones: generalSettingsConfig.Enabled.FancyZones = isEnabled; break;
|
||||||
case ModuleType.FileLocksmith: generalSettingsConfig.Enabled.FileLocksmith = isEnabled; break;
|
case ModuleType.FileLocksmith: generalSettingsConfig.Enabled.FileLocksmith = isEnabled; break;
|
||||||
case ModuleType.FindMyMouse: generalSettingsConfig.Enabled.FindMyMouse = 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.Hosts: generalSettingsConfig.Enabled.Hosts = isEnabled; break;
|
||||||
case ModuleType.ImageResizer: generalSettingsConfig.Enabled.ImageResizer = isEnabled; break;
|
case ModuleType.ImageResizer: generalSettingsConfig.Enabled.ImageResizer = isEnabled; break;
|
||||||
case ModuleType.KeyboardManager: generalSettingsConfig.Enabled.KeyboardManager = 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.FancyZones => FancyZonesSettings.ModuleName,
|
||||||
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
|
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
|
||||||
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
|
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
|
||||||
|
ModuleType.AltWindowCycle => AltWindowCycleSettings.ModuleName,
|
||||||
ModuleType.Hosts => HostsSettings.ModuleName,
|
ModuleType.Hosts => HostsSettings.ModuleName,
|
||||||
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
|
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
|
||||||
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,
|
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonSerializable(typeof(GeneralSettings))]
|
[JsonSerializable(typeof(GeneralSettings))]
|
||||||
[JsonSerializable(typeof(OutGoingGeneralSettings))]
|
[JsonSerializable(typeof(OutGoingGeneralSettings))]
|
||||||
[JsonSerializable(typeof(AdvancedPasteSettings))]
|
[JsonSerializable(typeof(AdvancedPasteSettings))]
|
||||||
|
[JsonSerializable(typeof(AltWindowCycleSettings))]
|
||||||
[JsonSerializable(typeof(AlwaysOnTopSettings))]
|
[JsonSerializable(typeof(AlwaysOnTopSettings))]
|
||||||
[JsonSerializable(typeof(AwakeSettings))]
|
[JsonSerializable(typeof(AwakeSettings))]
|
||||||
[JsonSerializable(typeof(CmdNotFoundSettings))]
|
[JsonSerializable(typeof(CmdNotFoundSettings))]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -27,6 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
|||||||
case ModuleType.FancyZones: return GPOWrapper.GetConfiguredFancyZonesEnabledValue();
|
case ModuleType.FancyZones: return GPOWrapper.GetConfiguredFancyZonesEnabledValue();
|
||||||
case ModuleType.FileLocksmith: return GPOWrapper.GetConfiguredFileLocksmithEnabledValue();
|
case ModuleType.FileLocksmith: return GPOWrapper.GetConfiguredFileLocksmithEnabledValue();
|
||||||
case ModuleType.FindMyMouse: return GPOWrapper.GetConfiguredFindMyMouseEnabledValue();
|
case ModuleType.FindMyMouse: return GPOWrapper.GetConfiguredFindMyMouseEnabledValue();
|
||||||
|
case ModuleType.AltWindowCycle: return GPOWrapper.GetConfiguredAltWindowCycleEnabledValue();
|
||||||
case ModuleType.Hosts: return GPOWrapper.GetConfiguredHostsFileEditorEnabledValue();
|
case ModuleType.Hosts: return GPOWrapper.GetConfiguredHostsFileEditorEnabledValue();
|
||||||
case ModuleType.ImageResizer: return GPOWrapper.GetConfiguredImageResizerEnabledValue();
|
case ModuleType.ImageResizer: return GPOWrapper.GetConfiguredImageResizerEnabledValue();
|
||||||
case ModuleType.KeyboardManager: return GPOWrapper.GetConfiguredKeyboardManagerEnabledValue();
|
case ModuleType.KeyboardManager: return GPOWrapper.GetConfiguredKeyboardManagerEnabledValue();
|
||||||
@@ -68,6 +69,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
|||||||
ModuleType.FancyZones => typeof(FancyZonesPage),
|
ModuleType.FancyZones => typeof(FancyZonesPage),
|
||||||
ModuleType.FileLocksmith => typeof(FileLocksmithPage),
|
ModuleType.FileLocksmith => typeof(FileLocksmithPage),
|
||||||
ModuleType.FindMyMouse => typeof(MouseUtilsPage),
|
ModuleType.FindMyMouse => typeof(MouseUtilsPage),
|
||||||
|
ModuleType.AltWindowCycle => typeof(AltWindowCyclePage),
|
||||||
ModuleType.GeneralSettings => typeof(GeneralPage),
|
ModuleType.GeneralSettings => typeof(GeneralPage),
|
||||||
ModuleType.Hosts => typeof(HostsPage),
|
ModuleType.Hosts => typeof(HostsPage),
|
||||||
ModuleType.ImageResizer => typeof(ImageResizerPage),
|
ModuleType.ImageResizer => typeof(ImageResizerPage),
|
||||||
|
|||||||
@@ -37,5 +37,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
|||||||
GrabAndMove,
|
GrabAndMove,
|
||||||
NewPlus,
|
NewPlus,
|
||||||
ZoomIt,
|
ZoomIt,
|
||||||
|
AltWindowCycle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.ViewModel
|
|||||||
(PowerToysModules.TextExtractor, false),
|
(PowerToysModules.TextExtractor, false),
|
||||||
(PowerToysModules.MeasureTool, false),
|
(PowerToysModules.MeasureTool, false),
|
||||||
(PowerToysModules.Hosts, false),
|
(PowerToysModules.Hosts, false),
|
||||||
|
(PowerToysModules.AltWindowCycle, false),
|
||||||
(PowerToysModules.Workspaces, false),
|
(PowerToysModules.Workspaces, false),
|
||||||
(PowerToysModules.GrabAndMove, false),
|
(PowerToysModules.GrabAndMove, false),
|
||||||
(PowerToysModules.RegistryPreview, false),
|
(PowerToysModules.RegistryPreview, false),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
|||||||
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
|
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
|
||||||
[JsonSerializable(typeof(FileLocksmithSettings))]
|
[JsonSerializable(typeof(FileLocksmithSettings))]
|
||||||
[JsonSerializable(typeof(FindMyMouseSettings))]
|
[JsonSerializable(typeof(FindMyMouseSettings))]
|
||||||
|
[JsonSerializable(typeof(AltWindowCycleSettings))]
|
||||||
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
|
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
|
||||||
[JsonSerializable(typeof(KeyboardManagerSettings))]
|
[JsonSerializable(typeof(KeyboardManagerSettings))]
|
||||||
[JsonSerializable(typeof(LightSwitchSettings))]
|
[JsonSerializable(typeof(LightSwitchSettings))]
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
|||||||
case "Dashboard": return typeof(DashboardPage);
|
case "Dashboard": return typeof(DashboardPage);
|
||||||
case "Overview": return typeof(GeneralPage);
|
case "Overview": return typeof(GeneralPage);
|
||||||
case "AdvancedPaste": return typeof(AdvancedPastePage);
|
case "AdvancedPaste": return typeof(AdvancedPastePage);
|
||||||
|
case "AltWindowCycle": return typeof(AltWindowCyclePage);
|
||||||
case "AlwaysOnTop": return typeof(AlwaysOnTopPage);
|
case "AlwaysOnTop": return typeof(AlwaysOnTopPage);
|
||||||
case "Awake": return typeof(AwakePage);
|
case "Awake": return typeof(AwakePage);
|
||||||
case "CmdNotFound": return typeof(CmdNotFoundPage);
|
case "CmdNotFound": return typeof(CmdNotFoundPage);
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,6 +166,11 @@
|
|||||||
x:Uid="Shell_TextExtractor"
|
x:Uid="Shell_TextExtractor"
|
||||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/TextExtractor.png}"
|
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/TextExtractor.png}"
|
||||||
Tag="TextExtractor" />
|
Tag="TextExtractor" />
|
||||||
|
<NavigationViewItem
|
||||||
|
x:Uid="Shell_AltWindowCycle"
|
||||||
|
AutomationProperties.AutomationId="AltWindowCycleOobeNavItem"
|
||||||
|
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowHopper.png}"
|
||||||
|
Tag="AltWindowCycle" />
|
||||||
<NavigationViewItem
|
<NavigationViewItem
|
||||||
x:Uid="Shell_Workspaces"
|
x:Uid="Shell_Workspaces"
|
||||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Workspaces.png}"
|
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Workspaces.png}"
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
|||||||
case "RegistryPreview": NavigationFrame.Navigate(typeof(OobeRegistryPreview)); break;
|
case "RegistryPreview": NavigationFrame.Navigate(typeof(OobeRegistryPreview)); break;
|
||||||
case "Peek": NavigationFrame.Navigate(typeof(OobePeek)); break;
|
case "Peek": NavigationFrame.Navigate(typeof(OobePeek)); break;
|
||||||
case "NewPlus": NavigationFrame.Navigate(typeof(OobeNewPlus)); break;
|
case "NewPlus": NavigationFrame.Navigate(typeof(OobeNewPlus)); break;
|
||||||
|
case "AltWindowCycle": NavigationFrame.Navigate(typeof(OobeAltWindowCycle)); break;
|
||||||
case "Workspaces": NavigationFrame.Navigate(typeof(OobeWorkspaces)); break;
|
case "Workspaces": NavigationFrame.Navigate(typeof(OobeWorkspaces)); break;
|
||||||
case "GrabAndMove": NavigationFrame.Navigate(typeof(OobeGrabAndMove)); break;
|
case "GrabAndMove": NavigationFrame.Navigate(typeof(OobeGrabAndMove)); break;
|
||||||
case "ZoomIt": NavigationFrame.Navigate(typeof(OobeZoomIt)); break;
|
case "ZoomIt": NavigationFrame.Navigate(typeof(OobeZoomIt)); break;
|
||||||
|
|||||||
@@ -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=}">
|
||||||
|
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.NextWindowShortcut, Mode=TwoWay}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
<tkcontrols:SettingsCard x:Uid="AltWindowCycle_PreviousWindowShortcut" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
|
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.PreviousWindowShortcut, Mode=TwoWay}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
</controls:SettingsGroup>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:SettingsPageControl.ModuleContent>
|
||||||
|
</controls:SettingsPageControl>
|
||||||
|
</local:NavigablePage>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,12 @@
|
|||||||
helpers:NavHelper.NavigateTo="views:GrabAndMovePage"
|
helpers:NavHelper.NavigateTo="views:GrabAndMovePage"
|
||||||
AutomationProperties.AutomationId="GrabAndMoveNavItem"
|
AutomationProperties.AutomationId="GrabAndMoveNavItem"
|
||||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}" />
|
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
|
<NavigationViewItem
|
||||||
x:Name="WorkspacesNavigationItem"
|
x:Name="WorkspacesNavigationItem"
|
||||||
x:Uid="Shell_Workspaces"
|
x:Uid="Shell_Workspaces"
|
||||||
|
|||||||
@@ -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">
|
<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>
|
<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>
|
||||||
|
<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">
|
<data name="AlwaysOnTop.ModuleDescription" xml:space="preserve">
|
||||||
<value>Always On Top is a quick and easy way to pin windows on top.</value>
|
<value>Always On Top is a quick and easy way to pin windows on top.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -493,6 +493,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
ModuleType.EnvironmentVariables => GetModuleItemsEnvironmentVariables(),
|
ModuleType.EnvironmentVariables => GetModuleItemsEnvironmentVariables(),
|
||||||
ModuleType.FancyZones => GetModuleItemsFancyZones(),
|
ModuleType.FancyZones => GetModuleItemsFancyZones(),
|
||||||
ModuleType.FindMyMouse => GetModuleItemsFindMyMouse(),
|
ModuleType.FindMyMouse => GetModuleItemsFindMyMouse(),
|
||||||
|
ModuleType.AltWindowCycle => GetModuleItemsAltWindowCycle(),
|
||||||
ModuleType.Hosts => GetModuleItemsHosts(),
|
ModuleType.Hosts => GetModuleItemsHosts(),
|
||||||
ModuleType.KeyboardManager => GetModuleItemsKeyboardManager(),
|
ModuleType.KeyboardManager => GetModuleItemsKeyboardManager(),
|
||||||
ModuleType.LightSwitch => GetModuleItemsLightSwitch(),
|
ModuleType.LightSwitch => GetModuleItemsLightSwitch(),
|
||||||
@@ -626,6 +627,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
return new ObservableCollection<DashboardModuleItem>(list);
|
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()
|
private ObservableCollection<DashboardModuleItem> GetModuleItemsHosts()
|
||||||
{
|
{
|
||||||
var list = new List<DashboardModuleItem>
|
var list = new List<DashboardModuleItem>
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ function Test-CoreFiles {
|
|||||||
# Module executables and libraries
|
# Module executables and libraries
|
||||||
'PowerToys.AlwaysOnTop.exe',
|
'PowerToys.AlwaysOnTop.exe',
|
||||||
'PowerToys.AlwaysOnTopModuleInterface.dll',
|
'PowerToys.AlwaysOnTopModuleInterface.dll',
|
||||||
|
'PowerToys.AltWindowCycle.dll',
|
||||||
'PowerToys.CmdNotFoundModuleInterface.dll',
|
'PowerToys.CmdNotFoundModuleInterface.dll',
|
||||||
'PowerToys.ColorPicker.dll',
|
'PowerToys.ColorPicker.dll',
|
||||||
'PowerToys.ColorPickerUI.dll',
|
'PowerToys.ColorPickerUI.dll',
|
||||||
|
|||||||