Enable "Space" only to activate Peek (#41867)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Closes: #26143

This pull request introduces a new "single Space key activation" mode
for the Peek PowerToy, allowing users to open Peek with just the Space
key when File Explorer or the Desktop is focused. The implementation
includes settings UI changes, backend logic to enforce and manage this
mode, eligibility checks for activation, and telemetry. It also enhances
the user experience by disabling the activation shortcut control when
space mode is enabled and providing appropriate tooltips and
localization.

**Key changes:**

### Feature: Single Space Key Activation Mode

* Added a new setting (`EnableSpaceToActivate`) to allow users to enable
Peek activation using only the Space key, restricted to File Explorer or
Desktop focus. When enabled, the activation shortcut is forced to bare
Space and the previous shortcut is stashed (not restored on toggle-off
for simplicity). (`src/modules/peek/peek/dllmain.cpp`,
`src/settings-ui/Settings.UI.Library/PeekProperties.cs`,
`src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs`,
`src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`,
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`)
[[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R132-R169)
[[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R79-R80)
[[3]](diffhunk://#diff-d482fce7c2d0abbe2b307351ef7588378ddf34d47b31ebf71411f264dcce07faR22)
[[4]](diffhunk://#diff-d482fce7c2d0abbe2b307351ef7588378ddf34d47b31ebf71411f264dcce07faR33-R35)
[[5]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R228-R257)
[[6]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51)
[[7]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165)

<img width="1018" height="197" alt="image"
src="https://github.com/user-attachments/assets/6f9eec4a-2583-41e5-92e9-9dfbc186728a"
/>

* UI will hide the activation shortcut control. Attempts to change the
shortcut programmatically are ignored while in this mode.
(`src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`,
`src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs`,
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`)
[[1]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51)
[[2]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R173-R178)
[[3]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R228-R257)
[[4]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165)

<img width="1014" height="116" alt="image"
src="https://github.com/user-attachments/assets/d1513101-a859-4b06-9252-2e707bce6689"
/>

### Activation Logic & Eligibility

* Implemented a foreground window hook and debounce logic to determine
if Peek can be activated by Space (only when File Explorer, Desktop, or
Peek itself is focused). This minimizes CPU overhead when user
repeatedly presses Space but not for Peek .
(`src/modules/peek/peek/dllmain.cpp`)
[[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R50-R60)
[[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R188-R292)
[[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039L457-R637)

* Managed hook installation and cleanup based on Peek's enabled state
and the space mode toggle. (`src/modules/peek/peek/dllmain.cpp`)
[[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R188-R292)
[[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R562)
[[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R593)
[[4]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R496)

### Settings & Telemetry

* Added the new toggle to the settings serialization and XAML UI, with
localization and descriptions. (`src/modules/peek/peek/dllmain.cpp`,
`src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`,
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`)
[[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R530)
[[2]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51)
[[3]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165)

* Added telemetry event for enabling/disabling space mode.
(`src/modules/peek/peek/trace.cpp`, `src/modules/peek/peek/trace.h`)
[[1]](diffhunk://#diff-db76a3e6fa1cc19889492b72d0c063835bdc8f67909cb9d91c9e7e47e248a87aR51-R60)
[[2]](diffhunk://#diff-8f824b0a7dd76f7fcd4a15b7885233b5b3212403a56c4efd67b83c4c2d02e486R18-R20)

### Code Quality

* Refactored includes and initialization logic for clarity and
maintainability. (`src/modules/peek/peek/dllmain.cpp`)
[[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039L2-R14)
[[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R37-R39)
[[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R483)

These changes collectively provide a safer, more accessible, and
user-friendly way to activate Peek with a single key, while ensuring
users are clearly informed and accidental activations are minimized.
<!-- Please review the items on the PR checklist before submitting-->

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Gordon Lam 2025-09-25 16:28:55 -07:00 committed by GitHub
parent d07f40eec3
commit 08a3ae2dee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 336 additions and 26 deletions

View File

@ -19,7 +19,9 @@ namespace ManagedCommon
private static readonly string Error = "Error";
private static readonly string Warning = "Warning";
private static readonly string Info = "Info";
#if DEBUG
private static readonly string Debug = "Debug";
#endif
private static readonly string TraceFlag = "Trace";
private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
@ -151,7 +153,9 @@ namespace ManagedCommon
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
#if DEBUG
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
#endif
}
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)

View File

@ -115,19 +115,55 @@ namespace Peek.UI.Helpers
}
/// <summary>
/// Returns whether the caret is visible in the specified window.
/// Heuristic to decide whether the user is actively typing so we should suppress Peek activation.
/// Current logic:
/// - If the focused control class name contains "Edit" or "Input" (e.g. Explorer search box or in-place rename), return true.
/// - Otherwise fall back to the legacy GUI_CARETBLINKING flag (covers other text contexts where class name differs but caret blinks).
/// - If we fail to retrieve GUI thread info, we default to false (do not suppress) to avoid blocking activation due to transient failures.
/// NOTE: This intentionally no longer walks ancestor chains; any Edit/Input focus inside the same top-level Explorer/Desktop window is treated as typing.
/// </summary>
private static bool CaretVisible(HWND hwnd)
private static unsafe bool CaretVisible(HWND hwnd)
{
GUITHREADINFO guiThreadInfo = new() { cbSize = (uint)Marshal.SizeOf<GUITHREADINFO>() };
// Get information for the foreground thread
if (PInvoke_PeekUI.GetGUIThreadInfo(0, ref guiThreadInfo))
GUITHREADINFO gi = new() { cbSize = (uint)Marshal.SizeOf<GUITHREADINFO>() };
if (!PInvoke_PeekUI.GetGUIThreadInfo(0, ref gi))
{
return guiThreadInfo.hwndActive == hwnd && (guiThreadInfo.flags & GUITHREADINFO_FLAGS.GUI_CARETBLINKING) != 0;
return false; // fail open (allow activation)
}
return false;
// Quick sanity: restrict to same top-level window (match prior behavior)
if (gi.hwndActive != hwnd)
{
return false;
}
HWND focus = gi.hwndFocus;
if (focus == HWND.Null)
{
return false;
}
// Get focused window class (96 chars buffer; GetClassNameW bounds writes). Treat any class containing
// "Edit" or "Input" as a text field (search / titlebar) and suppress Peek.
Span<char> buf = stackalloc char[96];
fixed (char* p = buf)
{
int len = PInvoke_PeekUI.GetClassName(focus, p, buf.Length);
if (len > 0)
{
var focusClass = new string(p, 0, len);
if (focusClass.Contains("Edit", StringComparison.OrdinalIgnoreCase) || focusClass.Contains("Input", StringComparison.OrdinalIgnoreCase))
{
return true; // treat any Edit/Input focus as typing.
}
else
{
ManagedCommon.Logger.LogDebug($"Peek suppression: focus class{focusClass}");
}
}
}
// Fallback: original caret blinking heuristic for other text-entry contexts
return (gi.flags & GUITHREADINFO_FLAGS.GUI_CARETBLINKING) != 0;
}
}
}

View File

@ -1,15 +1,17 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include "trace.h"
#include <atlbase.h>
#include <atomic>
#include <comdef.h>
#include <common/interop/shared_constants.h>
#include <common/logger/logger.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include <common/utils/winapi_error.h>
#include <filesystem>
#include <common/interop/shared_constants.h>
#include <atlbase.h>
#include <exdisp.h>
#include <comdef.h>
#include <common/utils/elevation.h>
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <exdisp.h>
#include <filesystem>
#include <interface/powertoy_module_interface.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
@ -32,6 +34,9 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/,
return TRUE;
}
// Forward declare global Peek so anonymous namespace uses same type
class Peek;
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
@ -42,6 +47,17 @@ namespace
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
const wchar_t JSON_KEY_ALWAYS_RUN_NOT_ELEVATED[] = L"AlwaysRunNotElevated";
const wchar_t JSON_KEY_ENABLE_SPACE_TO_ACTIVATE[] = L"EnableSpaceToActivate";
// Space activation (single-space mode) state
std::atomic_bool g_foregroundHookActive{ false }; // Foreground hook installed
std::atomic_bool g_foregroundEligible{ false }; // Cached eligibility (Explorer/Desktop/Peek focused)
HWINEVENTHOOK g_foregroundHook = nullptr; // Foreground change hook handle
constexpr DWORD FOREGROUND_DEBOUNCE_MS = 40; // Delay before eligibility recompute (ms)
HANDLE g_foregroundDebounceTimer = nullptr; // One-shot scheduled timer
std::atomic<DWORD> g_foregroundLastScheduleTick{ 0 }; // Tick count when timer last scheduled
Peek* g_instance = nullptr; // pointer to active instance (global Peek)
}
// The PowerToy name that will be shown in the settings.
@ -60,6 +76,7 @@ private:
// If we should always try to run Peek non-elevated.
bool m_alwaysRunNotElevated = true;
bool m_enableSpaceToActivate = false; // toggle from settings
HANDLE m_hProcess = 0;
DWORD m_processPid = 0;
@ -111,11 +128,55 @@ private:
m_alwaysRunNotElevated = true;
}
try
{
auto jsonEnableSpaceObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ENABLE_SPACE_TO_ACTIVATE);
m_enableSpaceToActivate = jsonEnableSpaceObject.GetNamedBoolean(L"value");
}
catch (...)
{
m_enableSpaceToActivate = false;
}
// Enforce design: if space toggle ON, force single-space hotkey and store previous combination once.
if (m_enableSpaceToActivate)
{
if (!(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' ')
{
// already single space
}
else
{
m_hotkey.win = false;
m_hotkey.alt = false;
m_hotkey.shift = false;
m_hotkey.ctrl = false;
m_hotkey.key = ' ';
}
}
else
{
// If toggle off and current hotkey is bare space, revert to default (simplified policy)
if (!(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' ')
{
set_default_key_settings();
}
}
manage_space_mode_hook();
Trace::SpaceModeEnabled(m_enableSpaceToActivate);
}
else
{
Logger::info("Peek settings are empty");
set_default_key_settings();
// First-run (no existing settings file or empty JSON): default to Space-only activation
Logger::info("Peek settings are empty - initializing first-run defaults (Space activation)");
m_enableSpaceToActivate = true;
m_hotkey.win = false;
m_hotkey.alt = false;
m_hotkey.shift = false;
m_hotkey.ctrl = false;
m_hotkey.key = ' ';
Trace::SpaceModeEnabled(true);
}
}
@ -129,6 +190,111 @@ private:
m_hotkey.key = ' ';
}
// Eligibility recompute (debounced via timer)
public: // callable from anonymous namespace helper
void recompute_space_mode_eligibility()
{
if (!m_enableSpaceToActivate)
{
g_foregroundEligible.store(false, std::memory_order_relaxed);
return;
}
const bool eligible = is_peek_or_explorer_or_desktop_window_focused();
g_foregroundEligible.store(eligible, std::memory_order_relaxed);
Logger::debug(L"Peek space-mode eligibility recomputed: {}", eligible);
}
private:
static void CALLBACK ForegroundDebounceTimerProc(PVOID /*param*/, BOOLEAN /*fired*/)
{
if (!g_instance || !g_foregroundHookActive.load(std::memory_order_relaxed))
{
return;
}
g_instance->recompute_space_mode_eligibility();
}
static void CALLBACK ForegroundWinEventProc(HWINEVENTHOOK /*hook*/, DWORD /*event*/, HWND /*hwnd*/, LONG /*idObject*/, LONG /*idChild*/, DWORD /*thread*/, DWORD /*time*/)
{
if (!g_foregroundHookActive.load(std::memory_order_relaxed) || !g_instance)
{
return;
}
const DWORD now = GetTickCount();
const DWORD last = g_foregroundLastScheduleTick.load(std::memory_order_relaxed);
// If no timer or sufficient time since last schedule, create a new one.
if (!g_foregroundDebounceTimer || (now - last) >= FOREGROUND_DEBOUNCE_MS || now < last)
{
if (g_foregroundDebounceTimer)
{
// Best effort: cancel previous pending timer; ignore failure.
DeleteTimerQueueTimer(nullptr, g_foregroundDebounceTimer, INVALID_HANDLE_VALUE);
g_foregroundDebounceTimer = nullptr;
}
if (CreateTimerQueueTimer(&g_foregroundDebounceTimer, nullptr, ForegroundDebounceTimerProc, nullptr, FOREGROUND_DEBOUNCE_MS, 0, WT_EXECUTEDEFAULT))
{
g_foregroundLastScheduleTick.store(now, std::memory_order_relaxed);
}
else
{
Logger::warn(L"Peek failed to create foreground debounce timer");
// Fallback: compute immediately if timer creation failed.
g_instance->recompute_space_mode_eligibility();
}
}
}
void install_foreground_hook()
{
if (g_foregroundHook || !m_enableSpaceToActivate)
{
return;
}
g_instance = this;
g_foregroundHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, nullptr, ForegroundWinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (g_foregroundHook)
{
g_foregroundHookActive.store(true, std::memory_order_relaxed);
recompute_space_mode_eligibility();
}
else
{
g_foregroundHookActive.store(false, std::memory_order_relaxed);
Logger::warn(L"Peek failed to install foreground hook. Falling back to polling.");
}
}
void uninstall_foreground_hook()
{
if (g_foregroundHook)
{
UnhookWinEvent(g_foregroundHook);
g_foregroundHook = nullptr;
}
if (g_foregroundDebounceTimer)
{
DeleteTimerQueueTimer(nullptr, g_foregroundDebounceTimer, INVALID_HANDLE_VALUE);
g_foregroundDebounceTimer = nullptr;
}
g_foregroundLastScheduleTick.store(0, std::memory_order_relaxed);
g_foregroundHookActive.store(false, std::memory_order_relaxed);
g_foregroundEligible.store(false, std::memory_order_relaxed);
g_instance = nullptr;
}
void manage_space_mode_hook()
{
if (m_enableSpaceToActivate && m_enabled)
{
install_foreground_hook();
}
else
{
uninstall_foreground_hook();
}
}
void parse_hotkey(winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
{
try
@ -319,6 +485,7 @@ private:
public:
Peek()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "Peek");
init_settings();
m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_PEEK_SHARED_EVENT);
@ -331,6 +498,7 @@ public:
{
}
m_enabled = false;
uninstall_foreground_hook();
};
// Destroy the powertoy and free memory
@ -364,6 +532,7 @@ public:
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
settings.add_bool_toggle(JSON_KEY_ENABLE_SPACE_TO_ACTIVATE, L"Enable single Space key activation", m_enableSpaceToActivate);
return settings.serialize_to_buffer(buffer, buffer_size);
}
@ -395,6 +564,7 @@ public:
launch_process();
m_enabled = true;
Trace::EnablePeek(true);
manage_space_mode_hook();
}
// Disable the powertoy
@ -425,6 +595,7 @@ public:
m_enabled = false;
Trace::EnablePeek(false);
uninstall_foreground_hook();
}
// Returns if the powertoys is enabled
@ -454,11 +625,21 @@ public:
{
if (m_enabled)
{
Logger::trace(L"Peek hotkey pressed");
// Only activate and consume the shortcut if a Peek, explorer or desktop window is the foreground application.
if (is_peek_or_explorer_or_desktop_window_focused())
bool spaceMode = m_enableSpaceToActivate && !(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' ';
bool eligible = false;
if (spaceMode && g_foregroundHookActive.load(std::memory_order_relaxed))
{
eligible = g_foregroundEligible.load(std::memory_order_relaxed);
}
else
{
eligible = is_peek_or_explorer_or_desktop_window_focused();
}
if (eligible)
{
Logger::trace(L"Peek hotkey pressed and eligible for launching");
// TODO: fix VK_SPACE DestroyWindow in viewer app
if (!is_viewer_running())
{
@ -468,7 +649,16 @@ public:
SetEvent(m_hInvokeEvent);
Trace::PeekInvoked();
return true;
if (spaceMode)
{
return false;
}
else
{
return true;
}
}
}

View File

@ -48,3 +48,13 @@ void Trace::SettingsTelemetry(PowertoyModuleIface::Hotkey& hotkey) noexcept
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingWideString(hotKeyStr.c_str(), "HotKey"));
}
void Trace::SpaceModeEnabled(bool enabled) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"Peek_SpaceModeEnabled",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}

View File

@ -15,4 +15,7 @@ public:
// Event to send settings telemetry.
static void Trace::SettingsTelemetry(PowertoyModuleIface::Hotkey& hotkey) noexcept;
// Space mode telemetry (single-key activation toggle)
static void SpaceModeEnabled(bool enabled) noexcept;
};

View File

@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
AlwaysRunNotElevated = new BoolProperty(true);
CloseAfterLosingFocus = new BoolProperty(false);
ConfirmFileDelete = new BoolProperty(true);
EnableSpaceToActivate = new BoolProperty(true); // Toggle is ON by default for new users. No impact on existing users.
}
public HotkeySettings ActivationShortcut { get; set; }
@ -29,6 +30,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public BoolProperty ConfirmFileDelete { get; set; }
public BoolProperty EnableSpaceToActivate { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
}
}

View File

@ -15,7 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig
{
public const string ModuleName = "Peek";
public const string ModuleVersion = "0.0.1";
public const string InitialModuleVersion = "0.0.1";
public const string SpaceActivationIntroducedVersion = "0.0.2";
public const string CurrentModuleVersion = SpaceActivationIntroducedVersion;
private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
@ -28,7 +30,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public PeekSettings()
{
Name = ModuleName;
Version = ModuleVersion;
Version = CurrentModuleVersion;
Properties = new PeekProperties();
}
@ -54,6 +56,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public bool UpgradeSettingsConfiguration()
{
if (string.IsNullOrEmpty(Version) ||
Version.Equals(InitialModuleVersion, StringComparison.OrdinalIgnoreCase))
{
Version = CurrentModuleVersion;
Properties.EnableSpaceToActivate.Value = false;
return true;
}
return false;
}

View File

@ -22,11 +22,19 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="Peek_Activation_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="Peek_ActivationMethod">
<ComboBox SelectedIndex="{x:Bind ViewModel.EnableSpaceToActivate, Mode=TwoWay, Converter={StaticResource BoolToComboBoxIndexConverter}}">
<ComboBoxItem x:Uid="Peek_ActivationMethod_CustomizedShortcut" />
<ComboBoxItem x:Uid="Peek_ActivationMethod_SpaceBar" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Name="ActivationShortcut"
x:Uid="Activation_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
Visibility="{x:Bind ViewModel.EnableSpaceToActivate, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>

View File

@ -3152,6 +3152,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Peek_ConfirmFileDelete.Description" xml:space="preserve">
<value>You'll be asked to confirm before files are moved to the Recycle Bin</value>
</data>
<data name="Peek_ActivationMethod.Header" xml:space="preserve">
<value>Activation method</value>
</data>
<data name="Peek_ActivationMethod.Description" xml:space="preserve">
<value>Use a shortcut or press the Spacebar when a file is selected</value>
<comment>Spacebar is a physical keyboard key</comment>
</data>
<data name="Peek_ActivationMethod_CustomizedShortcut.Content" xml:space="preserve">
<value>Custom shortcut</value>
</data>
<data name="Peek_ActivationMethod_SpaceBar.Content" xml:space="preserve">
<value>Spacebar</value>
</data>
<data name="FancyZones_DisableRoundCornersOnWindowSnap.Content" xml:space="preserve">
<value>Disable rounded corners when a window is snapped</value>
</data>

View File

@ -170,6 +170,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (_peekSettings.Properties.ActivationShortcut != value)
{
// If space mode toggle is on, ignore external attempts to change (UI will be disabled, but defensive).
if (EnableSpaceToActivate)
{
return;
}
_peekSettings.Properties.ActivationShortcut = value ?? _peekSettings.Properties.DefaultActivationShortcut;
OnPropertyChanged(nameof(ActivationShortcut));
NotifySettingsChanged();
@ -219,6 +225,33 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool EnableSpaceToActivate
{
get => _peekSettings.Properties.EnableSpaceToActivate.Value;
set
{
if (_peekSettings.Properties.EnableSpaceToActivate.Value != value)
{
_peekSettings.Properties.EnableSpaceToActivate.Value = value;
if (value)
{
// Force single space (0x20) without modifiers.
_peekSettings.Properties.ActivationShortcut = new HotkeySettings(false, false, false, false, 0x20);
}
else
{
// Revert to default (design simplification, not restoring previous custom combo).
_peekSettings.Properties.ActivationShortcut = _peekSettings.Properties.DefaultActivationShortcut;
}
OnPropertyChanged(nameof(EnableSpaceToActivate));
OnPropertyChanged(nameof(ActivationShortcut));
NotifySettingsChanged();
}
}
}
public bool SourceCodeWrapText
{
get => _peekPreviewSettings.SourceCodeWrapText.Value;