Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade
Some checks failed
Spell checking / Check Spelling (push) Has been cancelled
Spell checking / Report (Push) (push) Has been cancelled
Spell checking / Report (PR) (push) Has been cancelled
Spell checking / Update PR (push) Has been cancelled

This commit is contained in:
Jeremy Sinclair 2025-08-19 19:40:45 -04:00
commit c92eb60655
25 changed files with 394 additions and 84 deletions

View File

@ -637,6 +637,7 @@ hmodule
hmonitor
homies
homljgmgpmcbpjbnjpfijnhipfkiclkd
HOOKPROC
HORZRES
HORZSIZE
Hostbackdropbrush

View File

@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
## Rules
- **Follow the pattern of what you already see in the code.**
- [Coding style](development/style.md).
- [Coding style](style.md).
- Try to package new functionality/components into libraries that have nicely defined interfaces.
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.

View File

@ -298,5 +298,34 @@ namespace Hosts.Tests
var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden);
Assert.IsTrue(hidden);
}
[TestMethod]
public async Task NoLeadingSpaces_Disabled_RemovesIndent()
{
var content =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
";
var expected =
@"10.1.1.1 host host.local # comment
10.1.1.2 host2 host2.local # another comment
# 10.1.1.30 host30 host30.local # new entry
";
var fs = new CustomMockFileSystem();
var settings = new Mock<IUserSettings>();
settings.Setup(s => s.NoLeadingSpaces).Returns(true);
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object);
fs.AddFile(svc.HostsFilePath, new MockFileData(content));
var data = await svc.ReadAsync();
var entries = data.Entries.ToList();
entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
await svc.WriteAsync(data.AdditionalLines, entries);
var result = fs.GetFile(svc.HostsFilePath);
Assert.AreEqual(expected, result.TextContents);
}
}
}

View File

@ -26,6 +26,8 @@ namespace Hosts.Settings
private bool _loopbackDuplicates;
public bool NoLeadingSpaces { get; private set; }
public bool LoopbackDuplicates
{
get => _loopbackDuplicates;
@ -88,6 +90,7 @@ namespace Hosts.Settings
AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition;
Encoding = (HostsEncoding)settings.Properties.Encoding;
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
}
retry = false;

View File

@ -157,7 +157,7 @@ namespace HostsUILib.Helpers
{
lineBuilder.Append('#').Append(' ');
}
else if (anyDisabled)
else if (anyDisabled && !_userSettings.NoLeadingSpaces)
{
lineBuilder.Append(' ').Append(' ');
}

View File

@ -19,5 +19,7 @@ namespace HostsUILib.Settings
event EventHandler LoopbackDuplicatesChanged;
public delegate void OpenSettingsFunction();
public bool NoLeadingSpaces { get; }
}
}

View File

@ -63,6 +63,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
private Task? _initializeItemsTask;
private CancellationTokenSource? _cancellationTokenSource;
private CancellationTokenSource? _fetchItemsCancellationTokenSource;
private ListItemViewModel? _lastSelectedItem;
@ -129,22 +130,38 @@ public partial class ListViewModel : PageViewModel, IDisposable
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems()
{
// Cancel any previous FetchItems operation
_fetchItemsCancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Dispose();
_fetchItemsCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _fetchItemsCancellationTokenSource.Token;
// TEMPORARY: just plop all the items into a single group
// see 9806fe5d8 for the last commit that had this with sections
_isFetching = true;
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
try
{
// Check for cancellation before starting expensive operations
cancellationToken.ThrowIfCancellationRequested();
var newItems = _model.Unsafe!.GetItems();
// Collect all the items into new viewmodels
Collection<ListItemViewModel> newViewModels = [];
// Check for cancellation after getting items from extension
cancellationToken.ThrowIfCancellationRequested();
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
foreach (var item in newItems)
{
// Check for cancellation during item processing
cancellationToken.ThrowIfCancellationRequested();
ListItemViewModel viewModel = new(item, new(this));
// If an item fails to load, silently ignore it.
@ -154,25 +171,57 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
}
// Check for cancellation before initializing first twenty items
cancellationToken.ThrowIfCancellationRequested();
var firstTwenty = newViewModels.Take(20);
foreach (var item in firstTwenty)
{
cancellationToken.ThrowIfCancellationRequested();
item?.SafeInitializeProperties();
}
// Cancel any ongoing search
_cancellationTokenSource?.Cancel();
// Check for cancellation before updating the list
cancellationToken.ThrowIfCancellationRequested();
List<ListItemViewModel> removedItems = [];
lock (_listLock)
{
// Now that we have new ViewModels for everything from the
// extension, smartly update our list of VMs
ListHelpers.InPlaceUpdateList(Items, newViewModels);
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
// you'll clean up list items that we've now transferred into
// .Items
}
// If we removed items, we need to clean them up, to remove our event handlers
foreach (var removedItem in removedItems)
{
removedItem.SafeCleanup();
}
// TODO: Iterate over everything in Items, and prune items from the
// cache if we don't need them anymore
}
catch (OperationCanceledException)
{
// Cancellation is expected, don't treat as error
// However, if we were cancelled, we didn't actually add these items to
// our Items list. Before we release them to the GC, make sure we clean
// them up
foreach (var vm in newViewModels)
{
vm.SafeCleanup();
}
return;
}
catch (Exception ex)
{
// TODO: Move this within the for loop, so we can catch issues with individual items
@ -560,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
_fetchItemsCancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Dispose();
_fetchItemsCancellationTokenSource = null;
}
protected override void UnsafeCleanup()
@ -570,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(null), PageContext); // necessary?
_cancellationTokenSource?.Cancel();
_fetchItemsCancellationTokenSource?.Cancel();
lock (_listLock)
{

View File

@ -47,7 +47,8 @@
<Flyout
x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened">
Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False">
<cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout>

View File

@ -0,0 +1,157 @@
// 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 ManagedCommon;
using Windows.System;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// A class that listens for local keyboard events using a Windows hook.
/// </summary>
internal sealed class LocalKeyboardListener : IDisposable
{
/// <summary>
/// Event that is raised when a key is pressed down.
/// </summary>
public event EventHandler<LocalKeyboardListenerKeyPressedEventArgs>? KeyPressed;
private bool _disposed;
private UnhookWindowsHookExSafeHandle? _handle;
private HOOKPROC? _hookProc; // Keep reference to prevent GC collection
/// <summary>
/// Registers a global keyboard hook to listen for key down events.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Throws if the hook could not be registered, which may happen if the system is unable to set the hook.
/// </exception>
public void RegisterKeyboardHook()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_handle is not null && !_handle.IsInvalid)
{
// Hook is already set
return;
}
_hookProc = KeyEventHook;
if (!SetWindowKeyHook(_hookProc))
{
throw new InvalidOperationException("Failed to register keyboard hook.");
}
}
/// <summary>
/// Attempts to register a global keyboard hook to listen for key down events.
/// </summary>
/// <returns>
/// <see langword="true"/> if the keyboard hook was successfully registered; otherwise, <see langword="false"/>.
/// </returns>
public bool Start()
{
if (_disposed)
{
return false;
}
try
{
RegisterKeyboardHook();
return true;
}
catch (Exception ex)
{
Logger.LogError("Failed to register hook", ex);
return false;
}
}
private void UnregisterKeyboardHook()
{
if (_handle is not null && !_handle.IsInvalid)
{
// The SafeHandle should automatically call UnhookWindowsHookEx when disposed
_handle.Dispose();
_handle = null;
}
_hookProc = null;
}
private bool SetWindowKeyHook(HOOKPROC hookProc)
{
if (_handle is not null && !_handle.IsInvalid)
{
// Hook is already set
return false;
}
_handle = PInvoke.SetWindowsHookEx(
WINDOWS_HOOK_ID.WH_KEYBOARD,
hookProc,
PInvoke.GetModuleHandle(null),
PInvoke.GetCurrentThreadId());
// Check if the hook was successfully set
return _handle is not null && !_handle.IsInvalid;
}
private static bool IsKeyDownHook(LPARAM lParam)
{
// The 30th bit tells what the previous key state is with 0 being the "UP" state
// For more info see https://learn.microsoft.com/windows/win32/winmsg/keyboardproc#lparam-in
return ((lParam.Value >> 30) & 1) == 0;
}
private LRESULT KeyEventHook(int nCode, WPARAM wParam, LPARAM lParam)
{
try
{
if (nCode >= 0 && IsKeyDownHook(lParam))
{
InvokeKeyDown((VirtualKey)wParam.Value);
}
}
catch (Exception ex)
{
Logger.LogError("Failed when invoking key down keyboard hook event", ex);
}
// Call next hook in chain - pass null as first parameter for current hook
return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
}
private void InvokeKeyDown(VirtualKey virtualKey)
{
if (!_disposed)
{
KeyPressed?.Invoke(this, new LocalKeyboardListenerKeyPressedEventArgs(virtualKey));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
UnregisterKeyboardHook();
}
_disposed = true;
}
}
}

View File

@ -0,0 +1,12 @@
// 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 Windows.System;
namespace Microsoft.CmdPal.UI.Helpers;
public class LocalKeyboardListenerKeyPressedEventArgs(VirtualKey key) : EventArgs
{
public VirtualKey Key { get; } = key;
}

View File

@ -27,6 +27,7 @@ using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
@ -44,7 +45,8 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>
IRecipient<QuitMessage>,
IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", 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_")]
@ -54,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
private readonly KeyboardListener _keyboardListener;
private readonly LocalKeyboardListener _localKeyboardListener;
private bool _ignoreHotKeyWhenFullScreen = true;
private DesktopAcrylicController? _acrylicController;
@ -116,6 +119,18 @@ public sealed partial class MainWindow : WindowEx,
{
Summon(string.Empty);
});
_localKeyboardListener = new LocalKeyboardListener();
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
_localKeyboardListener.Start();
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
{
WeakReferenceMessenger.Default.Send(new GoBackMessage());
}
}
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
@ -376,6 +391,7 @@ public sealed partial class MainWindow : WindowEx,
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
_localKeyboardListener.Dispose();
DisposeAcrylic();
_keyboardListener.Stop();
@ -682,4 +698,10 @@ public sealed partial class MainWindow : WindowEx,
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
_localKeyboardListener.Dispose();
DisposeAcrylic();
}
}

View File

@ -47,4 +47,10 @@ DWM_CLOAKED_APP
CoWaitForMultipleObjects
INFINITE
CWMO_FLAGS
CWMO_FLAGS
GetCurrentThreadId
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
GetModuleHandle

View File

@ -8,7 +8,6 @@ using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
using Microsoft.CmdPal.Ext.WindowsTerminal.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml.Media.Imaging;
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages;
@ -16,7 +15,6 @@ internal sealed partial class ProfilesListPage : ListPage
{
private readonly TerminalQuery _terminalQuery = new();
private readonly SettingsManager _terminalSettings;
private readonly Dictionary<string, BitmapImage> _logoCache = [];
private bool showHiddenProfiles;
private bool openNewTab;
@ -54,14 +52,6 @@ internal sealed partial class ProfilesListPage : ListPage
MoreCommands = [
new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)),
],
// Icon = () => GetLogo(profile.Terminal),
// Action = _ =>
// {
// Launch(profile.Terminal.AppUserModelId, profile.Name);
// return true;
// },
// ContextData = profile,
#pragma warning restore SA1108
});
}
@ -70,17 +60,4 @@ internal sealed partial class ProfilesListPage : ListPage
}
public override IListItem[] GetItems() => Query().ToArray();
private BitmapImage GetLogo(TerminalPackage terminal)
{
var aumid = terminal.AppUserModelId;
if (!_logoCache.TryGetValue(aumid, out var value))
{
value = terminal.GetLogo();
_logoCache.Add(aumid, value);
}
return value;
}
}

View File

@ -5,7 +5,6 @@
using System;
using System.IO;
using ManagedCommon;
using Microsoft.UI.Xaml.Media.Imaging;
// using Wox.Infrastructure.Image;
namespace Microsoft.CmdPal.Ext.WindowsTerminal;
@ -30,23 +29,4 @@ public class TerminalPackage
SettingsPath = settingsPath;
LogoPath = logoPath;
}
public BitmapImage GetLogo()
{
var image = new BitmapImage();
if (File.Exists(LogoPath))
{
using var fileStream = File.OpenRead(LogoPath);
image.SetSource(fileStream.AsRandomAccessStream());
}
else
{
// Not using wox anymore, TODO: find the right new way to handle this
// image.UriSource = new Uri(ImageLoader.ErrorIconPath);
Logger.LogError($"Logo file not found: {LogoPath}");
}
return image;
}
}

View File

@ -65,12 +65,32 @@ public partial class ListHelpers
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents)
where T : class
{
InPlaceUpdateList(original, newContents, out _);
}
/// <summary>
/// Modifies the contents of `original` in-place, to match those of
/// `newContents`. The canonical use being:
/// ```cs
/// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn));
/// ```
/// </summary>
/// <typeparam name="T">Any type that can be compared for equality</typeparam>
/// <param name="original">Collection to modify</param>
/// <param name="newContents">The enumerable which `original` should match</param>
/// <param name="removedItems">List of items that were removed from the original collection</param>
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents, out List<T> removedItems)
where T : class
{
removedItems = [];
// we're not changing newContents - stash this so we don't re-evaluate it every time
var numberOfNew = newContents.Count();
// Short circuit - new contents should just be empty
if (numberOfNew == 0)
{
removedItems.AddRange(original);
original.Clear();
return;
}
@ -92,6 +112,7 @@ public partial class ListHelpers
for (var k = i; k < j; k++)
{
// This item from the original list was not in the new list. Remove it.
removedItems.Add(original[i]);
original.RemoveAt(i);
}
@ -120,6 +141,7 @@ public partial class ListHelpers
while (original.Count > numberOfNew)
{
// RemoveAtEnd
removedItems.Add(original[original.Count - 1]);
original.RemoveAt(original.Count - 1);
}

View File

@ -41,8 +41,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Web.WebView2" />
<PackageReference Include="System.Drawing.Common" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
</ItemGroup>

View File

@ -180,4 +180,4 @@
<Error Condition="!Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(WebView2Nuget)\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
</Project>
</Project>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4188" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.7.250513003" targetFramework="native" />
</packages>

View File

@ -24,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public HostsEncoding Encoding { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool NoLeadingSpaces { get; set; }
public HostsProperties()
{
ShowStartupWarning = true;
@ -31,6 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
LoopbackDuplicates = false;
AdditionalLinesPosition = HostsAdditionalLinesPosition.Top;
Encoding = HostsEncoding.Utf8;
NoLeadingSpaces = false;
}
}
}

View File

@ -320,7 +320,6 @@ namespace Microsoft.PowerToys.Settings.UI
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
settingsWindow.Activate();
settingsWindow.NavigateToSection(StartupPage);
ShowMessageDialog("The application is running in Debug mode.", "DEBUG");
#else
/* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
@ -329,31 +328,6 @@ namespace Microsoft.PowerToys.Settings.UI
}
}
#if !DEBUG
private async void ShowMessageDialogAndExit(string content, string title = null)
#else
private async void ShowMessageDialog(string content, string title = null)
#endif
{
await ShowDialogAsync(content, title);
#if !DEBUG
this.Exit();
#endif
}
public static Task<IUICommand> ShowDialogAsync(string content, string title = null)
{
var dialog = new MessageDialog(content, title ?? string.Empty);
var handle = NativeMethods.GetActiveWindow();
if (handle == IntPtr.Zero)
{
throw new InvalidOperationException();
}
InitializeWithWindow.Initialize(dialog, handle);
return dialog.ShowAsync().AsTask<IUICommand>();
}
public static TwoWayPipeMessageIPCManaged GetTwoWayIPCManager()
{
return ipcmanager;

View File

@ -49,6 +49,40 @@
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="OfficeKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<!-- Scale to visually align the height of the Office logo and text -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<Viewbox>
<PathIcon Data="M1792 405v1238q0 33-10 62t-28 54-44 41-57 27l-555 159q-23 6-47 6-31 0-58-8t-53-24l-363-205q-20-11-31-29t-12-42q0-35 24-59t60-25h470V458L735 584q-43 15-69 53t-26 83v651q0 41-20 73t-55 53l-167 91q-23 12-46 12-40 0-68-28t-28-68V587q0-51 26-96t71-71L949 81q41-23 89-23 17 0 30 2t30 8l555 153q31 9 56 27t44 42 29 54 10 61zm-128 1238V405q0-22-13-38t-34-23l-273-75-64-18-64-18v1586l401-115q21-6 34-22t13-39z" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="CopilotKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"
TargetType="local:KeyCharPresenter">
<!-- Scale to visually align the height of the Copilot logo and text -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyCharPresenter">
<Grid Height="{TemplateBinding FontSize}">
<Viewbox>
<PathIcon Data="M0 1213q0-60 10-124t27-130 35-129 38-121q18-55 41-119t54-129 70-125 87-106 106-74 129-28h661q59 0 114 17t96 64q30 34 46 72t33 81l22 58q11 29 34 52 23 25 56 31t65 9h4q157 0 238 83t82 240q0 60-10 125t-27 130-35 128-38 121q-18 55-41 119t-54 129-70 125-87 106-106 74-129 28H790q-61 0-107-15t-82-44-61-72-46-98q-11-29-24-60t-35-55q-23-25-51-31t-60-9h-4q-157 0-238-83T0 1213zm598-957q-50 0-93 25t-79 68-67 94-54 108-42 106-31 91q-17 51-35 110t-33 119-26 121-10 114q0 102 43 149t147 47h163q39 0 74-12t64-35 50-53 34-67q19-58 35-115t35-117q35-117 70-232t72-233q23-73 47-147t63-141H598zm452 285q69-29 143-29h281q-18-29-29-59t-21-58-21-54-30-44-46-30-69-11q-32 0-60 9t-48 35q-17 23-31 53t-27 63-23 65-19 60zm-296 867h101q39 0 74-12t66-34 52-52 33-68l58-191 42-140q21-70 43-140 11-36 28-69t43-62h-101q-39 0-74 12t-66 34-52 52-33 68q-15 48-29 96t-29 96q-21 70-41 140t-44 140q-11 36-28 68t-43 62zm814-768q-39 0-74 12t-64 35-50 53-34 68q-56 174-107 347t-106 349q-23 74-47 147t-63 141h427q50 0 93-25t79-68 67-94 54-108 42-106 31-91q16-51 34-110t34-119 26-121 10-114q0-102-43-149t-147-47h-162zm-570 867q-69 29-143 29H564q17 28 29 58t22 58 24 54 32 45 48 30 71 11q31 0 60-8t49-35q15-19 29-50t28-65 24-69 18-58z" />
</Viewbox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="GlyphKeyCharPresenterStyle"
BasedOn="{StaticResource DefaultKeyCharPresenterStyle}"

View File

@ -95,9 +95,23 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
return;
}
if (Content is string)
if (Content is string key)
{
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
switch (key)
{
case "Copilot":
_keyPresenter.Style = (Style)Application.Current.Resources["CopilotKeyCharPresenterStyle"];
break;
case "Office":
_keyPresenter.Style = (Style)Application.Current.Resources["OfficeKeyCharPresenterStyle"];
break;
default:
_keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"];
break;
}
return;
}

View File

@ -56,6 +56,9 @@
<tkcontrols:SettingsCard x:Uid="Hosts_Toggle_LoopbackDuplicates" HeaderIcon="{ui:FontIcon Glyph=&#xEC27;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.LoopbackDuplicates, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="Hosts_NoLeadingSpaces" HeaderIcon="{ui:FontIcon Glyph=&#xE8A5;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.NoLeadingSpaces, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="Hosts_Encoding">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.Encoding, Mode=TwoWay}">
<ComboBoxItem x:Uid="Hosts_Encoding_Utf8" />

View File

@ -5127,4 +5127,10 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="KeyBack" xml:space="preserve">
<value>Back key</value>
</data>
<data name="Hosts_NoLeadingSpaces.Header" xml:space="preserve">
<value>No leading spaces</value>
</data>
<data name="Hosts_NoLeadingSpaces.Description" xml:space="preserve">
<value>Do not prepend spaces to active lines when saving the hosts file</value>
</data>
</root>

View File

@ -105,6 +105,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool NoLeadingSpaces
{
get => Settings.Properties.NoLeadingSpaces;
set
{
if (value != Settings.Properties.NoLeadingSpaces)
{
Settings.Properties.NoLeadingSpaces = value;
NotifyPropertyChanged();
}
}
}
public int AdditionalLinesPosition
{
get => (int)Settings.Properties.AdditionalLinesPosition;