CmdPal: Add option to return to home automatically after a delay (#43551)

## Summary of the Pull Request

This PR replaces the Go home when activated setting with a new
Automatically return home option. This allows users to specify how long
the Command Palette should wait after being dismissed before
automatically returning to the home page. It also introduces migration
logic to transition from the old setting to the new one.

## Pictures? Pictures!

<img width="1337" height="762" alt="image"
src="https://github.com/user-attachments/assets/c649ef03-b3ee-40ba-ac67-485bc40efa73"
/>


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2025-11-27 16:24:47 +01:00
committed by GitHub
parent 1b72c0b969
commit 47d4a65223
6 changed files with 190 additions and 24 deletions

View File

@@ -6,7 +6,9 @@ using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation; using Windows.Foundation;
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsModel : ObservableObject public partial class SettingsModel : ObservableObject
{ {
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
[JsonIgnore] [JsonIgnore]
public static readonly string FilePath; public static readonly string FilePath;
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
public bool ShowAppDetails { get; set; } public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
public bool BackspaceGoesBack { get; set; } public bool BackspaceGoesBack { get; set; }
public bool SingleClickActivates { get; set; } public bool SingleClickActivates { get; set; }
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
public WindowPosition? LastWindowPosition { get; set; } public WindowPosition? LastWindowPosition { get; set; }
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
// END SETTINGS // END SETTINGS
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
{ {
// Read the JSON content from the file // Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath); var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel); var migratedAny = false;
try
{
if (JsonNode.Parse(jsonContent) is JsonObject root)
{
migratedAny |= ApplyMigrations(root, loaded);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Migration check failed: {ex}");
}
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse"); Debug.WriteLine("Loaded settings file");
return loaded ?? new(); if (migratedAny)
{
SaveSettings(loaded);
}
return loaded;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
return new(); return new();
} }
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
{
var migrated = false;
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
migrated |= TryMigrate(
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
root,
model,
nameof(AutoGoHomeInterval),
DeprecatedHotkeyGoesHomeKey,
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
JsonSerializationContext.Default.Boolean);
return migrated;
}
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
{
try
{
// If new key already present, skip migration
if (root.ContainsKey(newKey) && root[newKey] is not null)
{
return false;
}
// If old key present, try to deserialize and apply
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
{
var value = oldNode.Deserialize<T>(jsonTypeInfo);
apply(model, value!);
return true;
}
}
catch (Exception ex)
{
Logger.LogError($"Error during migration {migrationName}.", ex);
}
return false;
}
public static void SaveSettings(SettingsModel model) public static void SaveSettings(SettingsModel model)
{ {
if (string.IsNullOrEmpty(FilePath)) if (string.IsNullOrEmpty(FilePath))
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value?.DeepClone(); savedSettings[item.Key] = item.Value?.DeepClone();
} }
// Remove deprecated keys
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options); var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized); File.WriteAllText(FilePath, serialized);

View File

@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsViewModel : INotifyPropertyChanged public partial class SettingsViewModel : INotifyPropertyChanged
{ {
private static readonly List<TimeSpan> AutoGoHomeIntervals =
[
Timeout.InfiniteTimeSpan,
TimeSpan.Zero,
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(20),
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(60),
TimeSpan.FromSeconds(90),
TimeSpan.FromSeconds(120),
TimeSpan.FromSeconds(180),
];
private readonly SettingsModel _settings; private readonly SettingsModel _settings;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
@@ -58,16 +71,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
} }
} }
public bool HotkeyGoesHome
{
get => _settings.HotkeyGoesHome;
set
{
_settings.HotkeyGoesHome = value;
Save();
}
}
public bool BackspaceGoesBack public bool BackspaceGoesBack
{ {
get => _settings.BackspaceGoesBack; get => _settings.BackspaceGoesBack;
@@ -138,6 +141,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
} }
} }
public int AutoGoBackIntervalIndex
{
get
{
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
return index >= 0 ? index : 0;
}
set
{
if (value >= 0 && value < AutoGoHomeIntervals.Count)
{
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
}
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = []; public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; } public SettingsExtensionsViewModel Extensions { get; }

View File

@@ -58,6 +58,7 @@ public sealed partial class MainWindow : WindowEx,
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")] [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART; private readonly uint WM_TASKBAR_RESTART;
private readonly HWND _hwnd; private readonly HWND _hwnd;
private readonly DispatcherTimer _autoGoHomeTimer;
private readonly WNDPROC? _hotkeyWndProc; private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc; private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = []; private readonly List<TopLevelHotkey> _hotkeys = [];
@@ -68,6 +69,7 @@ public sealed partial class MainWindow : WindowEx,
private DesktopAcrylicController? _acrylicController; private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource; private SystemBackdropConfiguration? _configurationSource;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
private WindowPosition _currentWindowPosition = new(); private WindowPosition _currentWindowPosition = new();
@@ -75,6 +77,9 @@ public sealed partial class MainWindow : WindowEx,
{ {
InitializeComponent(); InitializeComponent();
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe unsafe
@@ -141,6 +146,15 @@ public sealed partial class MainWindow : WindowEx,
HideWindow(); HideWindow();
} }
private void OnAutoGoHomeTimerOnTick(object? s, object e)
{
_autoGoHomeTimer.Stop();
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
// and prevent the user from opening its context menu.
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{ {
if (e.Key == VirtualKey.GoBack) if (e.Key == VirtualKey.GoBack)
@@ -220,6 +234,9 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon); App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen; _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
_autoGoHomeInterval = settings.AutoGoHomeInterval;
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
} }
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
@@ -279,6 +296,8 @@ public sealed partial class MainWindow : WindowEx,
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{ {
StopAutoGoHome();
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd); var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Remember, IsIconic == "minimized", which is entirely different state // Remember, IsIconic == "minimized", which is entirely different state
@@ -533,6 +552,25 @@ public sealed partial class MainWindow : WindowEx,
// If the window was not cloaked, then leave it hidden. // If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible. // Sure, it's not ideal, but at least it's not visible.
} }
// Start auto-go-home timer
RestartAutoGoHome();
}
private void StopAutoGoHome()
{
_autoGoHomeTimer.Stop();
}
private void RestartAutoGoHome()
{
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
{
return;
}
_autoGoHomeTimer.Stop();
_autoGoHomeTimer.Start();
} }
private bool Cloak() private bool Cloak()

View File

@@ -345,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// Depending on the settings, either // Depending on the settings, either
// * Go home, or // * Go home, or
// * Select the search text (if we should remain open on this page) // * Select the search text (if we should remain open on this page)
if (settings.HotkeyGoesHome) if (settings.AutoGoHomeInterval == TimeSpan.Zero)
{ {
GoHome(false); GoHome(false);
} }

View File

@@ -51,8 +51,18 @@
</controls:SettingsCard> </controls:SettingsCard>
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}"> <controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" /> <ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After20Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After30Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After60Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After90Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After120Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After180Seconds" />
</ComboBox>
</controls:SettingsCard> </controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}"> <controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />

View File

@@ -344,12 +344,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve"> <data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value> <value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
</data> </data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
<value>Go home when activated</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically opens the home page upon activation</value>
</data>
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve"> <data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
<value>Highlight search on activate</value> <value>Highlight search on activate</value>
</data> </data>
@@ -523,4 +517,37 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve"> <data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
<value>Command Palette - Fatal error</value> <value>Command Palette - Fatal error</value>
</data> </data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Never.Content" xml:space="preserve">
<value>Never</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Immediately.Content" xml:space="preserve">
<value>Immediately</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After10Seconds.Content" xml:space="preserve">
<value>10 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After20Seconds.Content" xml:space="preserve">
<value>20 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After30Seconds.Content" xml:space="preserve">
<value>30 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After60Seconds.Content" xml:space="preserve">
<value>60 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After90Seconds.Content" xml:space="preserve">
<value>90 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After120Seconds.Content" xml:space="preserve">
<value>2 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After180Seconds.Content" xml:space="preserve">
<value>3 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Header" xml:space="preserve">
<value>Automatically return home</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
</data>
</root> </root>