From 9f5930fd7d3bc7194fa741510ef12a63e4f3f158 Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Fri, 19 Jul 2024 01:55:50 +0300 Subject: [PATCH] Fix hotkeys not registering properly on startup (#324) --- .../Internal/NativeModule.cs | 8 ++++ .../Internal/WndClassEx.cs | 8 +--- .../Internal/WndProcSponge.cs | 14 ++++-- LightBulb/App.axaml.cs | 19 ++++---- LightBulb/Services/HotKeyService.cs | 2 +- .../NotifyPropertyChangedExtensions.cs | 48 ++++++++++++++----- .../Components/DashboardViewModel.cs | 12 ++++- ...pplicationWhitelistSettingsTabViewModel.cs | 2 - .../Settings/LocationSettingsTabViewModel.cs | 4 +- .../Settings/SettingsTabViewModelBase.cs | 2 - LightBulb/ViewModels/MainViewModel.cs | 2 - ...plicationWhitelistSettingsTabView.axaml.cs | 3 +- 12 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 LightBulb.PlatformInterop/Internal/NativeModule.cs diff --git a/LightBulb.PlatformInterop/Internal/NativeModule.cs b/LightBulb.PlatformInterop/Internal/NativeModule.cs new file mode 100644 index 0000000..4748dfa --- /dev/null +++ b/LightBulb.PlatformInterop/Internal/NativeModule.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace LightBulb.PlatformInterop.Internal; + +internal static class NativeModule +{ + public static nint CurrentHandle { get; } = Marshal.GetHINSTANCE(typeof(NativeModule).Module); +} diff --git a/LightBulb.PlatformInterop/Internal/WndClassEx.cs b/LightBulb.PlatformInterop/Internal/WndClassEx.cs index 0f33a4e..3ff78eb 100644 --- a/LightBulb.PlatformInterop/Internal/WndClassEx.cs +++ b/LightBulb.PlatformInterop/Internal/WndClassEx.cs @@ -5,18 +5,14 @@ namespace LightBulb.PlatformInterop.Internal; [StructLayout(LayoutKind.Sequential)] internal readonly record struct WndClassEx { - public WndClassEx() - { - Size = (uint)Marshal.SizeOf(this); - Instance = Marshal.GetHINSTANCE(typeof(WndClassEx).Module); - } + public WndClassEx() => Size = (uint)Marshal.SizeOf(this); public uint Size { get; } public uint Style { get; init; } public required WndProc WndProc { get; init; } public int ClassExtra { get; init; } public int WindowExtra { get; init; } - public nint Instance { get; } + public nint Instance { get; init; } public nint Icon { get; init; } public nint Cursor { get; init; } public nint Background { get; init; } diff --git a/LightBulb.PlatformInterop/Internal/WndProcSponge.cs b/LightBulb.PlatformInterop/Internal/WndProcSponge.cs index 6e9ad46..b5bd37e 100644 --- a/LightBulb.PlatformInterop/Internal/WndProcSponge.cs +++ b/LightBulb.PlatformInterop/Internal/WndProcSponge.cs @@ -37,7 +37,7 @@ public void Dispose() if (!NativeMethods.DestroyWindow(windowHandle)) Debug.WriteLine($"Failed to destroy window #{windowHandle}."); - if (!NativeMethods.UnregisterClass(ClassName, classHandle)) + if (!NativeMethods.UnregisterClass(ClassName, NativeModule.CurrentHandle)) Debug.WriteLine($"Failed to unregister window class #{classHandle}."); } } @@ -67,7 +67,13 @@ internal partial class WndProcSponge } ); - var classInfo = new WndClassEx { ClassName = ClassName, WndProc = wndProc }; + var classInfo = new WndClassEx + { + ClassName = ClassName, + WndProc = wndProc, + Instance = NativeModule.CurrentHandle + }; + var classHandle = NativeMethods.RegisterClassEx(ref classInfo); if (classHandle == 0) { @@ -84,7 +90,7 @@ internal partial class WndProcSponge 0, 0, 0, - 0, + -3, // HWND_MESSAGE 0, 0, 0 @@ -93,7 +99,7 @@ internal partial class WndProcSponge if (windowHandle == 0) { Debug.WriteLine("Failed to create window."); - NativeMethods.UnregisterClass(ClassName, classHandle); + NativeMethods.UnregisterClass(ClassName, NativeModule.CurrentHandle); return null; } diff --git a/LightBulb/App.axaml.cs b/LightBulb/App.axaml.cs index 4adbe50..cc6332a 100644 --- a/LightBulb/App.axaml.cs +++ b/LightBulb/App.axaml.cs @@ -74,8 +74,7 @@ public App() }; InitializeTheme(); - }, - false + } ) ); @@ -95,13 +94,17 @@ public App() + Environment.NewLine + (_mainViewModel.Dashboard.IsActive ? status : "Disabled"); - Dispatcher.UIThread.Invoke(() => + try { - if (TrayIcon.GetIcons(this)?.FirstOrDefault() is { } trayIcon) - trayIcon.ToolTipText = tooltip; - }); - }, - false + Dispatcher.UIThread.Invoke(() => + { + if (TrayIcon.GetIcons(this)?.FirstOrDefault() is { } trayIcon) + trayIcon.ToolTipText = tooltip; + }); + } + // Ignore exceptions when the application is shutting down + catch (OperationCanceledException) { } + } ) ); } diff --git a/LightBulb/Services/HotKeyService.cs b/LightBulb/Services/HotKeyService.cs index bf4b69e..2d83613 100644 --- a/LightBulb/Services/HotKeyService.cs +++ b/LightBulb/Services/HotKeyService.cs @@ -15,7 +15,7 @@ public class HotKeyService : IDisposable public void RegisterHotKey(HotKey hotKey, Action callback) { - // Convert WPF key/modifiers to Windows API virtual key/modifiers + // Convert Avalonia key/modifiers to Windows API virtual key/modifiers var virtualKey = KeyInterop.VirtualKeyFromKey(hotKey.Key.ToQwertyKey()); var modifiers = (int)hotKey.Modifiers; diff --git a/LightBulb/Utils/Extensions/NotifyPropertyChangedExtensions.cs b/LightBulb/Utils/Extensions/NotifyPropertyChangedExtensions.cs index 50a18ff..32beca6 100644 --- a/LightBulb/Utils/Extensions/NotifyPropertyChangedExtensions.cs +++ b/LightBulb/Utils/Extensions/NotifyPropertyChangedExtensions.cs @@ -13,15 +13,11 @@ public static IDisposable WatchProperty( this TOwner owner, Expression> propertyExpression, Action callback, - bool watchInitialValue = true + bool watchInitialValue = false ) where TOwner : INotifyPropertyChanged { - var memberExpression = - propertyExpression.Body as MemberExpression - // Property value might be boxed inside a conversion expression, if the types don't match - ?? (propertyExpression.Body as UnaryExpression)?.Operand as MemberExpression; - + var memberExpression = propertyExpression.Body as MemberExpression; if (memberExpression?.Member is not PropertyInfo property) throw new ArgumentException("Provided expression must reference a property."); @@ -48,21 +44,51 @@ public static IDisposable WatchProperties( this TOwner owner, IReadOnlyList>> propertyExpressions, Action callback, - bool watchInitialValue = true + bool watchInitialValue = false ) where TOwner : INotifyPropertyChanged { - var watchers = propertyExpressions - .Select(x => WatchProperty(owner, x, callback, watchInitialValue)) + var properties = propertyExpressions + .Select(expression => + { + var memberExpression = + expression.Body as MemberExpression + // Because the expression is typed to return an object, the compiler will + // implicitly wrap it in a conversion unary expression if it's of any other type. + ?? (expression.Body as UnaryExpression)?.Operand as MemberExpression; + + if (memberExpression?.Member is not PropertyInfo property) + throw new ArgumentException("Provided expression must reference a property."); + + return property; + }) .ToArray(); - return Disposable.Create(() => watchers.DisposeAll()); + void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) + { + if ( + string.IsNullOrWhiteSpace(args.PropertyName) + || properties.Any(p => + string.Equals(args.PropertyName, p.Name, StringComparison.Ordinal) + ) + ) + { + callback(); + } + } + + owner.PropertyChanged += OnPropertyChanged; + + if (watchInitialValue) + callback(); + + return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged); } public static IDisposable WatchAllProperties( this TOwner owner, Action callback, - bool watchInitialValues = true + bool watchInitialValues = false ) where TOwner : INotifyPropertyChanged { diff --git a/LightBulb/ViewModels/Components/DashboardViewModel.cs b/LightBulb/ViewModels/Components/DashboardViewModel.cs index 6e58b6e..37f148d 100644 --- a/LightBulb/ViewModels/Components/DashboardViewModel.cs +++ b/LightBulb/ViewModels/Components/DashboardViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LightBulb.Core; @@ -100,6 +101,7 @@ ExternalApplicationService externalApplicationService // Re-register hotkeys when they get updated settingsService.WatchProperties( [ + o => o.FocusWindowHotKey, o => o.ToggleHotKey, o => o.IncreaseTemperatureOffsetHotKey, o => o.DecreaseTemperatureOffsetHotKey, @@ -196,6 +198,14 @@ private void RegisterHotKeys() { _hotKeyService.UnregisterAllHotKeys(); + if (_settingsService.FocusWindowHotKey != HotKey.None) + { + _hotKeyService.RegisterHotKey( + _settingsService.FocusWindowHotKey, + () => Application.Current?.TryFocusMainWindow() + ); + } + if (_settingsService.ToggleHotKey != HotKey.None) { _hotKeyService.RegisterHotKey( @@ -371,8 +381,6 @@ private void Initialize() _updateConfigurationTimer.Start(); _updateIsPausedTimer.Start(); - RegisterHotKeys(); - // Hack: feign property changes to refresh the tray icon OnAllPropertiesChanged(); } diff --git a/LightBulb/ViewModels/Components/Settings/ApplicationWhitelistSettingsTabViewModel.cs b/LightBulb/ViewModels/Components/Settings/ApplicationWhitelistSettingsTabViewModel.cs index 984ca67..ab8f4c9 100644 --- a/LightBulb/ViewModels/Components/Settings/ApplicationWhitelistSettingsTabViewModel.cs +++ b/LightBulb/ViewModels/Components/Settings/ApplicationWhitelistSettingsTabViewModel.cs @@ -67,9 +67,7 @@ private void RefreshApplications() protected override void Dispose(bool disposing) { if (disposing) - { _eventRoot.Dispose(); - } base.Dispose(disposing); } diff --git a/LightBulb/ViewModels/Components/Settings/LocationSettingsTabViewModel.cs b/LightBulb/ViewModels/Components/Settings/LocationSettingsTabViewModel.cs index cdf9703..4a4dc48 100644 --- a/LightBulb/ViewModels/Components/Settings/LocationSettingsTabViewModel.cs +++ b/LightBulb/ViewModels/Components/Settings/LocationSettingsTabViewModel.cs @@ -29,7 +29,7 @@ public LocationSettingsTabViewModel(SettingsService settingsService) : base(settingsService, 1, "Location") { _eventRoot.Add( - this.WatchProperty(o => o.Location, () => LocationQuery = Location?.ToString()) + this.WatchProperty(o => o.Location, () => LocationQuery = Location?.ToString(), true) ); } @@ -118,9 +118,7 @@ private async Task ResolveLocationAsync() protected override void Dispose(bool disposing) { if (disposing) - { _eventRoot.Dispose(); - } base.Dispose(disposing); } diff --git a/LightBulb/ViewModels/Components/Settings/SettingsTabViewModelBase.cs b/LightBulb/ViewModels/Components/Settings/SettingsTabViewModelBase.cs index d399f50..9b3be30 100644 --- a/LightBulb/ViewModels/Components/Settings/SettingsTabViewModelBase.cs +++ b/LightBulb/ViewModels/Components/Settings/SettingsTabViewModelBase.cs @@ -41,9 +41,7 @@ string displayName protected override void Dispose(bool disposing) { if (disposing) - { _eventRoot.Dispose(); - } base.Dispose(disposing); } diff --git a/LightBulb/ViewModels/MainViewModel.cs b/LightBulb/ViewModels/MainViewModel.cs index 0b1e249..4227770 100644 --- a/LightBulb/ViewModels/MainViewModel.cs +++ b/LightBulb/ViewModels/MainViewModel.cs @@ -194,9 +194,7 @@ private async Task ShowSettingsAsync() => protected override void Dispose(bool disposing) { if (disposing) - { _checkForUpdatesTimer.Dispose(); - } base.Dispose(disposing); } diff --git a/LightBulb/Views/Components/Settings/ApplicationWhitelistSettingsTabView.axaml.cs b/LightBulb/Views/Components/Settings/ApplicationWhitelistSettingsTabView.axaml.cs index 7054266..5dabc1b 100644 --- a/LightBulb/Views/Components/Settings/ApplicationWhitelistSettingsTabView.axaml.cs +++ b/LightBulb/Views/Components/Settings/ApplicationWhitelistSettingsTabView.axaml.cs @@ -30,7 +30,8 @@ private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) () => WhitelistedApplicationsListBox.SelectedItems = new AvaloniaList( DataContext.WhitelistedApplications ?? [] - ) + ), + true ) ); }