diff --git a/RadialActions.Tests/Actions/ActionsSettingsViewModelTests.cs b/RadialActions.Tests/Actions/ActionsSettingsViewModelTests.cs
index 8307ac3..23d2559 100644
--- a/RadialActions.Tests/Actions/ActionsSettingsViewModelTests.cs
+++ b/RadialActions.Tests/Actions/ActionsSettingsViewModelTests.cs
@@ -18,7 +18,6 @@ public void SettingsWindowViewModel_SelectAction_SelectsActionTabAndForwardsSele
Assert.Equal(1, settings.SettingsTabIndex);
Assert.Same(second, viewModel.Actions.SelectedAction);
- Assert.Equal(1, viewModel.Actions.SelectedActionIndex);
}
[Fact]
@@ -29,7 +28,6 @@ public void Constructor_SelectsFirstAction()
var viewModel = CreateViewModel(first, second);
Assert.Same(first, viewModel.SelectedAction);
- Assert.Equal(0, viewModel.SelectedActionIndex);
Assert.Same(first, viewModel.Editor.SelectedAction);
}
@@ -43,30 +41,55 @@ public void SelectAction_SelectsMatchingAction()
viewModel.SelectAction(second);
Assert.Same(second, viewModel.SelectedAction);
- Assert.Equal(1, viewModel.SelectedActionIndex);
Assert.Same(second, viewModel.Editor.SelectedAction);
}
[Fact]
- public void AddAction_InsertsBlankActionAfterSelectedActionAndSelectsIt()
+ public void AddAction_AppendsNewActionAtEndAndSelectsIt()
{
var first = PieAction.CreateKeyAction("Mute");
var second = PieAction.CreateKeyAction("VolumeUp");
- var third = PieAction.CreateKeyAction("VolumeDown");
- var viewModel = CreateViewModel(first, second, third);
- viewModel.SelectAction(second);
+ var viewModel = CreateViewModel(first, second);
+ viewModel.SelectAction(first);
viewModel.AddActionCommand.Execute(null);
var added = viewModel.Actions[2];
- Assert.Equal([first, second, added, third], viewModel.Actions);
- Assert.Equal("Blank action", added.Name);
+ Assert.Equal([first, second, added], viewModel.Actions);
+ Assert.Equal(PieAction.DefaultName, added.Name);
Assert.Equal(ActionType.None, added.Type);
Assert.Same(added, viewModel.SelectedAction);
- Assert.Equal(2, viewModel.SelectedActionIndex);
Assert.Same(added, viewModel.Editor.SelectedAction);
}
+ [Fact]
+ public void DuplicateAction_InsertsCopyAfterSourceAndSelectsIt()
+ {
+ var first = PieAction.CreateScriptAction("Backup", "Get-Date", icon: "🧹", interpreter: "pwsh.exe", workingDirectory: @"C:\", runHidden: true);
+ first.IsEnabled = false;
+ first.Arguments = "/select";
+ var second = PieAction.CreateKeyAction("Mute");
+ var viewModel = CreateViewModel(first, second);
+ viewModel.SelectAction(first);
+
+ viewModel.DuplicateActionCommand.Execute(null);
+
+ var copy = viewModel.Actions[1];
+ Assert.Equal([first, copy, second], viewModel.Actions);
+ Assert.NotSame(first, copy);
+ Assert.Equal(first.Name, copy.Name);
+ Assert.Equal(first.Icon, copy.Icon);
+ Assert.Equal(first.Type, copy.Type);
+ Assert.Equal(first.IsEnabled, copy.IsEnabled);
+ Assert.Equal(first.Parameter, copy.Parameter);
+ Assert.Equal(first.Arguments, copy.Arguments);
+ Assert.Equal(first.WorkingDirectory, copy.WorkingDirectory);
+ Assert.Equal(first.Script, copy.Script);
+ Assert.Equal(first.RunHidden, copy.RunHidden);
+ Assert.Same(copy, viewModel.SelectedAction);
+ Assert.Same(copy, viewModel.Editor.SelectedAction);
+ }
+
[Fact]
public void AddDroppedTargets_InsertsAfterSelectionAndSelectsLast()
{
@@ -85,7 +108,6 @@ public void AddDroppedTargets_InsertsAfterSelectionAndSelectsLast()
Assert.Equal("https://a.com", addedA.Parameter);
Assert.Equal("https://b.com", addedB.Parameter);
Assert.Same(addedB, viewModel.SelectedAction);
- Assert.Equal(3, viewModel.SelectedActionIndex);
Assert.Same(addedB, viewModel.Editor.SelectedAction);
}
@@ -99,7 +121,6 @@ public void AddDroppedTargets_WithoutSelection_AppendsAtEnd()
var added = Assert.Single(viewModel.Actions);
Assert.Equal("https://a.com", added.Parameter);
Assert.Same(added, viewModel.SelectedAction);
- Assert.Equal(0, viewModel.SelectedActionIndex);
}
[Fact]
@@ -113,7 +134,6 @@ public void AddDroppedTargets_EmptyOrNull_DoesNothing()
Assert.Equal([first], viewModel.Actions);
Assert.Same(first, viewModel.SelectedAction);
- Assert.Equal(0, viewModel.SelectedActionIndex);
}
[Fact]
@@ -170,7 +190,6 @@ public void RemoveAction_SelectsNextAvailableAction()
Assert.Equal([first, third], viewModel.Actions);
Assert.Same(third, viewModel.SelectedAction);
- Assert.Equal(1, viewModel.SelectedActionIndex);
Assert.Same(third, viewModel.Editor.SelectedAction);
}
@@ -184,42 +203,9 @@ public void RemoveAction_ClearsSelectionWhenListBecomesEmpty()
Assert.Empty(viewModel.Actions);
Assert.Null(viewModel.SelectedAction);
- Assert.Equal(-1, viewModel.SelectedActionIndex);
Assert.Null(viewModel.Editor.SelectedAction);
}
- [Fact]
- public void MoveUp_MovesSelectedActionAndKeepsSelection()
- {
- var first = PieAction.CreateKeyAction("Mute");
- var second = PieAction.CreateKeyAction("VolumeUp");
- var third = PieAction.CreateKeyAction("VolumeDown");
- var viewModel = CreateViewModel(first, second, third);
- viewModel.SelectAction(second);
-
- viewModel.MoveUpCommand.Execute(null);
-
- Assert.Equal([second, first, third], viewModel.Actions);
- Assert.Same(second, viewModel.SelectedAction);
- Assert.Equal(0, viewModel.SelectedActionIndex);
- }
-
- [Fact]
- public void MoveDown_MovesSelectedActionAndKeepsSelection()
- {
- var first = PieAction.CreateKeyAction("Mute");
- var second = PieAction.CreateKeyAction("VolumeUp");
- var third = PieAction.CreateKeyAction("VolumeDown");
- var viewModel = CreateViewModel(first, second, third);
- viewModel.SelectAction(second);
-
- viewModel.MoveDownCommand.Execute(null);
-
- Assert.Equal([first, third, second], viewModel.Actions);
- Assert.Same(second, viewModel.SelectedAction);
- Assert.Equal(2, viewModel.SelectedActionIndex);
- }
-
private static ActionsSettingsViewModel CreateViewModel(params PieAction[] actions)
{
return new ActionsSettingsViewModel(CreateSettings(actions));
diff --git a/RadialActions.Tests/Pie/PieReorderCalculatorTests.cs b/RadialActions.Tests/Pie/PieReorderCalculatorTests.cs
index f371dc2..baa652d 100644
--- a/RadialActions.Tests/Pie/PieReorderCalculatorTests.cs
+++ b/RadialActions.Tests/Pie/PieReorderCalculatorTests.cs
@@ -1,4 +1,4 @@
-using System.Windows;
+using System.Windows;
namespace RadialActions.Tests;
@@ -37,6 +37,20 @@ public void GetTargetSlot_MapsRotationToSlot(int originalIndex, double rotationO
Assert.Equal(expectedSlot, slot);
}
+ [Theory]
+ [InlineData(0, 360, 0)] // a full lap over the six-slot editor ring (five slices plus the ghost) lands home
+ [InlineData(4, 60, 5)] // the last slice dragged one slot clockwise lands in the ghost slot, which the control clamps to the last real slot
+ [InlineData(0, -60, 5)] // the first slice dragged counterclockwise wraps into the ghost slot the same way
+ public void GetTargetSlot_WithEditModeGhostSlot_WrapsOverTheFullSlotRing(int originalIndex, double rotationOffset, int expectedSlot)
+ {
+ const double angleStep = 60;
+ const int slotCount = 6;
+
+ var slot = PieReorderCalculator.GetTargetSlot(originalIndex, rotationOffset, angleStep, slotCount);
+
+ Assert.Equal(expectedSlot, slot);
+ }
+
[Theory]
[InlineData(0, 288, -72)] // short way is backward
[InlineData(300, 0, 360)] // short way keeps going forward
diff --git a/RadialActions/Actions/Action.cs b/RadialActions/Actions/Action.cs
index 18186f0..0b6bb95 100644
--- a/RadialActions/Actions/Action.cs
+++ b/RadialActions/Actions/Action.cs
@@ -200,6 +200,22 @@ public static PieAction CreateScriptAction(string name, string script, string ic
RunHidden = runHidden
};
+ ///
+ /// Creates a copy of this action with the same configuration.
+ ///
+ public PieAction Clone() => new()
+ {
+ Name = Name,
+ Icon = Icon,
+ Type = Type,
+ IsEnabled = IsEnabled,
+ Parameter = Parameter,
+ Arguments = Arguments,
+ WorkingDirectory = WorkingDirectory,
+ Script = Script,
+ RunHidden = RunHidden,
+ };
+
///
/// Executes the action.
///
diff --git a/RadialActions/Pie/PieControl.xaml.cs b/RadialActions/Pie/PieControl.xaml.cs
index c893a8f..121cc4e 100644
--- a/RadialActions/Pie/PieControl.xaml.cs
+++ b/RadialActions/Pie/PieControl.xaml.cs
@@ -2,6 +2,7 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
+using System.Windows.Automation.Peers;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
@@ -19,6 +20,7 @@ public partial class PieControl : UserControl
{
private const double DefaultCenterHoleRatio = 0.25;
private const int SliceZIndex = 10;
+ private const int SelectedSliceZIndex = 12;
private const int HoveredSliceZIndex = 14;
private const int SliceContentZIndex = 15;
private const int DraggedSliceZIndex = 16;
@@ -63,10 +65,13 @@ private enum InteractionMode
private bool _hasKeyboardModeMousePosition;
private Point _layoutCenter;
private double _layoutAngleStep;
+ private int _layoutSlotCount;
+ private bool _slicesHandlersAttached;
private PieSliceVisual _dragCandidate;
private Point _dragPressPosition;
private DragReorderState _drag;
private bool _isReleasingDragCapture;
+ private Path _ghostPath;
private sealed class DragReorderState
{
@@ -95,6 +100,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)
{
SystemParameters.StaticPropertyChanged += OnSystemParametersChanged;
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
+ AttachSlicesHandlers();
RequestRenderRefresh();
}
@@ -102,6 +108,9 @@ private void OnUnloaded(object sender, RoutedEventArgs e)
{
SystemParameters.StaticPropertyChanged -= OnSystemParametersChanged;
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
+
+ // Detach so a closed or hidden host (like the settings window) doesn't stay alive through handlers on the app-lifetime actions collection.
+ DetachSlicesHandlers(Slices);
}
private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
@@ -151,6 +160,101 @@ public bool TriggerHoveredSlice()
return true;
}
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+
+ if (!IsEditMode || e.Handled)
+ {
+ return;
+ }
+
+ var key = e.Key == Key.System ? e.SystemKey : e.Key;
+ e.Handled = HandleEditModeKey(key, Keyboard.Modifiers);
+ }
+
+ ///
+ /// Keyboard support for the editor pie: arrows move the selection around the ring, Ctrl+arrows reorder the selected action, Enter or Insert adds one, and Delete removes it.
+ ///
+ private bool HandleEditModeKey(Key key, ModifierKeys modifiers)
+ {
+ switch (key)
+ {
+ case Key.Right:
+ case Key.Down:
+ return modifiers.HasFlag(ModifierKeys.Control) ? MoveSelectedSlice(1) : MoveEditSelection(1);
+ case Key.Left:
+ case Key.Up:
+ return modifiers.HasFlag(ModifierKeys.Control) ? MoveSelectedSlice(-1) : MoveEditSelection(-1);
+ case Key.Return:
+ case Key.Insert:
+ RequestAddSlice();
+ return true;
+ case Key.Delete:
+ if (SelectedSlice == null)
+ {
+ return false;
+ }
+
+ RemoveSliceRequested?.Invoke(this, new SliceClickEventArgs(SelectedSlice));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private bool MoveEditSelection(int delta)
+ {
+ if (_sliceVisuals.Count == 0)
+ {
+ return false;
+ }
+
+ var count = _sliceVisuals.Count;
+ var index = _sliceVisuals.FindIndex(visual => visual.Action == SelectedSlice);
+ index = index < 0
+ ? (delta > 0 ? 0 : count - 1)
+ : (((index + delta) % count) + count) % count;
+ SelectedSlice = _sliceVisuals[index].Action;
+ return true;
+ }
+
+ private bool MoveSelectedSlice(int delta)
+ {
+ if (SelectedSlice == null || Slices == null)
+ {
+ return false;
+ }
+
+ var fromIndex = Slices.IndexOf(SelectedSlice);
+ if (fromIndex < 0 || Slices.Count < 2)
+ {
+ return false;
+ }
+
+ // Wrap like the ring does, so moving past either end carries the slice around.
+ var count = Slices.Count;
+ var toIndex = (((fromIndex + delta) % count) + count) % count;
+ Log.Information("Reordered slice by keyboard: {SliceName} ({FromIndex} -> {ToIndex})", SelectedSlice.Name, fromIndex, toIndex);
+ Slices.Move(fromIndex, toIndex);
+ SlicesReordered?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ internal void RequestAddSlice()
+ {
+ AddSliceRequested?.Invoke(this, EventArgs.Empty);
+ }
+
+ internal IReadOnlyList SliceVisuals => _sliceVisuals;
+
+ internal Path GhostPath => _ghostPath;
+
+ protected override AutomationPeer OnCreateAutomationPeer()
+ {
+ return new PieControlAutomationPeer(this);
+ }
+
public bool HandleMenuKey(Key key, ModifierKeys modifiers)
{
switch (key)
@@ -238,7 +342,7 @@ private void ActivateSelectedSlice()
private bool OpenSelectedSliceContextMenu()
{
var selectedSlice = GetSelectedSliceVisual();
- if (selectedSlice == null)
+ if (selectedSlice?.ContextMenu == null)
{
return false;
}
@@ -267,34 +371,117 @@ public ObservableCollection Slices
set => SetValue(SlicesProperty, value);
}
- private static void OnSlicesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ public static readonly DependencyProperty IsEditModeProperty =
+ DependencyProperty.Register(
+ nameof(IsEditMode),
+ typeof(bool),
+ typeof(PieControl),
+ new PropertyMetadata(false, OnIsEditModePropertyChanged));
+
+ ///
+ /// When true the pie renders as a live editor surface: every action is shown (disabled ones dimmed), a ghost slice at the end adds new actions, clicking selects instead of executing, and the selected slice is highlighted with the accent color.
+ ///
+ public bool IsEditMode
+ {
+ get => (bool)GetValue(IsEditModeProperty);
+ set => SetValue(IsEditModeProperty, value);
+ }
+
+ private static void OnIsEditModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not PieControl control)
+ {
+ return;
+ }
+
+ // The editor is a keyboard-navigable control; the live menu keeps window-level key handling and stays unfocusable.
+ control.Focusable = e.NewValue is true;
+ control.RequestRenderRefresh();
+ }
+
+ public static readonly DependencyProperty SelectedSliceProperty =
+ DependencyProperty.Register(
+ nameof(SelectedSlice),
+ typeof(PieAction),
+ typeof(PieControl),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedSlicePropertyChanged));
+
+ ///
+ /// The action highlighted as selected while is on.
+ ///
+ public PieAction SelectedSlice
+ {
+ get => (PieAction)GetValue(SelectedSliceProperty);
+ set => SetValue(SelectedSliceProperty, value);
+ }
+
+ private static void OnSelectedSlicePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not PieControl control)
{
return;
}
- if (e.OldValue is ObservableCollection oldCollection)
+ control.RefreshVisualState(animate: true);
+
+ if (e.NewValue is PieAction selected
+ && AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected)
+ && UIElementAutomationPeer.FromElement(control) is PieControlAutomationPeer peer)
{
- oldCollection.CollectionChanged -= control.OnSlicesCollectionChanged;
- foreach (var item in oldCollection)
- {
- item.PropertyChanged -= control.OnSlicePropertyChanged;
- }
+ peer.RaiseSelectionEvent(selected);
}
+ }
- if (e.NewValue is ObservableCollection newCollection)
+ private static void OnSlicesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not PieControl control)
{
- newCollection.CollectionChanged += control.OnSlicesCollectionChanged;
- foreach (var item in newCollection)
- {
- item.PropertyChanged += control.OnSlicePropertyChanged;
- }
+ return;
+ }
+
+ control.DetachSlicesHandlers(e.OldValue as ObservableCollection);
+
+ // Wait for Loaded when the binding resolves before the control enters the tree, so unloaded controls never hold handlers on a long-lived collection.
+ if (control.IsLoaded)
+ {
+ control.AttachSlicesHandlers();
}
control.RequestRenderRefresh();
}
+ private void AttachSlicesHandlers()
+ {
+ if (_slicesHandlersAttached || Slices is not ObservableCollection collection)
+ {
+ return;
+ }
+
+ collection.CollectionChanged += OnSlicesCollectionChanged;
+ foreach (var item in collection)
+ {
+ item.PropertyChanged += OnSlicePropertyChanged;
+ }
+
+ _slicesHandlersAttached = true;
+ }
+
+ private void DetachSlicesHandlers(ObservableCollection collection)
+ {
+ if (!_slicesHandlersAttached || collection == null)
+ {
+ return;
+ }
+
+ collection.CollectionChanged -= OnSlicesCollectionChanged;
+ foreach (var item in collection)
+ {
+ item.PropertyChanged -= OnSlicePropertyChanged;
+ }
+
+ _slicesHandlersAttached = false;
+ }
+
private void OnSlicesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems is not null)
@@ -331,16 +518,21 @@ private void CreatePieMenu()
_renderRefreshPending = false;
_drag = null;
_dragCandidate = null;
+ _ghostPath = null;
- var enabledSlices = Slices?
- .Where(slice => slice?.IsEnabled == true)
+ // Edit mode shows every action so hidden ones can still be selected and edited; the live menu only shows enabled ones.
+ var visibleSlices = Slices?
+ .Where(slice => slice != null && (IsEditMode || slice.IsEnabled))
.ToList();
- if (enabledSlices == null || enabledSlices.Count == 0 || ActualWidth <= 0 || ActualHeight <= 0)
+ // The ghost add slice takes up one extra slot in edit mode, so an empty editor still renders a full-circle ghost.
+ var slotCount = (visibleSlices?.Count ?? 0) + (IsEditMode ? 1 : 0);
+
+ if (visibleSlices == null || slotCount == 0 || ActualWidth <= 0 || ActualHeight <= 0)
{
Log.Debug(
- "Skipping pie render (EnabledSlices={EnabledSliceCount}, TotalSlices={TotalSliceCount}, Width={Width}, Height={Height})",
- enabledSlices?.Count ?? 0,
+ "Skipping pie render (VisibleSlices={VisibleSliceCount}, TotalSlices={TotalSliceCount}, Width={Width}, Height={Height})",
+ visibleSlices?.Count ?? 0,
Slices?.Count ?? 0,
ActualWidth,
ActualHeight);
@@ -356,15 +548,15 @@ private void CreatePieMenu()
if (!PieLayoutCalculator.TryCreateLayout(
ActualWidth,
ActualHeight,
- enabledSlices.Count,
+ slotCount,
DefaultCenterHoleRatio,
theme.SliceStrokeThickness,
SnapToDevicePixel,
out var layout))
{
Log.Warning(
- "Failed to create pie layout (EnabledSlices={EnabledSliceCount}, TotalSlices={TotalSliceCount}, Width={Width}, Height={Height})",
- enabledSlices.Count,
+ "Failed to create pie layout (VisibleSlices={VisibleSliceCount}, TotalSlices={TotalSliceCount}, Width={Width}, Height={Height})",
+ visibleSlices.Count,
Slices?.Count ?? 0,
ActualWidth,
ActualHeight);
@@ -394,6 +586,7 @@ private void CreatePieMenu()
var angleStep = layout.AngleStep;
_layoutCenter = center;
_layoutAngleStep = angleStep;
+ _layoutSlotCount = slotCount;
if (innerRadius > 0)
{
@@ -416,73 +609,22 @@ private void CreatePieMenu()
};
_centerVisual = centerVisual;
- var isCenterMouseDown = false;
-
- centerElements.Target.MouseEnter += (_, _) =>
+ if (IsEditMode)
{
- if (_interactionMode != InteractionMode.Mouse)
- {
- return;
- }
-
- ApplyCenterHoverVisual(animate: true);
- };
-
- centerElements.Target.MouseLeave += (_, _) =>
- {
- if (_interactionMode == InteractionMode.Mouse)
- {
- ApplyCenterNormalVisual(animate: true);
- }
-
- if (!isCenterMouseDown)
- {
- return;
- }
-
- isCenterMouseDown = false;
- _animationService.AnimateClickUp(centerElements.Target, pressDuration, _renderState.StandardEasing);
- };
-
- centerElements.Target.MouseLeftButtonDown += (_, e) =>
- {
- EnterMouseInteractionMode(refreshVisualState: false, animate: false);
- isCenterMouseDown = true;
- _animationService.AnimateClickDown(centerElements.Target, pressDuration, _renderState.StandardEasing);
- e.Handled = true;
- };
-
- centerElements.Target.MouseLeftButtonUp += (_, e) =>
- {
- if (!isCenterMouseDown)
- {
- return;
- }
-
- isCenterMouseDown = false;
- _animationService.AnimateClickUp(centerElements.Target, pressDuration, _renderState.StandardEasing);
- if (_interactionMode == InteractionMode.Mouse)
+ // The hub is inert in edit mode; there is no menu to close.
+ centerElements.Target.Cursor = Cursors.Arrow;
+ foreach (UIElement child in centerElements.Target.Children)
{
- if (centerElements.Target.IsMouseOver)
- {
- ApplyCenterHoverVisual(animate: true);
- }
- else
+ if (child is FrameworkElement childElement)
{
- ApplyCenterNormalVisual(animate: true);
+ childElement.Cursor = Cursors.Arrow;
}
}
-
- CenterClicked?.Invoke(this, EventArgs.Empty);
- e.Handled = true;
- };
-
- centerElements.Target.MouseRightButtonUp += (_, e) =>
+ }
+ else
{
- EnterMouseInteractionMode(refreshVisualState: false, animate: false);
- CenterContextMenuRequested?.Invoke(this, EventArgs.Empty);
- e.Handled = true;
- };
+ WireCenterInteractions(centerElements, pressDuration);
+ }
Canvas.SetLeft(centerElements.Target, SnapToDevicePixel(center.X - innerRadius, isXAxis: true));
Canvas.SetTop(centerElements.Target, SnapToDevicePixel(center.Y - innerRadius, isXAxis: false));
@@ -490,9 +632,9 @@ private void CreatePieMenu()
PieCanvas.Children.Add(centerElements.Target);
}
- for (var i = 0; i < enabledSlices.Count; i++)
+ for (var i = 0; i < visibleSlices.Count; i++)
{
- var sliceAction = enabledSlices[i];
+ var sliceAction = visibleSlices[i];
var startAngle = (i * angleStep) - 90;
var endAngle = startAngle + angleStep;
@@ -527,7 +669,8 @@ private void CreatePieMenu()
var reorderRotation = new RotateTransform(0, center.X, center.Y);
slice.RenderTransform = new TransformGroup { Children = { pressScale, reorderRotation } };
- var contextMenu = CreateSliceContextMenu(sliceAction);
+ // No "Edit..." context menu in edit mode; the slice is already being edited in place.
+ var contextMenu = IsEditMode ? null : CreateSliceContextMenu(sliceAction);
slice.ContextMenu = contextMenu;
var sliceVisual = new PieSliceVisual
@@ -549,6 +692,11 @@ private void CreatePieMenu()
slice.MouseLeftButtonDown += (_, e) =>
{
EnterMouseInteractionMode(refreshVisualState: false, animate: false);
+ if (IsEditMode)
+ {
+ Focus();
+ }
+
isMouseDown = true;
_animationService.AnimateBrushColor(fillBrush, theme.PressedColor, pressDuration, _renderState.StandardEasing);
_animationService.AnimateBrushColor(strokeBrush, _renderState.BorderHoverColor, pressDuration, _renderState.StandardEasing);
@@ -577,6 +725,12 @@ private void CreatePieMenu()
isMouseDown = false;
_animationService.AnimateClickUp(slice, pressDuration, _renderState.StandardEasing);
+
+ if (IsEditMode)
+ {
+ SelectedSlice = sliceAction;
+ }
+
if (_interactionMode == InteractionMode.Mouse)
{
if (slice.IsMouseOver)
@@ -585,7 +739,7 @@ private void CreatePieMenu()
}
else
{
- ApplySliceNormalVisual(sliceVisual, animate: true);
+ ApplySliceRestingVisual(sliceVisual, animate: true);
}
}
else
@@ -616,7 +770,7 @@ private void CreatePieMenu()
if (_interactionMode == InteractionMode.Mouse && _drag == null)
{
- ApplySliceNormalVisual(sliceVisual, animate: true);
+ ApplySliceRestingVisual(sliceVisual, animate: true);
}
if (!isMouseDown)
@@ -627,10 +781,17 @@ private void CreatePieMenu()
isMouseDown = false;
_animationService.AnimateClickUp(slice, pressDuration, _renderState.StandardEasing);
};
+ // Hidden actions render dimmed in edit mode so they can still be selected and edited.
+ if (IsEditMode && !sliceAction.IsEnabled)
+ {
+ slice.Opacity = 0.45;
+ }
+
Panel.SetZIndex(slice, SliceZIndex);
PieCanvas.Children.Add(slice);
- if (i < MaxDigitHints)
+ // Digit hints only matter for triggering slices from the live menu.
+ if (!IsEditMode && i < MaxDigitHints)
{
var digitHint = PieVisualBuilder.CreateSliceDigitHint(
i + 1,
@@ -687,12 +848,28 @@ private void CreatePieMenu()
sliceVisual.ContentCounter = contentCounter;
sliceVisual.ContentOrbit = contentOrbit;
+ if (IsEditMode && !sliceAction.IsEnabled)
+ {
+ contentPanel.Opacity = 0.45;
+ }
+
Panel.SetZIndex(contentPanel, SliceContentZIndex);
PieCanvas.Children.Add(contentPanel);
}
+ if (IsEditMode)
+ {
+ AddGhostSlice(theme, center, outerRadius, innerRadius, visibleSlices.Count, angleStep);
+ }
+
_selectionController.EnsureSelectionIsValid(GetSelectionItems());
+ // The rendered slices back the automation tree in edit mode, so rebuilds must invalidate the cached child peers.
+ if (IsEditMode && UIElementAutomationPeer.FromElement(this) is PieControlAutomationPeer peer)
+ {
+ peer.ResetChildrenCache();
+ }
+
Log.Debug(
"Pie menu rendered with {SliceCount} slices at {CanvasSize}px",
_sliceVisuals.Count,
@@ -700,6 +877,192 @@ private void CreatePieMenu()
RefreshVisualState(animate: false);
}
+ ///
+ /// Adds the dashed "+" slice that follows the real slices in edit mode and adds a new action when clicked.
+ ///
+ private void AddGhostSlice(PieThemeSnapshot theme, Point center, double outerRadius, double innerRadius, int slotIndex, double angleStep)
+ {
+ var startAngle = (slotIndex * angleStep) - 90;
+ var endAngle = startAngle + angleStep;
+
+ // A full-circle arc degenerates (start and end coincide), so an empty editor gets a ring instead of a slice.
+ var ghost = angleStep >= 360
+ ? new Path
+ {
+ Data = new CombinedGeometry(
+ GeometryCombineMode.Exclude,
+ new EllipseGeometry(center, outerRadius, outerRadius),
+ new EllipseGeometry(center, innerRadius, innerRadius)),
+ }
+ : PieLayoutCalculator.CreateSlice(
+ center,
+ outerRadius,
+ innerRadius,
+ startAngle,
+ endAngle,
+ (centerPoint, radius, angle) => PieLayoutCalculator.GetPointOnCircle(centerPoint, radius, angle, SnapPoint));
+ if (theme.SlicePathStyle != null)
+ {
+ ghost.Style = theme.SlicePathStyle;
+ }
+
+ var transparentFill = Color.FromArgb(0, theme.HoverColor.R, theme.HoverColor.G, theme.HoverColor.B);
+ var fillBrush = new SolidColorBrush(transparentFill);
+ var strokeBrush = new SolidColorBrush(theme.BorderColor);
+
+ ghost.Fill = fillBrush;
+ ghost.Stroke = strokeBrush;
+ ghost.StrokeThickness = theme.SliceStrokeThickness;
+ ghost.StrokeDashArray = [4, 3];
+ ghost.Cursor = Cursors.Hand;
+ ghost.SnapsToDevicePixels = true;
+ ghost.ToolTip = "Add an action";
+
+ var pathBounds = ghost.Data.Bounds;
+ ghost.RenderTransform = new ScaleTransform(1, 1)
+ {
+ CenterX = pathBounds.X + (pathBounds.Width / 2),
+ CenterY = pathBounds.Y + (pathBounds.Height / 2),
+ };
+
+ var pressDuration = _renderState.PressDuration;
+ var isMouseDown = false;
+
+ ghost.MouseEnter += (_, _) =>
+ {
+ _animationService.AnimateBrushColor(fillBrush, theme.HoverColor, _renderState.HoverDuration, _renderState.StandardEasing);
+ _animationService.AnimateBrushColor(strokeBrush, theme.AccentColor, _renderState.HoverDuration, _renderState.StandardEasing);
+ };
+
+ ghost.MouseLeave += (_, _) =>
+ {
+ if (isMouseDown)
+ {
+ isMouseDown = false;
+ _animationService.AnimateClickUp(ghost, pressDuration, _renderState.StandardEasing);
+ }
+
+ _animationService.AnimateBrushColor(fillBrush, transparentFill, _renderState.HoverDuration, _renderState.StandardEasing);
+ _animationService.AnimateBrushColor(strokeBrush, theme.BorderColor, _renderState.HoverDuration, _renderState.StandardEasing);
+ };
+
+ ghost.MouseLeftButtonDown += (_, e) =>
+ {
+ Focus();
+ isMouseDown = true;
+ _animationService.AnimateClickDown(ghost, pressDuration, _renderState.StandardEasing);
+ e.Handled = true;
+ };
+
+ ghost.MouseLeftButtonUp += (_, e) =>
+ {
+ if (!isMouseDown)
+ {
+ return;
+ }
+
+ isMouseDown = false;
+ _animationService.AnimateClickUp(ghost, pressDuration, _renderState.StandardEasing);
+ AddSliceRequested?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ };
+
+ _ghostPath = ghost;
+ Panel.SetZIndex(ghost, SliceZIndex);
+ PieCanvas.Children.Add(ghost);
+
+ var textRadius = innerRadius > 0 ? (outerRadius + innerRadius) / 2 : outerRadius * 0.6;
+ var glyphPosition = PieLayoutCalculator.GetTextPosition(center, textRadius, startAngle, endAngle, SnapPoint);
+
+ var addGlyph = new TextBlock
+ {
+ Style = theme.IconTextStyle,
+ Text = "\uE710",
+ FontFamily = new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets"),
+ Foreground = new SolidColorBrush(theme.AccentColor),
+ FontSize = Math.Max(16, outerRadius * 0.1),
+ IsHitTestVisible = false,
+ SnapsToDevicePixels = true,
+ };
+
+ addGlyph.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ Canvas.SetLeft(addGlyph, SnapToDevicePixel(glyphPosition.X - (addGlyph.DesiredSize.Width / 2), isXAxis: true));
+ Canvas.SetTop(addGlyph, SnapToDevicePixel(glyphPosition.Y - (addGlyph.DesiredSize.Height / 2), isXAxis: false));
+ Panel.SetZIndex(addGlyph, SliceContentZIndex);
+ PieCanvas.Children.Add(addGlyph);
+ }
+
+ private void WireCenterInteractions(PieVisualBuilder.CenterElements centerElements, Duration pressDuration)
+ {
+ var isCenterMouseDown = false;
+
+ centerElements.Target.MouseEnter += (_, _) =>
+ {
+ if (_interactionMode != InteractionMode.Mouse)
+ {
+ return;
+ }
+
+ ApplyCenterHoverVisual(animate: true);
+ };
+
+ centerElements.Target.MouseLeave += (_, _) =>
+ {
+ if (_interactionMode == InteractionMode.Mouse)
+ {
+ ApplyCenterNormalVisual(animate: true);
+ }
+
+ if (!isCenterMouseDown)
+ {
+ return;
+ }
+
+ isCenterMouseDown = false;
+ _animationService.AnimateClickUp(centerElements.Target, pressDuration, _renderState.StandardEasing);
+ };
+
+ centerElements.Target.MouseLeftButtonDown += (_, e) =>
+ {
+ EnterMouseInteractionMode(refreshVisualState: false, animate: false);
+ isCenterMouseDown = true;
+ _animationService.AnimateClickDown(centerElements.Target, pressDuration, _renderState.StandardEasing);
+ e.Handled = true;
+ };
+
+ centerElements.Target.MouseLeftButtonUp += (_, e) =>
+ {
+ if (!isCenterMouseDown)
+ {
+ return;
+ }
+
+ isCenterMouseDown = false;
+ _animationService.AnimateClickUp(centerElements.Target, pressDuration, _renderState.StandardEasing);
+ if (_interactionMode == InteractionMode.Mouse)
+ {
+ if (centerElements.Target.IsMouseOver)
+ {
+ ApplyCenterHoverVisual(animate: true);
+ }
+ else
+ {
+ ApplyCenterNormalVisual(animate: true);
+ }
+ }
+
+ CenterClicked?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ };
+
+ centerElements.Target.MouseRightButtonUp += (_, e) =>
+ {
+ EnterMouseInteractionMode(refreshVisualState: false, animate: false);
+ CenterContextMenuRequested?.Invoke(this, EventArgs.Empty);
+ e.Handled = true;
+ };
+ }
+
private void EnterMouseInteractionMode(bool refreshVisualState, bool animate)
{
_interactionMode = InteractionMode.Mouse;
@@ -788,11 +1151,14 @@ private void UpdateDrag(Point position)
SetSliceRotation(drag.Slice, drag.RotationOffset);
- var targetSlot = PieReorderCalculator.GetTargetSlot(
- drag.Slice.Index,
- drag.RotationOffset,
- _layoutAngleStep,
- _sliceVisuals.Count);
+ // Wrap over every layout slot (including the edit-mode ghost) so the angle math matches the geometry, then land ghost-slot drops on the last real slot.
+ var targetSlot = Math.Min(
+ PieReorderCalculator.GetTargetSlot(
+ drag.Slice.Index,
+ drag.RotationOffset,
+ _layoutAngleStep,
+ _layoutSlotCount),
+ _sliceVisuals.Count - 1);
if (targetSlot == drag.TargetSlot)
{
return;
@@ -941,7 +1307,7 @@ private void RefreshVisualState(bool animate)
}
else
{
- ApplySliceNormalVisual(sliceVisual, animate);
+ ApplySliceRestingVisual(sliceVisual, animate);
}
}
@@ -950,7 +1316,7 @@ private void RefreshVisualState(bool animate)
return;
}
- var isCenterHovered = _interactionMode == InteractionMode.Mouse && _centerVisual.Target.IsMouseOver;
+ var isCenterHovered = !IsEditMode && _interactionMode == InteractionMode.Mouse && _centerVisual.Target.IsMouseOver;
if (isCenterHovered)
{
ApplyCenterHoverVisual(animate);
@@ -961,20 +1327,53 @@ private void RefreshVisualState(bool animate)
}
}
+ ///
+ /// Applies the visual a slice returns to when not hovered: the accent selection highlight in edit mode when it is the selected slice, otherwise the normal resting look.
+ ///
+ private void ApplySliceRestingVisual(PieSliceVisual sliceVisual, bool animate)
+ {
+ if (IsSelectedInEditMode(sliceVisual))
+ {
+ ApplySliceSelectedVisual(sliceVisual, animate);
+ }
+ else
+ {
+ ApplySliceNormalVisual(sliceVisual, animate);
+ }
+ }
+
private void ApplySliceNormalVisual(PieSliceVisual sliceVisual, bool animate)
{
Panel.SetZIndex(sliceVisual.Path, SliceZIndex);
+ sliceVisual.Path.StrokeThickness = _renderState.SliceStrokeThickness;
_animationService.ApplyBrushColor(sliceVisual.FillBrush, _renderState.SliceColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
_animationService.ApplyBrushColor(sliceVisual.StrokeBrush, _renderState.BorderColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
}
+ private void ApplySliceSelectedVisual(PieSliceVisual sliceVisual, bool animate)
+ {
+ // Raised above neighbors so the accent stroke is not painted over by their edges.
+ Panel.SetZIndex(sliceVisual.Path, SelectedSliceZIndex);
+ sliceVisual.Path.StrokeThickness = _renderState.SliceStrokeThickness + 1;
+ _animationService.ApplyBrushColor(sliceVisual.FillBrush, _renderState.SelectedFillColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
+ _animationService.ApplyBrushColor(sliceVisual.StrokeBrush, _renderState.AccentColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
+ }
+
private void ApplySliceHoverVisual(PieSliceVisual sliceVisual, bool animate)
{
Panel.SetZIndex(sliceVisual.Path, HoveredSliceZIndex);
+ sliceVisual.Path.StrokeThickness = IsSelectedInEditMode(sliceVisual)
+ ? _renderState.SliceStrokeThickness + 1
+ : _renderState.SliceStrokeThickness;
_animationService.ApplyBrushColor(sliceVisual.FillBrush, _renderState.HoverColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
_animationService.ApplyBrushColor(sliceVisual.StrokeBrush, _renderState.BorderHoverColor, animate, _renderState.HoverDuration, _renderState.StandardEasing);
}
+ private bool IsSelectedInEditMode(PieSliceVisual sliceVisual)
+ {
+ return IsEditMode && SelectedSlice != null && sliceVisual.Action == SelectedSlice;
+ }
+
private void ApplyCenterNormalVisual(bool animate)
{
var centerVisual = _centerVisual;
@@ -1123,6 +1522,16 @@ private double SnapToDevicePixel(double value, bool isXAxis)
/// Occurs when the center target requests the main context menu.
///
public event EventHandler CenterContextMenuRequested;
+
+ ///
+ /// Occurs when the ghost add slice is clicked in edit mode.
+ ///
+ public event EventHandler AddSliceRequested;
+
+ ///
+ /// Occurs when removal of the selected slice is requested by keyboard in edit mode.
+ ///
+ public event EventHandler RemoveSliceRequested;
}
///
diff --git a/RadialActions/Pie/PieControlAutomationPeer.cs b/RadialActions/Pie/PieControlAutomationPeer.cs
new file mode 100644
index 0000000..e530881
--- /dev/null
+++ b/RadialActions/Pie/PieControlAutomationPeer.cs
@@ -0,0 +1,180 @@
+using System.Windows.Automation;
+using System.Windows.Automation.Peers;
+using System.Windows.Automation.Provider;
+using System.Windows.Shapes;
+using System.Windows.Threading;
+
+namespace RadialActions;
+
+///
+/// Exposes the edit-mode pie to UI Automation as a selectable list of actions plus an add button, so screen readers can enumerate and operate the editor. The live menu keeps the default peer behavior.
+///
+internal sealed class PieControlAutomationPeer : UserControlAutomationPeer, ISelectionProvider
+{
+ public PieControlAutomationPeer(PieControl owner)
+ : base(owner)
+ {
+ }
+
+ private PieControl Pie => (PieControl)Owner;
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return Pie.IsEditMode ? AutomationControlType.List : base.GetAutomationControlTypeCore();
+ }
+
+ protected override string GetNameCore()
+ {
+ var name = base.GetNameCore();
+ if (!string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+
+ return Pie.IsEditMode ? "Actions" : string.Empty;
+ }
+
+ protected override List GetChildrenCore()
+ {
+ if (!Pie.IsEditMode)
+ {
+ return base.GetChildrenCore();
+ }
+
+ var children = new List();
+ foreach (var sliceVisual in Pie.SliceVisuals)
+ {
+ children.Add(new PieSliceAutomationPeer(sliceVisual.Path, Pie, sliceVisual.Action));
+ }
+
+ if (Pie.GhostPath != null)
+ {
+ children.Add(new PieGhostSliceAutomationPeer(Pie.GhostPath, Pie));
+ }
+
+ return children;
+ }
+
+ public override object GetPattern(PatternInterface patternInterface)
+ {
+ if (Pie.IsEditMode && patternInterface == PatternInterface.Selection)
+ {
+ return this;
+ }
+
+ return base.GetPattern(patternInterface);
+ }
+
+ public bool CanSelectMultiple => false;
+
+ public bool IsSelectionRequired => false;
+
+ public IRawElementProviderSimple[] GetSelection()
+ {
+ var selected = Pie.SelectedSlice;
+ if (selected == null)
+ {
+ return [];
+ }
+
+ var peer = GetChildrenCore()?.OfType().FirstOrDefault(child => child.Action == selected);
+ return peer == null ? [] : [ProviderFromPeer(peer)];
+ }
+
+ public void RaiseSelectionEvent(PieAction selected)
+ {
+ var peer = GetChildrenCore()?.OfType().FirstOrDefault(child => child.Action == selected);
+ peer?.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected);
+ }
+}
+
+///
+/// A single action slice exposed as a selectable list item.
+///
+internal sealed class PieSliceAutomationPeer : FrameworkElementAutomationPeer, ISelectionItemProvider
+{
+ private readonly PieControl _pie;
+
+ public PieSliceAutomationPeer(Path owner, PieControl pie, PieAction action)
+ : base(owner)
+ {
+ _pie = pie;
+ Action = action;
+ }
+
+ public PieAction Action { get; }
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem;
+
+ protected override string GetClassNameCore() => nameof(PieSliceAutomationPeer);
+
+ protected override string GetNameCore() => Action.Name ?? string.Empty;
+
+ protected override string GetHelpTextCore() => Action.IsEnabled ? Action.Type.ToString() : $"{Action.Type}, hidden from menu";
+
+ protected override bool IsContentElementCore() => true;
+
+ protected override bool IsControlElementCore() => true;
+
+ public override object GetPattern(PatternInterface patternInterface)
+ {
+ return patternInterface == PatternInterface.SelectionItem ? this : base.GetPattern(patternInterface);
+ }
+
+ public bool IsSelected => _pie.SelectedSlice == Action;
+
+ public IRawElementProviderSimple SelectionContainer
+ {
+ get
+ {
+ var containerPeer = UIElementAutomationPeer.FromElement(_pie);
+ return containerPeer == null ? null : ProviderFromPeer(containerPeer);
+ }
+ }
+
+ public void Select()
+ {
+ _pie.Dispatcher.InvokeAsync(() => _pie.SelectedSlice = Action, DispatcherPriority.Input);
+ }
+
+ public void AddToSelection() => Select();
+
+ public void RemoveFromSelection()
+ {
+ // Single-selection list; deselecting an item is not supported.
+ }
+}
+
+///
+/// The ghost add slice exposed as an invokable button.
+///
+internal sealed class PieGhostSliceAutomationPeer : FrameworkElementAutomationPeer, IInvokeProvider
+{
+ private readonly PieControl _pie;
+
+ public PieGhostSliceAutomationPeer(Path owner, PieControl pie)
+ : base(owner)
+ {
+ _pie = pie;
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Button;
+
+ protected override string GetClassNameCore() => nameof(PieGhostSliceAutomationPeer);
+
+ protected override string GetNameCore() => "Add an action";
+
+ protected override bool IsContentElementCore() => true;
+
+ protected override bool IsControlElementCore() => true;
+
+ public override object GetPattern(PatternInterface patternInterface)
+ {
+ return patternInterface == PatternInterface.Invoke ? this : base.GetPattern(patternInterface);
+ }
+
+ public void Invoke()
+ {
+ _pie.Dispatcher.InvokeAsync(_pie.RequestAddSlice, DispatcherPriority.Input);
+ }
+}
diff --git a/RadialActions/Pie/PieRenderState.cs b/RadialActions/Pie/PieRenderState.cs
index 5d97905..b8516c8 100644
--- a/RadialActions/Pie/PieRenderState.cs
+++ b/RadialActions/Pie/PieRenderState.cs
@@ -18,6 +18,9 @@ internal sealed class PieRenderState
public Color HubHoverColor { get; private set; } = SystemColors.ControlLightColor;
public Color HubBorderColor { get; private set; } = SystemColors.ControlDarkColor;
public Color CenterHoverBorderColor { get; private set; } = SystemColors.ControlDarkColor;
+ public Color AccentColor { get; private set; } = SystemColors.HighlightColor;
+ public Color SelectedFillColor { get; private set; } = SystemColors.ControlLightColor;
+ public double SliceStrokeThickness { get; private set; } = 1.5;
public void ApplyTheme(PieThemeSnapshot theme)
{
@@ -32,5 +35,8 @@ public void ApplyTheme(PieThemeSnapshot theme)
HubHoverColor = theme.HubHoverColor;
HubBorderColor = theme.HubBorderColor;
CenterHoverBorderColor = theme.CenterHoverBorderColor;
+ AccentColor = theme.AccentColor;
+ SelectedFillColor = theme.SelectedFillColor;
+ SliceStrokeThickness = theme.SliceStrokeThickness;
}
}
diff --git a/RadialActions/Pie/PieThemeSnapshot.cs b/RadialActions/Pie/PieThemeSnapshot.cs
index ebee1ff..cfc8a78 100644
--- a/RadialActions/Pie/PieThemeSnapshot.cs
+++ b/RadialActions/Pie/PieThemeSnapshot.cs
@@ -36,6 +36,8 @@ internal sealed class PieThemeSnapshot
public required Color CenterHoverBorderColor { get; init; }
public required Color IconTextColor { get; init; }
public required Color LabelTextColor { get; init; }
+ public required Color AccentColor { get; init; }
+ public required Color SelectedFillColor { get; init; }
public static PieThemeSnapshot Capture(
Func tryFindResource,
@@ -161,6 +163,8 @@ static Duration ToDuration(object value, Duration fallbackValue)
labelTextColor = GetAccessibleAccentColor(labelTextColor, sliceColor);
}
+ var selectedFillColor = BlendColor(sliceColor, accentColor, isHighContrast ? 0.5 : 0.18);
+
return new PieThemeSnapshot
{
IsHighContrast = isHighContrast,
@@ -191,6 +195,8 @@ static Duration ToDuration(object value, Duration fallbackValue)
CenterHoverBorderColor = BlendColor(hubBorderColor, accentColor, 0.45),
IconTextColor = iconTextColor,
LabelTextColor = labelTextColor,
+ AccentColor = accentColor,
+ SelectedFillColor = selectedFillColor,
};
}
diff --git a/RadialActions/Settings/ActionEditorView.xaml b/RadialActions/Settings/ActionEditorView.xaml
index 7891956..e4069fc 100644
--- a/RadialActions/Settings/ActionEditorView.xaml
+++ b/RadialActions/Settings/ActionEditorView.xaml
@@ -13,25 +13,50 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
+
+
+
+
+
@@ -51,10 +76,10 @@
-
-
@@ -90,7 +115,7 @@
-
-
@@ -121,14 +146,13 @@
Command="{Binding BrowseOpenTargetCommand}"
Padding="8,2" />
-
-
-
+
@@ -139,14 +163,13 @@
-
-
-
-
+
@@ -181,20 +203,18 @@
-
-
diff --git a/RadialActions/Settings/ActionsSettingsView.xaml b/RadialActions/Settings/ActionsSettingsView.xaml
index 6ff6a88..5c40368 100644
--- a/RadialActions/Settings/ActionsSettingsView.xaml
+++ b/RadialActions/Settings/ActionsSettingsView.xaml
@@ -21,7 +21,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Text="Click the + slice to add your first action.">