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
19 changes: 19 additions & 0 deletions RadialActions.Tests/Pie/PieSelectionControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ public class PieSelectionControllerTests
new(3, 170),
];

[Theory]
[InlineData(true, false, 1, 2, PieSelectionController.NoSelection)]
[InlineData(true, true, 1, 2, PieSelectionController.NoSelection)]
[InlineData(false, true, 1, 2, 1)]
[InlineData(false, true, PieSelectionController.NoSelection, 2, PieSelectionController.NoSelection)]
[InlineData(false, false, 1, 2, 2)]
[InlineData(false, false, PieSelectionController.NoSelection, PieSelectionController.NoSelection, PieSelectionController.NoSelection)]
public void GetReleaseTriggerIndex_FollowsInteractionMode(
bool isDragActive,
bool isKeyboardMode,
int selectedIndex,
int hoveredIndex,
int expectedIndex)
{
var result = PieSelectionController.GetReleaseTriggerIndex(isDragActive, isKeyboardMode, selectedIndex, hoveredIndex);

Assert.Equal(expectedIndex, result);
}

[Theory]
[InlineData(Key.Up, 0)]
[InlineData(Key.Right, 1)]
Expand Down
6 changes: 5 additions & 1 deletion RadialActions/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ private void OnTrayExitMenuItemClick(object sender, RoutedEventArgs e)
private void OnSliceClicked(object sender, SliceClickEventArgs e)
{
Log.Debug($"Slice clicked: {e.Slice.Name}");

// A slice has fired; releasing the still-held hotkey must not fire another one when the menu stays open.
_hotkeyReleasePending = false;

try
{
e.Slice.Execute();
Expand Down Expand Up @@ -323,7 +327,7 @@ private void Window_KeyUp(object sender, KeyEventArgs e)

_hotkeyReleasePending = false;

if (PieMenu.TriggerHoveredSlice())
if (PieMenu.TriggerActiveSlice())
{
Log.Debug("Activation hotkey released over a slice; triggered it");
e.Handled = true;
Expand Down
64 changes: 51 additions & 13 deletions RadialActions/Pie/PieControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
RequestRenderRefresh();
}

protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
{
base.OnDpiChanged(oldDpi, newDpi);

// Snapped geometry and the surface shadow's BitmapCache scale are baked at build-time DPI, and the menu's
// size in DIPs doesn't change when it opens on a different-DPI monitor, so nothing else triggers a rebuild.
RequestRenderRefresh();
}

private void OnLoaded(object sender, RoutedEventArgs e)
{
SystemParameters.StaticPropertyChanged += OnSystemParametersChanged;
Expand Down Expand Up @@ -127,27 +136,34 @@ private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArg

public void ResetInputState()
{
// The visuals survive across opens, so press and drag tracking from the previous open must be discarded
// here or a held button in the next open can resume a drag that was never started there.
_drag = null;
_dragCandidate = null;

EnterMouseInteractionMode(refreshVisualState: true, animate: false);
}

/// <summary>
/// Triggers the slice currently under the mouse, if any.
/// Triggers the active slice: the keyboard-selected slice in keyboard mode, otherwise the slice under the mouse.
/// </summary>
/// <returns>True if a hovered slice was triggered.</returns>
public bool TriggerHoveredSlice()
/// <returns>True if a slice was triggered.</returns>
public bool TriggerActiveSlice()
{
if (_drag != null)
var hoveredIndex = _sliceVisuals.FirstOrDefault(slice => slice.Path.IsMouseOver)?.Index ?? PieSelectionController.NoSelection;
var targetIndex = PieSelectionController.GetReleaseTriggerIndex(
_drag != null,
_interactionMode == InteractionMode.Keyboard,
_selectionController.SelectedIndex,
hoveredIndex);

var targetSlice = _sliceVisuals.FirstOrDefault(slice => slice.Index == targetIndex);
if (targetSlice == null)
{
return false;
}

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

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

Expand Down Expand Up @@ -345,6 +361,9 @@ private void CreatePieMenu()
ActualWidth,
ActualHeight);
_selectionController.Reset();

// Nothing was rendered; keep the refresh pending so the next opportunity (like the next open) retries.
_renderRefreshPending = true;
return;
}

Expand All @@ -368,6 +387,9 @@ private void CreatePieMenu()
Slices?.Count ?? 0,
ActualWidth,
ActualHeight);

// Nothing was rendered; keep the refresh pending so the next opportunity (like the next open) retries.
_renderRefreshPending = true;
return;
}

Expand Down Expand Up @@ -863,9 +885,11 @@ private void CommitReorder(PieSliceVisual sliceVisual, int targetSlot)

// Move the dragged action to the position of the action that was built at the target
// slot; disabled actions keep their relative placement in the collection.
var targetAction = _sliceVisuals.First(visual => visual.Index == targetSlot).Action;
// The commit runs from an animation callback, so a rebuild (settings edit, theme change) may have
// replaced the visuals in the meantime and the target slot may no longer exist.
var targetAction = _sliceVisuals.FirstOrDefault(visual => visual.Index == targetSlot)?.Action;
var fromIndex = Slices.IndexOf(sliceVisual.Action);
var toIndex = Slices.IndexOf(targetAction);
var toIndex = targetAction == null ? -1 : Slices.IndexOf(targetAction);
if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex)
{
Log.Warning(
Expand Down Expand Up @@ -1034,6 +1058,13 @@ private void ApplyBrushColor(SolidColorBrush brush, Color color, bool animate)

private void OnSystemParametersChanged(object sender, PropertyChangedEventArgs e)
{
// SystemEvents can deliver on a worker thread, and the refresh path reads dependency properties.
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(() => OnSystemParametersChanged(sender, e));
return;
}

var propertyName = e.PropertyName;
if (string.IsNullOrEmpty(propertyName)
|| propertyName.Contains("Color", StringComparison.OrdinalIgnoreCase)
Expand All @@ -1047,6 +1078,13 @@ private void OnSystemParametersChanged(object sender, PropertyChangedEventArgs e

private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
// SystemEvents can deliver on a worker thread, and the refresh path reads dependency properties.
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(() => OnUserPreferenceChanged(sender, e));
return;
}

RequestRenderRefresh();
}

Expand Down
15 changes: 15 additions & 0 deletions RadialActions/Pie/PieSelectionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ public void Reset()
SelectedIndex = NoSelection;
}

/// <summary>
/// Decides which slice a hotkey-release flick should trigger, honoring the active interaction mode
/// so the triggered slice always matches the one shown highlighted.
/// </summary>
/// <returns>The slice index to trigger, or <see cref="NoSelection"/> to trigger nothing.</returns>
public static int GetReleaseTriggerIndex(bool isDragActive, bool isKeyboardMode, int selectedIndex, int hoveredIndex)
{
if (isDragActive)
{
return NoSelection;
}

return isKeyboardMode ? selectedIndex : hoveredIndex;
}

public void EnsureSelectionIsValid(IReadOnlyList<Item> items)
{
if (SelectedIndex == NoSelection)
Expand Down