Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions RadialActions.Tests/Hotkeys/HotkeyUtilTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ public void TryParse_InvalidToken_ReturnsFalse()
Assert.False(ok);
}

[Theory]
[InlineData(Key.Space, true)]
[InlineData(Key.LeftCtrl, true)]
[InlineData(Key.RightCtrl, true)]
[InlineData(Key.LeftAlt, true)]
[InlineData(Key.RightAlt, true)]
[InlineData(Key.LeftShift, false)]
[InlineData(Key.LWin, false)]
[InlineData(Key.A, false)]
public void IsHotkeyComponent_CtrlAltSpace_MatchesMainKeyAndModifiers(Key key, bool expected)
{
var isComponent = HotkeyUtil.IsHotkeyComponent(key, ModifierKeys.Control | ModifierKeys.Alt, Key.Space);

Assert.Equal(expected, isComponent);
}

[Fact]
public void IsHotkeyComponent_NoneKey_NeverMatches()
{
Assert.False(HotkeyUtil.IsHotkeyComponent(Key.None, ModifierKeys.None, Key.None));
}

[Theory]
[InlineData(Key.Space, ModifierKeys.Control | ModifierKeys.Alt)]
[InlineData(Key.F12, ModifierKeys.Shift)]
Expand Down
11 changes: 11 additions & 0 deletions RadialActions.Tests/Settings/SettingsSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ public void DeserializeFromJson_ScriptActionMissingRunHidden_DefaultsToFalse()
Assert.False(settings.Actions[0].RunHidden);
}

[Fact]
public void TriggerSliceOnHotkeyRelease_DefaultsToTrueAndRoundTrips()
{
var settings = Settings.DeserializeFromJson("{}");
Assert.True(settings.TriggerSliceOnHotkeyRelease);

settings.TriggerSliceOnHotkeyRelease = false;
var loaded = Settings.DeserializeFromJson(settings.SerializeToJson());
Assert.False(loaded.TriggerSliceOnHotkeyRelease);
}

[Fact]
public void DeserializeFromJson_MissingIsEnabled_DefaultsToTrue()
{
Expand Down
7 changes: 6 additions & 1 deletion RadialActions/Hotkeys/HotkeyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public class HotkeyManager : IDisposable
private const uint ModControl = 0x0002;
private const uint ModShift = 0x0004;
private const uint ModWin = 0x0008;

/// <summary>
/// Prevents keyboard autorepeat from firing extra WM_HOTKEY messages while the hotkey is held.
/// </summary>
private const uint ModNoRepeat = 0x4000;
private int _currentId;
private readonly Dictionary<string, int> _hotkeys = new(StringComparer.OrdinalIgnoreCase);
private bool _disposed;
Expand Down Expand Up @@ -74,7 +79,7 @@ public bool RegisterHotkey(string hotkey)

var id = ++_currentId;

if (RegisterHotKey(_windowHandle, id, ToModifierFlags(modifiers), keyCode))
if (RegisterHotKey(_windowHandle, id, ToModifierFlags(modifiers) | ModNoRepeat, keyCode))
{
_hotkeys[hotkey] = id;
Log.Information($"Registered hotkey: {hotkey} (ID: {id})");
Expand Down
18 changes: 18 additions & 0 deletions RadialActions/Hotkeys/HotkeyUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ public static bool TryParse(string hotkey, out ModifierKeys modifiers, out Key k
return key != Key.None;
}

/// <summary>
/// Returns whether a key is part of a hotkey combination, either as its main key or as one of its modifiers.
/// </summary>
public static bool IsHotkeyComponent(Key key, ModifierKeys modifiers, Key hotkeyKey)
{
if (key == hotkeyKey && key != Key.None)
return true;

return key switch
{
Key.LeftCtrl or Key.RightCtrl => modifiers.HasFlag(ModifierKeys.Control),
Key.LeftAlt or Key.RightAlt => modifiers.HasFlag(ModifierKeys.Alt),
Key.LeftShift or Key.RightShift => modifiers.HasFlag(ModifierKeys.Shift),
Key.LWin or Key.RWin => modifiers.HasFlag(ModifierKeys.Windows),
_ => false
};
}

public static string BuildHotkeyString(Key key, ModifierKeys modifiers)
{
if (key == Key.None)
Expand Down
1 change: 1 addition & 0 deletions RadialActions/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
d:DataContext="{d:DesignInstance Type=local:MainWindow}"
AllowsTransparency="True" Background="Transparent"
Deactivated="Window_Deactivated" PreviewKeyDown="Window_KeyDown"
PreviewKeyUp="Window_KeyUp"
Loaded="Window_Loaded" Opacity="0"
ResizeMode="NoResize" ShowActivated="False" ShowInTaskbar="False"
SizeToContent="WidthAndHeight"
Expand Down
51 changes: 44 additions & 7 deletions RadialActions/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public partial class MainWindow : Window
private readonly HotkeyService _hotkeyService = new();
private readonly MenuService _menuService;

/// <summary>
/// True while the menu was opened by the activation hotkey and the keys have not been released
/// yet, so releasing them over a slice triggers it (flick gesture).
/// </summary>
private bool _hotkeyReleasePending;

public MainWindow()
{
InitializeComponent();
Expand Down Expand Up @@ -96,22 +102,25 @@ public void Exit()

public void ShowMenu(bool atCursor)
{
_hotkeyReleasePending = false;
_menuService.ShowMenu(atCursor);
}

public void HideMenu(bool animate = true)
{
_hotkeyReleasePending = false;
_menuService.HideMenu(animate);
}

private void ShowMenuUsingConfiguredPosition()
{
_hotkeyReleasePending = false;
_menuService.ShowMenu(!Settings.Default.OpenMenuInScreenCenter);
}

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
_menuService.HideMenu(animate: false);
HideMenu(animate: false);

var handle = new WindowInteropHelper(this).Handle;
_hotkeyService.Initialize(handle, OnHotkeyPressed);
Expand All @@ -137,11 +146,12 @@ private void OnHotkeyPressed(object sender, EventArgs e)

if (IsActive)
{
_menuService.HideMenu();
HideMenu();
}
else
{
ShowMenuUsingConfiguredPosition();
_hotkeyReleasePending = Settings.Default.TriggerSliceOnHotkeyRelease;
}
}

Expand Down Expand Up @@ -223,7 +233,7 @@ private void OnSliceClicked(object sender, SliceClickEventArgs e)

if (!Settings.Default.KeepMenuOpenAfterSliceClick)
{
_menuService.HideMenu();
HideMenu();
}
}

Expand All @@ -239,7 +249,7 @@ private void OnSlicesReordered(object sender, EventArgs e)
private void OnCenterClicked(object sender, EventArgs e)
{
Log.Debug("Center close target clicked");
_menuService.HideMenu();
HideMenu();
}

private void OnCenterContextMenuRequested(object sender, EventArgs e)
Expand All @@ -263,21 +273,21 @@ private void OnSliceEditRequested(object sender, SliceClickEventArgs e)
OpenSettingsWindow(1);
var settingsWindow = Application.Current.Windows.OfType<SettingsWindow>().FirstOrDefault();
settingsWindow?.SelectAction(e.Slice);
_menuService.HideMenu();
HideMenu();
}

private void Window_Deactivated(object sender, EventArgs e)
{
Log.Debug("Lost focus");
_menuService.HideMenu();
HideMenu();
}

private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
Log.Debug("Escape pressed");
_menuService.HideMenu();
HideMenu();
e.Handled = true;
return;
}
Expand All @@ -297,6 +307,33 @@ private void Window_KeyDown(object sender, KeyEventArgs e)
}
}

private void Window_KeyUp(object sender, KeyEventArgs e)
{
if (!_hotkeyReleasePending)
{
return;
}

var key = e.Key == Key.System ? e.SystemKey : e.Key;
if (!HotkeyUtil.TryParse(Settings.Default.ActivationHotkey, out var modifiers, out var hotkeyKey)
|| !HotkeyUtil.IsHotkeyComponent(key, modifiers, hotkeyKey))
{
return;
}

_hotkeyReleasePending = false;

if (PieMenu.TriggerHoveredSlice())
{
Log.Debug("Activation hotkey released over a slice; triggered it");
e.Handled = true;
}
else
{
Log.Debug("Activation hotkey released with no slice hovered; menu stays open");
}
}

private void FadeOutStoryboard_Completed(object sender, EventArgs e)
{
_menuService.OnFadeOutCompleted();
Expand Down
21 changes: 21 additions & 0 deletions RadialActions/Pie/PieControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ public void ResetInputState()
EnterMouseInteractionMode(refreshVisualState: true, animate: false);
}

/// <summary>
/// Triggers the slice currently under the mouse, if any.
/// </summary>
/// <returns>True if a hovered slice was triggered.</returns>
public bool TriggerHoveredSlice()
{
if (_drag != null)
{
return false;
}

var hoveredSlice = _sliceVisuals.FirstOrDefault(slice => slice.Path.IsMouseOver);
if (hoveredSlice == null)
{
return false;
}

SliceClicked?.Invoke(this, new SliceClickEventArgs(hoveredSlice.Action));
return true;
}

public bool HandleMenuKey(Key key, ModifierKeys modifiers)
{
switch (key)
Expand Down
6 changes: 6 additions & 0 deletions RadialActions/Properties/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public sealed partial class Settings
[ObservableProperty]
private bool _keepMenuOpenAfterSliceClick;

/// <summary>
/// Triggers the hovered slice when the held activation hotkey is released.
/// </summary>
[ObservableProperty]
private bool _triggerSliceOnHotkeyRelease = true;

/// <summary>
/// Opens the menu at the center of the current screen instead of the cursor.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions RadialActions/Settings/GeneralSettingsView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@
</Grid>
</Border>

<Border Style="{StaticResource SettingsCardRowBorder}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="&#xE7C9;"
Style="{StaticResource SettingsCardIconTextBlock}" />
<StackPanel Grid.Column="1"
VerticalAlignment="Center">
<TextBlock Text="Release hotkey to trigger"
Style="{StaticResource SettingsCardTitleTextBlock}" />
<TextBlock Text="Hold the hotkey, glide onto a slice, and release to run it in one motion."
Style="{StaticResource SettingsCardDescriptionTextBlock}" />
</StackPanel>
<CheckBox Grid.Column="2"
MinWidth="0"
Margin="16,0,0,0"
VerticalAlignment="Center"
IsChecked="{Binding Settings.TriggerSliceOnHotkeyRelease, Mode=TwoWay}" />
</Grid>
</Border>

<Border Style="{StaticResource SettingsCardRowBorder}">
<Grid>
<Grid.ColumnDefinitions>
Expand Down