diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs new file mode 100644 index 0000000000..3f5b8e9964 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.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.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictControlClickedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public int ConflictCount { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs new file mode 100644 index 0000000000..b8d7c13497 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.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.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictDetectedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public int ConflictCount { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs new file mode 100644 index 0000000000..7f5bf56e82 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.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.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictResolvedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public string Source { get; set; } + } +} 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 25643e0c64..7195b159e1 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 @@ -7,7 +7,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard; +using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.ApplicationModel.Resources; @@ -18,6 +20,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { private static readonly ResourceLoader ResourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + private static bool _telemetryEventSent; + public static readonly DependencyProperty AllHotkeyConflictsDataProperty = DependencyProperty.Register( nameof(AllHotkeyConflictsData), @@ -92,6 +96,17 @@ namespace Microsoft.PowerToys.Settings.UI.Controls // Update visibility based on conflict count Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; + + if (!_telemetryEventSent && HasConflicts) + { + // Log telemetry event when conflicts are detected + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictDetectedEvent() + { + ConflictCount = ConflictCount, + }); + + _telemetryEventSent = true; + } } private void OnPropertyChanged(string propertyName) @@ -115,6 +130,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls return; } + // Log telemetry event when user clicks the shortcut conflict button + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictControlClickedEvent() + { + ConflictCount = this.ConflictCount, + }); + // Create and show the new window instead of dialog var conflictWindow = new ShortcutConflictWindow(); 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 3e3df56690..5b21743d5f 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 @@ -10,17 +10,26 @@ using CommunityToolkit.WinUI; 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.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.Windows.ApplicationModel.Resources; using Windows.System; namespace Microsoft.PowerToys.Settings.UI.Controls { + public enum ShortcutControlSource + { + SettingsPage, + ConflictWindow, + } + public sealed partial class ShortcutControl : UserControl, IDisposable { private readonly UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555; @@ -43,6 +52,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls 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)); + // 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)); + private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -74,6 +86,47 @@ namespace Microsoft.PowerToys.Settings.UI.Controls } control.UpdateKeyVisualStyles(); + + // Check if conflict was resolved (had conflict before, no conflict now) + var oldValue = (bool)(e.OldValue ?? false); + var newValue = (bool)(e.NewValue ?? false); + + // General conflict resolution telemetry (for all sources) + if (oldValue && !newValue) + { + // Determine the actual source based on the control's context + var actualSource = DetermineControlSource(control); + + // Conflict was resolved - send general telemetry + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictResolvedEvent() + { + Source = actualSource.ToString(), + }); + } + } + + private static ShortcutControlSource DetermineControlSource(ShortcutControl control) + { + // Walk up the visual tree to find the parent window/container + DependencyObject parent = control; + while (parent != null) + { + parent = VisualTreeHelper.GetParent(parent); + + // Check if we're in a ShortcutConflictWindow + if (parent != null && parent.GetType().Name == "ShortcutConflictWindow") + { + return ShortcutControlSource.ConflictWindow; + } + + if (parent != null && (parent.GetType().Name == "MainWindow" || parent.GetType().Name == "ShellPage")) + { + return ShortcutControlSource.SettingsPage; + } + } + + // Fallback to the explicitly set value or default + return ShortcutControlSource.ConflictWindow; } private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -108,6 +161,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(TooltipProperty, value); } + public ShortcutControlSource Source + { + get => (ShortcutControlSource)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + public bool Enabled { get diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs index 496a8712a1..3d81acd81d 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs @@ -29,7 +29,7 @@ using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MouseWithoutBordersViewModel : PageViewModelBase, IDisposable + public partial class MouseWithoutBordersViewModel : PageViewModelBase { protected override string ModuleName => MouseWithoutBordersSettings.ModuleName; @@ -43,6 +43,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private readonly Lock _machineMatrixStringLock = new(); + private bool _disposed; + private static readonly Dictionary StatusColors = new Dictionary() { { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, @@ -1262,38 +1264,43 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels protected override void Dispose(bool disposing) { - if (disposing) + if (!_disposed) { - // Cancel the cancellation token source - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); + if (disposing) + { + // Cancel the cancellation token source + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); - // Wait for the machine polling task to complete - try - { - _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); - } - catch (AggregateException) - { - // Task was cancelled, which is expected + // Wait for the machine polling task to complete + try + { + _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); + } + catch (AggregateException) + { + // Task was cancelled, which is expected + } + + // Dispose the named pipe stream + try + { + syncHelperStream?.Dispose(); + } + catch (Exception ex) + { + Logger.LogError($"Error disposing sync helper stream: {ex}"); + } + finally + { + syncHelperStream = null; + } + + // Dispose the semaphore + _ipcSemaphore?.Dispose(); } - // Dispose the named pipe stream - try - { - syncHelperStream?.Dispose(); - } - catch (Exception ex) - { - Logger.LogError($"Error disposing sync helper stream: {ex}"); - } - finally - { - syncHelperStream = null; - } - - // Dispose the semaphore - _ipcSemaphore?.Dispose(); + _disposed = true; } base.Dispose(disposing);