Skip to content

Commit

Permalink
Fix hotkeys not registering properly on startup (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz authored Jul 18, 2024
1 parent d1d8de8 commit 9f5930f
Show file tree
Hide file tree
Showing 12 changed files with 82 additions and 42 deletions.
8 changes: 8 additions & 0 deletions LightBulb.PlatformInterop/Internal/NativeModule.cs
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 2 additions & 6 deletions LightBulb.PlatformInterop/Internal/WndClassEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
14 changes: 10 additions & 4 deletions LightBulb.PlatformInterop/Internal/WndProcSponge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}.");
}
}
Expand Down Expand Up @@ -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)
{
Expand All @@ -84,7 +90,7 @@ internal partial class WndProcSponge
0,
0,
0,
0,
-3, // HWND_MESSAGE
0,
0,
0
Expand All @@ -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;
}

Expand Down
19 changes: 11 additions & 8 deletions LightBulb/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ public App()
};
InitializeTheme();
},
false
}
)
);

Expand All @@ -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) { }
}
)
);
}
Expand Down
2 changes: 1 addition & 1 deletion LightBulb/Services/HotKeyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
48 changes: 37 additions & 11 deletions LightBulb/Utils/Extensions/NotifyPropertyChangedExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ public static IDisposable WatchProperty<TOwner, TProperty>(
this TOwner owner,
Expression<Func<TOwner, TProperty>> 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.");

Expand All @@ -48,21 +44,51 @@ public static IDisposable WatchProperties<TOwner>(
this TOwner owner,
IReadOnlyList<Expression<Func<TOwner, object?>>> 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<TOwner>(
this TOwner owner,
Action callback,
bool watchInitialValues = true
bool watchInitialValues = false
)
where TOwner : INotifyPropertyChanged
{
Expand Down
12 changes: 10 additions & 2 deletions LightBulb/ViewModels/Components/DashboardViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LightBulb.Core;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -371,8 +381,6 @@ private void Initialize()
_updateConfigurationTimer.Start();
_updateIsPausedTimer.Start();

RegisterHotKeys();

// Hack: feign property changes to refresh the tray icon
OnAllPropertiesChanged();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ private void RefreshApplications()
protected override void Dispose(bool disposing)
{
if (disposing)
{
_eventRoot.Dispose();
}

base.Dispose(disposing);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -118,9 +118,7 @@ private async Task ResolveLocationAsync()
protected override void Dispose(bool disposing)
{
if (disposing)
{
_eventRoot.Dispose();
}

base.Dispose(disposing);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ string displayName
protected override void Dispose(bool disposing)
{
if (disposing)
{
_eventRoot.Dispose();
}

base.Dispose(disposing);
}
Expand Down
2 changes: 0 additions & 2 deletions LightBulb/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,7 @@ private async Task ShowSettingsAsync() =>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_checkForUpdatesTimer.Dispose();
}

base.Dispose(disposing);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ private void UserControl_OnLoaded(object? sender, RoutedEventArgs args)
() =>
WhitelistedApplicationsListBox.SelectedItems = new AvaloniaList<object>(
DataContext.WhitelistedApplications ?? []
)
),
true
)
);
}
Expand Down

0 comments on commit 9f5930f

Please sign in to comment.