diff --git a/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml b/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml index 147c7d782a..6d3547dc33 100644 --- a/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml +++ b/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml @@ -1,11 +1,11 @@ + xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"> - - - - - - diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyCharPresenter.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyCharPresenter.xaml.cs deleted file mode 100644 index 0457e715ee..0000000000 --- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyCharPresenter.xaml.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Documents; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; - -namespace KeyboardManagerEditorUI.Controls; - -public sealed partial class KeyCharPresenter : Control -{ - public KeyCharPresenter() - { - DefaultStyleKey = typeof(KeyCharPresenter); - } - - public object Content - { - get => (object)GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyCharPresenter), new PropertyMetadata(default(string))); -} diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml deleted file mode 100644 index e0f04391c7..0000000000 --- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml.cs deleted file mode 100644 index a0ed4b0306..0000000000 --- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/KeyVisual/KeyVisual.xaml.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Windows.System; - -namespace KeyboardManagerEditorUI.Controls -{ - [TemplatePart(Name = KeyPresenter, Type = typeof(KeyCharPresenter))] - [TemplateVisualState(Name = NormalState, GroupName = "CommonStates")] - [TemplateVisualState(Name = DisabledState, GroupName = "CommonStates")] - [TemplateVisualState(Name = InvalidState, GroupName = "CommonStates")] - [TemplateVisualState(Name = WarningState, GroupName = "CommonStates")] - public sealed partial class KeyVisual : Control - { - private const string KeyPresenter = "KeyPresenter"; - private const string NormalState = "Normal"; - private const string DisabledState = "Disabled"; - private const string InvalidState = "Invalid"; - private const string WarningState = "Warning"; - private KeyCharPresenter _keyPresenter; - - public object Content - { - get => (object)GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged)); - - public State State - { - get => (State)GetValue(StateProperty); - set => SetValue(StateProperty, value); - } - - public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(State), typeof(KeyVisual), new PropertyMetadata(State.Normal, OnStateChanged)); - - public bool RenderKeyAsGlyph - { - get => (bool)GetValue(RenderKeyAsGlyphProperty); - set => SetValue(RenderKeyAsGlyphProperty, value); - } - - public static readonly DependencyProperty RenderKeyAsGlyphProperty = DependencyProperty.Register(nameof(RenderKeyAsGlyph), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnContentChanged)); - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - public KeyVisual() -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. - { - this.DefaultStyleKey = typeof(KeyVisual); - } - - protected override void OnApplyTemplate() - { - IsEnabledChanged -= KeyVisual_IsEnabledChanged; - _keyPresenter = (KeyCharPresenter)this.GetTemplateChild(KeyPresenter); - Update(); - SetVisualStates(); - IsEnabledChanged += KeyVisual_IsEnabledChanged; - base.OnApplyTemplate(); - } - - private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((KeyVisual)d).SetVisualStates(); - } - - private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((KeyVisual)d).SetVisualStates(); - } - - private void SetVisualStates() - { - if (this != null) - { - if (State == State.Error) - { - VisualStateManager.GoToState(this, InvalidState, true); - } - else if (State == State.Warning) - { - VisualStateManager.GoToState(this, WarningState, true); - } - else if (!IsEnabled) - { - VisualStateManager.GoToState(this, DisabledState, true); - } - else - { - VisualStateManager.GoToState(this, NormalState, true); - } - } - } - - private void Update() - { - if (Content == null) - { - return; - } - - if (Content is string key) - { - 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; - } - - if (Content is int keyCode) - { - VirtualKey virtualKey = (VirtualKey)keyCode; - switch (virtualKey) - { - case VirtualKey.Enter: - SetGlyphOrText("\uE751", virtualKey); - break; - - case VirtualKey.Back: - SetGlyphOrText("\uE750", virtualKey); - break; - - case VirtualKey.Shift: - case (VirtualKey)160: // Left Shift - case (VirtualKey)161: // Right Shift - SetGlyphOrText("\uE752", virtualKey); - break; - - case VirtualKey.Up: - _keyPresenter.Content = "\uE0E4"; - break; - - case VirtualKey.Down: - _keyPresenter.Content = "\uE0E5"; - break; - - case VirtualKey.Left: - _keyPresenter.Content = "\uE0E2"; - break; - - case VirtualKey.Right: - _keyPresenter.Content = "\uE0E3"; - break; - - case VirtualKey.LeftWindows: - case VirtualKey.RightWindows: - _keyPresenter.Style = (Style)Application.Current.Resources["WindowsKeyCharPresenterStyle"]; - break; - } - } - } - - private void SetGlyphOrText(string glyph, VirtualKey key) - { - if (RenderKeyAsGlyph) - { - _keyPresenter.Content = glyph; - _keyPresenter.Style = (Style)Application.Current.Resources["GlyphKeyCharPresenterStyle"]; - } - else - { - _keyPresenter.Content = key.ToString(); - _keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"]; - } - } - - private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) - { - SetVisualStates(); - } - } - - public enum State - { - Normal, - Error, - Warning, - } -} diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml index e3842618ce..35c3974a55 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml +++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml @@ -1,13 +1,14 @@ - + @@ -95,7 +96,7 @@ - - @@ -302,27 +304,37 @@ FontSize="13" GotFocus="UrlPathInput_GotFocus" Header="URL to open" - PlaceholderText="https://example.com" /> + PlaceholderText="https://example.com" + TextChanged="UrlPathInput_TextChanged" /> - + + + + + + PlaceholderText="C:\Program Files\..." + TextChanged="ProgramPathInput_TextChanged" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml new file mode 100644 index 0000000000..bb3c06dbcd --- /dev/null +++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml @@ -0,0 +1,525 @@ + + + + + 800 + 800 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs similarity index 91% rename from src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs rename to src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs index 53b1b3d73d..ae79ab5626 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs +++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; @@ -24,7 +25,7 @@ namespace KeyboardManagerEditorUI.Pages /// A consolidated page that displays all mappings from Remappings, Text, Programs, and URLs pages. /// #pragma warning disable SA1124 // Do not use regions - public sealed partial class All : Page, IDisposable + public sealed partial class MainPage : Page, IDisposable, INotifyPropertyChanged { private KeyboardMappingService? _mappingService; private bool _disposed; @@ -33,6 +34,26 @@ namespace KeyboardManagerEditorUI.Pages private bool _isEditMode; private EditingItem? _editingItem; + private string _mappingState = "Empty"; + + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets the current mapping state for the SwitchPresenter: "HasMappings" or "Empty". + /// + public string MappingState + { + get => _mappingState; + private set + { + if (_mappingState != value) + { + _mappingState = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MappingState))); + } + } + } + public ObservableCollection RemappingList { get; } = new ObservableCollection(); public ObservableCollection TextMappings { get; } = new ObservableCollection(); @@ -68,7 +89,7 @@ namespace KeyboardManagerEditorUI.Pages public bool IsAllApps { get; set; } = true; } - public All() + public MainPage() { this.InitializeComponent(); @@ -79,7 +100,7 @@ namespace KeyboardManagerEditorUI.Pages } catch (Exception ex) { - Logger.LogError("Failed to initialize KeyboardMappingService in All page: " + ex.Message); + Logger.LogError("Failed to initialize KeyboardMappingService in MainPage page: " + ex.Message); } this.Unloaded += All_Unloaded; @@ -215,11 +236,16 @@ namespace KeyboardManagerEditorUI.Pages // Hook up the primary button click handler RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick; + // Hook up real-time validation + UnifiedMappingControl.ValidationStateChanged += UnifiedMappingControl_ValidationStateChanged; + RemappingDialog.IsPrimaryButtonEnabled = UnifiedMappingControl.IsInputComplete(); + // Show the dialog await RemappingDialog.ShowAsync(); - // Unhook the handler + // Unhook the handlers RemappingDialog.PrimaryButtonClick -= RemappingDialog_PrimaryButtonClick; + UnifiedMappingControl.ValidationStateChanged -= UnifiedMappingControl_ValidationStateChanged; // Reset edit mode _isEditMode = false; @@ -229,6 +255,36 @@ namespace KeyboardManagerEditorUI.Pages KeyboardHookHelper.Instance.CleanupHook(); } + private void UnifiedMappingControl_ValidationStateChanged(object? sender, EventArgs e) + { + if (!UnifiedMappingControl.IsInputComplete()) + { + RemappingDialog.IsPrimaryButtonEnabled = false; + return; + } + + // Run full validation (self-mapping, illegal shortcuts, etc.) when inputs are complete + if (_mappingService != null) + { + var actionType = UnifiedMappingControl.CurrentActionType; + List triggerKeys = UnifiedMappingControl.GetTriggerKeys(); + + if (triggerKeys != null && triggerKeys.Count > 0) + { + ValidationErrorType error = ValidateMapping(actionType, triggerKeys); + if (error != ValidationErrorType.NoError) + { + UnifiedMappingControl.ShowValidationErrorFromType(error); + RemappingDialog.IsPrimaryButtonEnabled = false; + return; + } + } + } + + UnifiedMappingControl.HideValidationMessage(); + RemappingDialog.IsPrimaryButtonEnabled = true; + } + #endregion #region Save Logic @@ -348,9 +404,20 @@ namespace KeyboardManagerEditorUI.Pages _isEditMode); case UnifiedMappingControl.ActionType.OpenUrl: - case UnifiedMappingControl.ActionType.OpenApp: - return ValidationHelper.ValidateProgramOrUrlMapping( + string urlContent = UnifiedMappingControl.GetUrl(); + return ValidationHelper.ValidateUrlMapping( triggerKeys, + urlContent, + isAppSpecific, + appName, + _mappingService!, + _isEditMode); + + case UnifiedMappingControl.ActionType.OpenApp: + string programPath = UnifiedMappingControl.GetProgramPath(); + return ValidationHelper.ValidateAppMapping( + triggerKeys, + programPath, isAppSpecific, appName, _mappingService!, @@ -620,6 +687,7 @@ namespace KeyboardManagerEditorUI.Pages Logger.LogWarning($"Failed to delete remapping: {string.Join("+", remapping.Shortcut)}"); } + UpdateHasAnyMappings(); break; default: @@ -748,6 +816,16 @@ namespace KeyboardManagerEditorUI.Pages LoadTextMappings(); LoadProgramShortcuts(); LoadUrlShortcuts(); + UpdateHasAnyMappings(); + } + + private void UpdateHasAnyMappings() + { + bool hasAny = RemappingList.Count > 0 + || TextMappings.Count > 0 + || ProgramShortcuts.Count > 0 + || UrlShortcuts.Count > 0; + MappingState = hasAny ? "HasMappings" : "Empty"; } private void LoadRemappings()