diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp index 84cf9ed949..1ed96e79bd 100644 --- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp +++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp @@ -258,16 +258,6 @@ private: { Logger::info("AlwaysOnTop settings are empty"); } - - if (!m_hotkey.key) - { - Logger::info("AlwaysOnTop is going to use default shortcut"); - m_hotkey.win = true; - m_hotkey.alt = false; - m_hotkey.shift = false; - m_hotkey.ctrl = true; - m_hotkey.key = 'T'; - } } bool is_process_running() diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index bb45f7f5ae..50dd8dbbc8 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -14,6 +14,29 @@ #include #include +namespace +{ + json::JsonValue create_empty_shortcut_array_value() + { + return json::JsonValue::Parse(L"[]"); + } + + void ensure_ignored_conflict_properties_shape(json::JsonObject& obj) + { + if (!json::has(obj, L"ignored_shortcuts", json::JsonValueType::Array)) + { + obj.SetNamedValue(L"ignored_shortcuts", create_empty_shortcut_array_value()); + } + } + + json::JsonObject create_default_ignored_conflict_properties() + { + json::JsonObject obj; + ensure_ignored_conflict_properties_shape(obj); + return obj; + } +} + // TODO: would be nice to get rid of these globals, since they're basically cached json settings static std::wstring settings_theme = L"system"; static bool show_tray_icon = true; @@ -23,11 +46,15 @@ static bool download_updates_automatically = true; static bool show_whats_new_after_updates = true; static bool enable_experimentation = true; static bool enable_warnings_elevated_apps = true; +static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties(); json::JsonObject GeneralSettings::to_json() { json::JsonObject result; + auto ignoredProps = ignoredConflictProperties; + ensure_ignored_conflict_properties_shape(ignoredProps); + result.SetNamedValue(L"startup", json::value(isStartupEnabled)); if (!startupDisabledReason.empty()) { @@ -53,6 +80,7 @@ json::JsonObject GeneralSettings::to_json() result.SetNamedValue(L"theme", json::value(theme)); result.SetNamedValue(L"system_theme", json::value(systemTheme)); result.SetNamedValue(L"powertoys_version", json::value(powerToysVersion)); + result.SetNamedValue(L"ignored_conflict_properties", json::value(ignoredProps)); return result; } @@ -72,6 +100,17 @@ json::JsonObject load_general_settings() enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true); enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true); + if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object)) + { + ignored_conflict_properties = loaded.GetNamedObject(L"ignored_conflict_properties"); + } + else + { + ignored_conflict_properties = create_default_ignored_conflict_properties(); + } + + ensure_ignored_conflict_properties_shape(ignored_conflict_properties); + return loaded; } @@ -91,9 +130,12 @@ GeneralSettings get_general_settings() .enableExperimentation = enable_experimentation, .theme = settings_theme, .systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light", - .powerToysVersion = get_product_version() + .powerToysVersion = get_product_version(), + .ignoredConflictProperties = ignored_conflict_properties }; + ensure_ignored_conflict_properties_shape(settings.ignoredConflictProperties); + settings.isStartupEnabled = is_auto_start_task_active_for_this_user(); for (auto& [name, powertoy] : modules()) @@ -232,6 +274,12 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) set_tray_icon_visible(show_tray_icon); } + if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object)) + { + ignored_conflict_properties = general_configs.GetNamedObject(L"ignored_conflict_properties"); + ensure_ignored_conflict_properties_shape(ignored_conflict_properties); + } + if (save) { GeneralSettings save_settings = get_general_settings(); diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index ef2224b132..38fbd5789a 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -19,6 +19,7 @@ struct GeneralSettings std::wstring theme; std::wstring systemTheme; std::wstring powerToysVersion; + json::JsonObject ignoredConflictProperties; json::JsonObject to_json(); }; diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index 3d295284e3..24ff4584fe 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -76,6 +76,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("enable_experimentation")] public bool EnableExperimentation { get; set; } + [JsonPropertyName("ignored_conflict_properties")] + public ShortcutConflictProperties IgnoredConflictProperties { get; set; } + public GeneralSettings() { Startup = false; @@ -100,6 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library Enabled = new EnabledModules(); CustomActionName = string.Empty; + IgnoredConflictProperties = new ShortcutConflictProperties(); } // converts the current to a json string. @@ -137,6 +141,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library // If there is an issue with the version number format, don't migrate settings. } + // Ensure IgnoredConflictProperties is initialized (for backward compatibility) + if (IgnoredConflictProperties == null) + { + IgnoredConflictProperties = new ShortcutConflictProperties(); + return true; // Indicate that settings were upgraded + } + return false; } diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs index a420ec7a2b..0c76ddf8ea 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs @@ -2,11 +2,7 @@ // 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.Text; -using System.Threading.Tasks; namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts { @@ -16,6 +12,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts public bool IsSystemConflict { get; set; } + public bool ConflictIgnored { get; set; } + + public bool ConflictVisible => !ConflictIgnored; + + public bool ShouldShowSysConflict => !ConflictIgnored && IsSystemConflict; + public List Modules { get; set; } } } diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs index 724e1b5159..b5fa41fcf6 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs @@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library private bool _hasConflict; private string _conflictDescription; private bool _isSystemConflict; + private bool _ignoreConflict; public event PropertyChangedEventHandler PropertyChanged; @@ -57,6 +58,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library HasConflict = false; } + [JsonIgnore] + public bool IgnoreConflict + { + get => _ignoreConflict; + set + { + if (_ignoreConflict != value) + { + _ignoreConflict = value; + OnPropertyChanged(); + } + } + } + + [JsonIgnore] public bool HasConflict { get => _hasConflict; @@ -70,9 +86,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + [JsonIgnore] public string ConflictDescription { - get => _conflictDescription ?? string.Empty; + get => _ignoreConflict ? null : _conflictDescription; set { if (_conflictDescription != value) @@ -83,6 +100,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + [JsonIgnore] public bool IsSystemConflict { get => _isSystemConflict; diff --git a/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs b/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs new file mode 100644 index 0000000000..7ce5fb5b1f --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class ShortcutConflictProperties + { + [JsonPropertyName("ignored_shortcuts")] + public List IgnoredShortcuts { get; set; } + + public ShortcutConflictProperties() + { + IgnoredShortcuts = new List(); + } + } +} diff --git a/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs b/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs new file mode 100644 index 0000000000..04a62b02c7 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs @@ -0,0 +1,45 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Controls; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public partial class BoolToKeyVisualStateConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is bool b && parameter is string param) + { + if (b && param == "Warning") + { + return State.Warning; + } + else if (b && param == "Error") + { + return State.Error; + } + else + { + return State.Normal; + } + } + else + { + return State.Normal; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs new file mode 100644 index 0000000000..d2e737180a --- /dev/null +++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs @@ -0,0 +1,229 @@ +// 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 ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Views; + +namespace Microsoft.PowerToys.Settings.UI.Helpers +{ + /// + /// Static helper class to manage and check hotkey conflict ignore settings + /// + public static class HotkeyConflictIgnoreHelper + { + private static readonly ISettingsRepository _generalSettingsRepository; + private static readonly ISettingsUtils _settingsUtils; + + static HotkeyConflictIgnoreHelper() + { + _settingsUtils = new SettingsUtils(); + _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); + } + + /// + /// Ensures ignored conflict properties are initialized + /// + private static void EnsureInitialized() + { + var settings = _generalSettingsRepository.SettingsConfig; + if (settings.IgnoredConflictProperties == null) + { + settings.IgnoredConflictProperties = new ShortcutConflictProperties(); + SaveSettings(); + } + } + + /// + /// Checks if a specific hotkey setting is configured to ignore conflicts + /// + /// The hotkey settings to check + /// True if the hotkey is set to ignore conflicts, false otherwise + public static bool IsIgnoringConflicts(HotkeySettings hotkeySettings) + { + if (hotkeySettings == null) + { + return false; + } + + try + { + EnsureInitialized(); + var settings = _generalSettingsRepository.SettingsConfig; + return settings.IgnoredConflictProperties.IgnoredShortcuts + .Any(h => AreHotkeySettingsEqual(h, hotkeySettings)); + } + catch (Exception ex) + { + Logger.LogError($"Error checking if hotkey is ignoring conflicts: {ex.Message}"); + return false; + } + } + + /// + /// Adds a hotkey setting to the ignored shortcuts list + /// + /// The hotkey settings to add to the ignored list + /// True if successfully added, false if it was already ignored or on error + public static bool AddToIgnoredList(HotkeySettings hotkeySettings) + { + if (hotkeySettings == null) + { + return false; + } + + try + { + EnsureInitialized(); + var settings = _generalSettingsRepository.SettingsConfig; + + // Check if already ignored (avoid duplicates) + if (IsIgnoringConflicts(hotkeySettings)) + { + Logger.LogInfo($"Hotkey already in ignored list: {hotkeySettings}"); + return false; + } + + // Add to ignored list + settings.IgnoredConflictProperties.IgnoredShortcuts.Add(hotkeySettings); + SaveSettings(); + + Logger.LogInfo($"Added hotkey to ignored list: {hotkeySettings}"); + return true; + } + catch (Exception ex) + { + Logger.LogError($"Error adding hotkey to ignored list: {ex.Message}"); + return false; + } + } + + /// + /// Removes a hotkey setting from the ignored shortcuts list + /// + /// The hotkey settings to remove from the ignored list + /// True if successfully removed, false if it wasn't in the list or on error + public static bool RemoveFromIgnoredList(HotkeySettings hotkeySettings) + { + if (hotkeySettings == null) + { + return false; + } + + try + { + EnsureInitialized(); + var settings = _generalSettingsRepository.SettingsConfig; + var ignoredShortcut = settings.IgnoredConflictProperties.IgnoredShortcuts + .FirstOrDefault(h => AreHotkeySettingsEqual(h, hotkeySettings)); + + if (ignoredShortcut != null) + { + settings.IgnoredConflictProperties.IgnoredShortcuts.Remove(ignoredShortcut); + SaveSettings(); + + Logger.LogInfo($"Removed hotkey from ignored list: {ignoredShortcut}"); + return true; + } + + Logger.LogInfo($"Hotkey not found in ignored list: {hotkeySettings}"); + return false; + } + catch (Exception ex) + { + Logger.LogError($"Error removing hotkey from ignored list: {ex.Message}"); + return false; + } + } + + /// + /// Gets all hotkey settings that are currently being ignored + /// + /// List of ignored hotkey settings + public static List GetAllIgnoredShortcuts() + { + try + { + EnsureInitialized(); + var settings = _generalSettingsRepository.SettingsConfig; + return new List(settings.IgnoredConflictProperties.IgnoredShortcuts); + } + catch (Exception ex) + { + Logger.LogError($"Error getting ignored shortcuts: {ex.Message}"); + return new List(); + } + } + + /// + /// Clears all ignored shortcuts from the list + /// + /// True if successfully cleared, false on error + public static bool ClearAllIgnoredShortcuts() + { + try + { + EnsureInitialized(); + var settings = _generalSettingsRepository.SettingsConfig; + var count = settings.IgnoredConflictProperties.IgnoredShortcuts.Count; + settings.IgnoredConflictProperties.IgnoredShortcuts.Clear(); + SaveSettings(); + + Logger.LogInfo($"Cleared all {count} ignored shortcuts"); + return true; + } + catch (Exception ex) + { + Logger.LogError($"Error clearing ignored shortcuts: {ex.Message}"); + return false; + } + } + + /// + /// Compares two HotkeySettings for equality + /// + /// First hotkey settings + /// Second hotkey settings + /// True if they represent the same shortcut, false otherwise + private static bool AreHotkeySettingsEqual(HotkeySettings hotkey1, HotkeySettings hotkey2) + { + if (hotkey1 == null || hotkey2 == null) + { + return false; + } + + return hotkey1.Win == hotkey2.Win && + hotkey1.Ctrl == hotkey2.Ctrl && + hotkey1.Alt == hotkey2.Alt && + hotkey1.Shift == hotkey2.Shift && + hotkey1.Code == hotkey2.Code; + } + + /// + /// Saves the general settings using PowerToys standard settings persistence + /// + private static void SaveSettings() + { + try + { + var settings = _generalSettingsRepository.SettingsConfig; + + // Send IPC message to notify runner of changes (this is thread-safe) + var outgoing = new OutGoingGeneralSettings(settings); + ShellPage.SendDefaultIPCMessage(outgoing.ToString()); + ShellPage.ShellHandler?.SignalGeneralDataUpdate(); + } + catch (Exception ex) + { + Logger.LogError($"Error saving shortcut conflict settings: {ex.Message}"); + Logger.LogError($"Stack trace: {ex.StackTrace}"); + throw; + } + } + } +} diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs index bd72be5f8c..8a25055620 100644 --- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs +++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs @@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext; [JsonSerializable(typeof(PowerOcrSettings))] [JsonSerializable(typeof(PowerOcrSettings))] [JsonSerializable(typeof(RegistryPreviewSettings))] +[JsonSerializable(typeof(ShortcutConflictProperties))] [JsonSerializable(typeof(ShortcutGuideSettings))] [JsonSerializable(typeof(WINDOWPLACEMENT))] [JsonSerializable(typeof(WorkspacesSettings))] diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml index 69a7a1084d..b071e7f6fe 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml @@ -8,7 +8,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs index 7195b159e1..d7806f17ea 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs @@ -47,12 +47,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls int count = 0; if (AllHotkeyConflictsData.InAppConflicts != null) { - count += AllHotkeyConflictsData.InAppConflicts.Count; + foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts) + { + if (!inAppConflict.ConflictIgnored) + { + count++; + } + } } if (AllHotkeyConflictsData.SystemConflicts != null) { - count += AllHotkeyConflictsData.SystemConflicts.Count; + foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts) + { + if (!systemConflict.ConflictIgnored) + { + count++; + } + } } return count; @@ -95,7 +107,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls OnPropertyChanged(nameof(HasConflicts)); // Update visibility based on conflict count - Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; + if (HasConflicts) + { + VisualStateManager.GoToState(this, "ConflictState", true); + } + else + { + VisualStateManager.GoToState(this, "NoConflictState", true); + } if (!_telemetryEventSent && HasConflicts) { @@ -119,13 +138,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls InitializeComponent(); DataContext = this; - // Initially hide the control if no conflicts - Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; + UpdateProperties(); } private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e) { - if (AllHotkeyConflictsData == null || !HasConflicts) + if (AllHotkeyConflictsData == null) { return; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml index 46f8d4f962..11d9b5f7b0 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml @@ -53,34 +53,22 @@ - - - - - - - - - - + + + + + + + @@ -97,22 +85,40 @@ - + - + + + + - + @@ -137,15 +143,15 @@ + Background="Transparent" + BorderThickness="0,1,0,0" + CornerRadius="0" + IsEnabled="{x:Bind ShouldShowSysConflict, Mode=OneWay}"> - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs index 5bcc282261..b9bee4ff08 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs @@ -14,6 +14,7 @@ using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Windows.Graphics; using WinUIEx; @@ -21,8 +22,6 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard { public sealed partial class ShortcutConflictWindow : WindowEx { - public ShortcutConflictViewModel DataContext { get; } - public ShortcutConflictViewModel ViewModel { get; private set; } public ShortcutConflictWindow() @@ -33,14 +32,17 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); - DataContext = ViewModel; InitializeComponent(); + // Set DataContext on the root Grid instead of the Window + RootGrid.DataContext = ViewModel; + this.Activated += Window_Activated_SetIcon; // Set localized window title var resourceLoader = ResourceLoaderInstance.ResourceLoader; - this.ExtendsContentIntoTitleBar = true; + ExtendsContentIntoTitleBar = true; + SetTitleBar(titleBar); this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title"); this.CenterOnScreen(); @@ -74,6 +76,54 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard } } + private void OnIgnoreConflictClicked(object sender, RoutedEventArgs e) + { + if (sender is CheckBox checkBox && checkBox.DataContext is HotkeyConflictGroupData conflictGroup) + { + // The Click event only fires from user interaction, not programmatic changes + if (checkBox.IsChecked == true) + { + IgnoreConflictGroup(conflictGroup); + } + else + { + UnignoreConflictGroup(conflictGroup); + } + } + } + + private void IgnoreConflictGroup(HotkeyConflictGroupData conflictGroup) + { + try + { + // Ignore all hotkey settings in this conflict group + if (conflictGroup.Modules != null) + { + HotkeySettings hotkey = new(conflictGroup.Hotkey.Win, conflictGroup.Hotkey.Ctrl, conflictGroup.Hotkey.Alt, conflictGroup.Hotkey.Shift, conflictGroup.Hotkey.Key); + ViewModel.IgnoreShortcut(hotkey); + } + } + catch + { + } + } + + private void UnignoreConflictGroup(HotkeyConflictGroupData conflictGroup) + { + try + { + // Unignore all hotkey settings in this conflict group + if (conflictGroup.Modules != null) + { + HotkeySettings hotkey = new(conflictGroup.Hotkey.Win, conflictGroup.Hotkey.Ctrl, conflictGroup.Hotkey.Alt, conflictGroup.Hotkey.Shift, conflictGroup.Hotkey.Key); + ViewModel.UnignoreShortcut(hotkey); + } + } + catch + { + } + } + private void WindowEx_Closed(object sender, WindowEventArgs args) { ViewModel?.Dispose(); @@ -82,10 +132,7 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args) { // Set window icon - var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); - WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd); - AppWindow appWindow = AppWindow.GetFromWindowId(windowId); - appWindow.SetIcon("Assets\\Settings\\icon.ico"); + AppWindow.SetIcon("Assets\\Settings\\icon.ico"); } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml index 931286ceaf..1228911082 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml @@ -63,10 +63,18 @@ - + + + + + + + + + @@ -120,6 +128,11 @@ + + + + + @@ -177,10 +190,18 @@ - + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs index b638c32f2b..87dc9a4c21 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs @@ -12,12 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls [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 @@ -28,13 +30,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged)); - public bool IsInvalid + public State State { - get => (bool)GetValue(IsInvalidProperty); - set => SetValue(IsInvalidProperty, value); + get => (State)GetValue(StateProperty); + set => SetValue(StateProperty, value); } - public static readonly DependencyProperty IsInvalidProperty = DependencyProperty.Register(nameof(IsInvalid), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsInvalidChanged)); + public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(State), typeof(KeyVisual), new PropertyMetadata(State.Normal, OnStateChanged)); public bool RenderKeyAsGlyph { @@ -64,7 +66,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls ((KeyVisual)d).SetVisualStates(); } - private static void OnIsInvalidChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((KeyVisual)d).SetVisualStates(); } @@ -73,10 +75,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { if (this != null) { - if (IsInvalid) + 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); @@ -177,4 +183,11 @@ namespace Microsoft.PowerToys.Settings.UI.Controls SetVisualStates(); } } + + public enum State + { + Normal, + Error, + Warning, + } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml index d81be4aa6c..b7983585ac 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml @@ -6,11 +6,14 @@ xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" x:Name="LayoutRoot" d:DesignHeight="300" d:DesignWidth="400" mc:Ignorable="d"> - + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index 6b4b9b7957..ba053e1124 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -12,6 +12,7 @@ using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard; using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; @@ -51,6 +52,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged)); public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged)); public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged)); + public static readonly DependencyProperty KeyVisualShouldShowConflictProperty = DependencyProperty.Register("KeyVisualShouldShowConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false)); + public static readonly DependencyProperty IgnoreConflictProperty = DependencyProperty.Register("IgnoreConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false)); // Dependency property to track the source/context of the ShortcutControl public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ShortcutControlSource), typeof(ShortcutControl), new PropertyMetadata(ShortcutControlSource.SettingsPage)); @@ -161,6 +164,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(TooltipProperty, value); } + public bool KeyVisualShouldShowConflict + { + get => (bool)GetValue(KeyVisualShouldShowConflictProperty); + set => SetValue(KeyVisualShouldShowConflictProperty, value); + } + + public bool IgnoreConflict + { + get => (bool)GetValue(IgnoreConflictProperty); + set => SetValue(IgnoreConflictProperty, value); + } + public ShortcutControlSource Source { get => (ShortcutControlSource)GetValue(SourceProperty); @@ -241,6 +256,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls // Update the ShortcutControl's conflict properties from HotkeySettings HasConflict = hotkeySettings.HasConflict; Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null; + IgnoreConflict = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings); + KeyVisualShouldShowConflict = !IgnoreConflict && HasConflict; } else { @@ -257,6 +274,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls this.Unloaded += ShortcutControl_Unloaded; this.Loaded += ShortcutControl_Loaded; + c.ResetClick += C_ResetClick; + c.ClearClick += C_ClearClick; + c.LearnMoreClick += C_LearnMoreClick; + // We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme. shortcutDialog = new ContentDialog { @@ -264,11 +285,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls Title = resourceLoader.GetString("Activation_Shortcut_Title"), Content = c, PrimaryButtonText = resourceLoader.GetString("Activation_Shortcut_Save"), - SecondaryButtonText = resourceLoader.GetString("Activation_Shortcut_Reset"), CloseButtonText = resourceLoader.GetString("Activation_Shortcut_Cancel"), DefaultButton = ContentDialogButton.Primary, }; - shortcutDialog.SecondaryButtonClick += ShortcutDialog_Reset; shortcutDialog.RightTapped += ShortcutDialog_Disable; AutomationProperties.SetName(EditButton, resourceLoader.GetString("Activation_Shortcut_Title")); @@ -276,6 +295,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls OnAllowDisableChanged(this, null); } + private void C_LearnMoreClick(object sender, RoutedEventArgs e) + { + // Close the current shortcut dialog + shortcutDialog.Hide(); + + // Create and show the ShortcutConflictWindow + var conflictWindow = new ShortcutConflictWindow(); + conflictWindow.Activate(); + } + private void UpdateKeyVisualStyles() { if (PreviewKeysControl?.ItemsSource != null) @@ -305,6 +334,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls shortcutDialog.Opened -= ShortcutDialog_Opened; shortcutDialog.Closing -= ShortcutDialog_Closing; + c.LearnMoreClick -= C_LearnMoreClick; + if (App.GetSettingsWindow() != null) { App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated; @@ -510,6 +541,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls else { EnableKeys(); + if (lastValidSettings.IsValid()) { if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase)) @@ -578,16 +610,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { shortcutDialog.IsPrimaryButtonEnabled = true; c.IsError = false; - - // WarningLabel.Style = (Style)App.Current.Resources["SecondaryTextStyle"]; } private void DisableKeys() { shortcutDialog.IsPrimaryButtonEnabled = false; c.IsError = true; - - // WarningLabel.Style = (Style)App.Current.Resources["SecondaryWarningTextStyle"]; } private void Hotkey_KeyUp(int key) @@ -648,6 +676,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls c.Keys = null; c.Keys = HotkeySettings.GetKeysList(); + c.IgnoreConflict = IgnoreConflict; c.HasConflict = hotkeySettings.HasConflict; c.ConflictMessage = hotkeySettings.ConflictDescription; @@ -660,7 +689,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls await shortcutDialog.ShowAsync(); } - private void ShortcutDialog_Reset(ContentDialog sender, ContentDialogButtonClickEventArgs args) + private void C_ResetClick(object sender, RoutedEventArgs e) { hotkeySettings = null; @@ -674,6 +703,20 @@ namespace Microsoft.PowerToys.Settings.UI.Controls GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); } + private void C_ClearClick(object sender, RoutedEventArgs e) + { + hotkeySettings = new HotkeySettings(); + + SetValue(HotkeySettingsProperty, hotkeySettings); + SetKeys(); + + lastValidSettings = hotkeySettings; + shortcutDialog.Hide(); + + // Send RequestAllConflicts IPC to update the UI after changed hotkey settings. + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + } + private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { if (ComboIsValid(lastValidSettings)) @@ -728,7 +771,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls args.Handled = true; if (args.WindowActivationState != WindowActivationState.Deactivated && (hook == null || hook.GetDisposedState() == true)) { - // If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input. + // If the PT settings window gets focused/activated again, we enable the keyboard hook to catch the keyboard input. hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents); } else if (args.WindowActivationState == WindowActivationState.Deactivated && hook != null && hook.GetDisposedState() == false) @@ -742,6 +785,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls private void ShortcutDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args) { _isActive = false; + lastValidSettings = hotkeySettings; } private void Dispose(bool disposing) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index 3f345b8650..31e2f742e6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -3,78 +3,332 @@ 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:converters="using:Microsoft.PowerToys.Settings.UI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" + xmlns:tk7controls="using:CommunityToolkit.WinUI.Controls" + xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" x:Name="ShortcutContentControl" mc:Ignorable="d"> - + + + + + - + + - + - - - - - - - - - - - - + Margin="0,16,0,0" + Background="{ThemeResource SolidBackgroundFillColorTertiaryBrush}" + CornerRadius="{StaticResource OverlayCornerRadius}"> + + + + + + + + + + + + + + - - - - - - + HorizontalAlignment="Center" + Orientation="Horizontal" + Spacing="12"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs index 8907f12415..9a369f0ebc 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs @@ -2,8 +2,10 @@ // 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.Diagnostics.Eventing.Reader; +using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -14,8 +16,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string))); public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); - public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false, OnConflictPropertyChanged)); public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty)); + public static readonly DependencyProperty IgnoreConflictProperty = DependencyProperty.Register("IgnoreConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false, OnIgnoreConflictChanged)); + + public static readonly DependencyProperty ShouldShowConflictProperty = DependencyProperty.Register("ShouldShowConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty ShouldShowPotentialConflictProperty = DependencyProperty.Register("ShouldShowPotentialConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + + public event EventHandler IgnoreConflictChanged; + + public event RoutedEventHandler LearnMoreClick; + + public bool IgnoreConflict + { + get => (bool)GetValue(IgnoreConflictProperty); + set => SetValue(IgnoreConflictProperty, value); + } public bool HasConflict { @@ -29,9 +45,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(ConflictMessageProperty, value); } + public bool ShouldShowConflict + { + get => (bool)GetValue(ShouldShowConflictProperty); + private set => SetValue(ShouldShowConflictProperty, value); + } + + public bool ShouldShowPotentialConflict + { + get => (bool)GetValue(ShouldShowPotentialConflictProperty); + private set => SetValue(ShouldShowPotentialConflictProperty, value); + } + public ShortcutDialogContentControl() { this.InitializeComponent(); + UpdateShouldShowConflict(); } public List Keys @@ -51,5 +80,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls get => (bool)GetValue(IsWarningAltGrProperty); set => SetValue(IsWarningAltGrProperty, value); } + + public event RoutedEventHandler ResetClick; + + public event RoutedEventHandler ClearClick; + + private static void OnIgnoreConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutDialogContentControl; + if (control == null) + { + return; + } + + control.UpdateShouldShowConflict(); + + control.IgnoreConflictChanged?.Invoke(control, (bool)e.NewValue); + } + + private static void OnConflictPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutDialogContentControl; + if (control == null) + { + return; + } + + control.UpdateShouldShowConflict(); + } + + private void UpdateShouldShowConflict() + { + ShouldShowConflict = !IgnoreConflict && HasConflict; + ShouldShowPotentialConflict = IgnoreConflict && HasConflict; + } + + private void ResetBtn_Click(object sender, RoutedEventArgs e) + { + ResetClick?.Invoke(this, new RoutedEventArgs()); + } + + private void ClearBtn_Click(object sender, RoutedEventArgs e) + { + ClearClick?.Invoke(this, new RoutedEventArgs()); + } + + private void LearnMoreBtn_Click(object sender, RoutedEventArgs e) + { + LearnMoreClick?.Invoke(this, new RoutedEventArgs()); + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs index baade0fb16..933a4d0c24 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs @@ -14,7 +14,6 @@ using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard; using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Telemetry; -using Microsoft.UI; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; @@ -29,6 +28,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); private Windows.ApplicationModel.Resources.ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + private int _conflictCount; + public bool EnableDataDiagnostics { get @@ -60,6 +61,9 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views if (_allHotkeyConflictsData != value) { _allHotkeyConflictsData = value; + + UpdateConflictCount(); + OnPropertyChanged(nameof(AllHotkeyConflictsData)); OnPropertyChanged(nameof(ConflictCount)); OnPropertyChanged(nameof(ConflictText)); @@ -71,28 +75,43 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views } } - public int ConflictCount + public int ConflictCount => _conflictCount; + + private void UpdateConflictCount() { - get + int count = 0; + if (AllHotkeyConflictsData == null) { - if (AllHotkeyConflictsData == null) - { - return 0; - } - - int count = 0; - if (AllHotkeyConflictsData.InAppConflicts != null) - { - count += AllHotkeyConflictsData.InAppConflicts.Count; - } - - if (AllHotkeyConflictsData.SystemConflicts != null) - { - count += AllHotkeyConflictsData.SystemConflicts.Count; - } - - return count; + _conflictCount = count; } + + if (AllHotkeyConflictsData.InAppConflicts != null) + { + foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts) + { + var hotkey = inAppConflict.Hotkey; + var hotkeySettings = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + if (!HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings)) + { + count++; + } + } + } + + if (AllHotkeyConflictsData.SystemConflicts != null) + { + foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts) + { + var hotkey = systemConflict.Hotkey; + var hotkeySettings = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + if (!HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings)) + { + count++; + } + } + } + + _conflictCount = count; } public string ConflictText diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs index ba7962840a..cf4488b759 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs @@ -16,7 +16,9 @@ using CommunityToolkit.WinUI.Controls; using global::PowerToys.GPOWrapper; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; using Microsoft.PowerToys.Settings.UI.SerializationContext; @@ -39,6 +41,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar(); + private int _conflictCount; + public AllHotkeyConflictsData AllHotkeyConflictsData { get => _allHotkeyConflictsData; @@ -47,34 +51,48 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views if (_allHotkeyConflictsData != value) { _allHotkeyConflictsData = value; + + UpdateConflictCount(); + OnPropertyChanged(nameof(AllHotkeyConflictsData)); OnPropertyChanged(nameof(HasConflicts)); } } } - public bool HasConflicts + public bool HasConflicts => _conflictCount > 0; + + private void UpdateConflictCount() { - get + int count = 0; + if (AllHotkeyConflictsData == null) { - if (AllHotkeyConflictsData == null) - { - return false; - } - - int count = 0; - if (AllHotkeyConflictsData.InAppConflicts != null) - { - count += AllHotkeyConflictsData.InAppConflicts.Count; - } - - if (AllHotkeyConflictsData.SystemConflicts != null) - { - count += AllHotkeyConflictsData.SystemConflicts.Count; - } - - return count > 0; + _conflictCount = count; } + + if (AllHotkeyConflictsData.InAppConflicts != null) + { + foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts) + { + if (!inAppConflict.ConflictIgnored) + { + count++; + } + } + } + + if (AllHotkeyConflictsData.SystemConflicts != null) + { + foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts) + { + if (!systemConflict.ConflictIgnored) + { + count++; + } + } + } + + _conflictCount = count; } public event PropertyChangedEventHandler PropertyChanged; @@ -100,6 +118,21 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => { + var allConflictData = e.Conflicts; + foreach (var inAppConflict in allConflictData.InAppConflicts) + { + var hotkey = inAppConflict.Hotkey; + var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting); + } + + foreach (var systemConflict in allConflictData.SystemConflicts) + { + var hotkey = systemConflict.Hotkey; + var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting); + } + AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); }); } diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 9a3009ca80..66e337bda0 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2667,23 +2667,20 @@ From there, simply click on one of the supported files in the File Explorer and Press a combination of keys to change this shortcut. Right-click to remove the key combination, thereby deactivating the shortcut. - - Reset - Save Activation shortcut - + Invalid shortcut - Only shortcuts that start with **Windows key**, **Ctrl**, **Alt** or **Shift** are valid. + A shortcut should start with **Windows key**, **Ctrl**, **Alt** or **Shift**. The ** sequences are used for text formatting of the key names. Don't remove them on translation. - + Possible shortcut interference with Alt Gr Alt Gr refers to the right alt key on some international keyboards @@ -2691,8 +2688,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Shortcuts with **Ctrl** and **Alt** may remove functionality from some international keyboards, because **Ctrl** + **Alt** = **Alt Gr** in those keyboards. The ** sequences are used for text formatting of the key names. Don't remove them on translation. - - Shortcut conflict + + This shortcut has a potential conflict, but the warning is ignored. A conflict has been detected for this shortcut. @@ -5256,23 +5253,23 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m PowerToys shortcut conflicts - + PowerToys shortcut conflicts - Conflicting shortcuts may cause unexpected behavior. Edit them here or go to the module settings to update them. + If any shortcut conflicts are detected, they’ll appear below. Conflicts can happen between PowerToys utilities or Windows system shortcuts, and may cause unexpected behavior. If everything works as expected, you can safely ignore the conflict. Conflicts found for - System + System shortcut - Windows system shortcut + This shortcut is reserved by Windows and can't be reassigned. - - This shortcut can't be changed. + + See all Windows shortcuts This shortcut is used by Windows and can't be changed. @@ -5312,4 +5309,31 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Utilities + + Dismiss + + + Dismiss + + + Reset shortcut + + + Reset to the default shortcut + + + Reset + + + Clear shortcut + + + Clear and unassign this shortcut + + + Clear + + + Learn more + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs index d9be787e70..d7b03efad4 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs @@ -128,14 +128,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (value != _hotkey) { - if (value == null || value.IsEmpty()) - { - _hotkey = AlwaysOnTopProperties.DefaultHotkeyValue; - } - else - { - _hotkey = value; - } + _hotkey = value ?? AlwaysOnTopProperties.DefaultHotkeyValue; Settings.Properties.Hotkey.Value = _hotkey; NotifyPropertyChanged(); diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs index 7b62732e87..82d3aa358d 100644 --- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs @@ -29,7 +29,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { protected override string ModuleName => "Dashboard"; - private const string JsonFileType = ".json"; private Dispatcher dispatcher; public Func SendConfigMSG { get; } @@ -88,6 +87,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { dispatcher.BeginInvoke(() => { + var allConflictData = e.Conflicts; + foreach (var inAppConflict in allConflictData.InAppConflicts) + { + var hotkey = inAppConflict.Hotkey; + var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting); + } + + foreach (var systemConflict in allConflictData.SystemConflicts) + { + var hotkey = systemConflict.Hotkey; + var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key); + systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting); + } + AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); }); } diff --git a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs index 0f0ba98d11..7ce6ec74c6 100644 --- a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs @@ -776,7 +776,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (value != _editorHotkey) { - if (value == null || value.IsEmpty()) + if (value == null) { _editorHotkey = FZConfigProperties.DefaultEditorHotkeyValue; } @@ -822,7 +822,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (value != _nextTabHotkey) { - if (value == null || value.IsEmpty()) + if (value == null) { _nextTabHotkey = FZConfigProperties.DefaultNextTabHotkeyValue; } @@ -848,7 +848,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (value != _prevTabHotkey) { - if (value == null || value.IsEmpty()) + if (value == null) { _prevTabHotkey = FZConfigProperties.DefaultPrevTabHotkeyValue; } diff --git a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs index 2cfcbaf42f..cfd3683080 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs @@ -12,12 +12,10 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using System.Windows; using System.Windows.Threading; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.SerializationContext; @@ -70,6 +68,36 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels protected override string ModuleName => "ShortcutConflictsWindow"; + /// + /// Ignore a specific HotkeySettings + /// + /// The HotkeySettings to ignore + public void IgnoreShortcut(HotkeySettings hotkeySettings) + { + if (hotkeySettings == null) + { + return; + } + + HotkeyConflictIgnoreHelper.AddToIgnoredList(hotkeySettings); + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + } + + /// + /// Remove a HotkeySettings from the ignored list + /// + /// The HotkeySettings to unignore + public void UnignoreShortcut(HotkeySettings hotkeySettings) + { + if (hotkeySettings == null) + { + return; + } + + HotkeyConflictIgnoreHelper.RemoveFromIgnoredList(hotkeySettings); + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + } + private IHotkeyConfig GetModuleSettings(string moduleKey) { try @@ -120,20 +148,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels foreach (var conflict in conflicts) { - ProcessConflictGroup(conflict, isSystemConflict); + HotkeySettings hotkey = new(conflict.Hotkey.Win, conflict.Hotkey.Ctrl, conflict.Hotkey.Alt, conflict.Hotkey.Shift, conflict.Hotkey.Key); + var isIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkey); + conflict.ConflictIgnored = isIgnored; + + ProcessConflictGroup(conflict, isSystemConflict, isIgnored); items.Add(conflict); } } - private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict) + private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict, bool isIgnored) { foreach (var module in conflict.Modules) { - SetupModuleData(module, isSystemConflict); + SetupModuleData(module, isSystemConflict, isIgnored); } } - private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict) + private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict, bool isIgnored) { try { @@ -220,55 +252,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } - private void SaveModuleSettingsAndNotify(string moduleName) - { - try - { - var settings = GetModuleSettings(moduleName); - - if (settings is ISettingsConfig settingsConfig) - { - // No need to save settings here, the runner will call module interface to save it - // SaveSettingsToFile(settings); - - // Send IPC notification using the same format as other ViewModels - SendConfigMSG(settingsConfig, moduleName); - - System.Diagnostics.Debug.WriteLine($"Saved settings and sent IPC notification for module: {moduleName}"); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Error saving settings and notifying for {moduleName}: {ex.Message}"); - } - } - - private void SaveSettingsToFile(IHotkeyConfig settings) - { - try - { - // Get the repository for this settings type using reflection - var settingsType = settings.GetType(); - var repositoryMethod = typeof(SettingsFactory).GetMethod("GetRepository"); - if (repositoryMethod != null) - { - var genericMethod = repositoryMethod.MakeGenericMethod(settingsType); - var repository = genericMethod.Invoke(_settingsFactory, null); - - if (repository != null) - { - var saveMethod = repository.GetType().GetMethod("SaveSettingsToFile"); - saveMethod?.Invoke(repository, null); - System.Diagnostics.Debug.WriteLine($"Saved settings to file for type: {settingsType.Name}"); - } - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Error saving settings to file: {ex.Message}"); - } - } - /// /// Sends IPC notification using the same format as other ViewModels /// diff --git a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs index 2c05c79358..842c3cf368 100644 --- a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs @@ -127,7 +127,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (value != _hotkey) { - if (value == null || value.IsEmpty()) + if (value == null) { _hotkey = WorkspacesProperties.DefaultHotkeyValue; }