Skip to content
Open
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
40 changes: 40 additions & 0 deletions src/Android/Avalonia.Android/Platform/AndroidPlatformFeedback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Android.Content;
using Android.Media;
using Android.Views;
using Avalonia.Controls.Platform;

namespace Avalonia.Android.Platform
{
internal class AndroidPlatformFeedback(View view) : IPlatformFeedback
{
public bool Perform(FeedbackEffect feedback, FeedbackType type)
{
var playSound = type != FeedbackType.Haptic;
var vibrate = type != FeedbackType.Sound;

switch (feedback)
{
case FeedbackEffect.Click:
if (playSound)
{
(view.Context?.GetSystemService(Context.AudioService) as AudioManager)?.PlaySoundEffect(SoundEffect.KeyClick);
}
if (vibrate)
{
view.PerformHapticFeedback(FeedbackConstants.ContextClick);
}
break;
case FeedbackEffect.LongPress:
if (vibrate)
{
view.PerformHapticFeedback(FeedbackConstants.LongPress);
}
break;
default:
break;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Platform.Surfaces;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Surfaces;
using Avalonia.Rendering.Composition;
using Java.Lang;
using ClipboardManager = Android.Content.ClipboardManager;
Expand All @@ -39,6 +39,7 @@ class TopLevelImpl : ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSur
private readonly Clipboard _clipboard;
private readonly AndroidLauncher? _launcher;
private readonly AndroidScreens? _screens;
private readonly AndroidPlatformFeedback _feedback;
private SurfaceViewImpl _view;
private WindowTransparencyLevel _transparencyLevel;

Expand All @@ -57,6 +58,7 @@ public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>(),
context));
_screens = new AndroidScreens(context);
_feedback = new AndroidPlatformFeedback(avaloniaView);

if (context is Activity mainActivity)
{
Expand Down Expand Up @@ -355,6 +357,11 @@ public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> tran
{
return _screens;
}

if(featureType == typeof(IPlatformFeedback))
{
return _feedback;
}

return null;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Avalonia.Controls/Avalonia.Controls.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32.Automation, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LinuxFramebuffer, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Controls/Button.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
Expand Down Expand Up @@ -105,6 +105,7 @@ public class Button : ContentControl, ICommandSource, IClickableControl
static Button()
{
FocusableProperty.OverrideDefaultValue(typeof(Button), true);
PlatformFeedback.FeedbackTypeProperty.OverrideDefaultValue(typeof(Button), FeedbackType.Auto);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>(OnAccessKeyPressed);
}

Expand Down Expand Up @@ -355,6 +356,8 @@ protected virtual void OnClick()
OpenFlyout();
}

this.PerformFeedback(FeedbackEffect.Click);

var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);

Expand Down
3 changes: 3 additions & 0 deletions src/Avalonia.Controls/ComboBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
Expand Down Expand Up @@ -122,6 +123,7 @@ static ComboBox()
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<ComboBox>(true);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
PlatformFeedback.FeedbackTypeProperty.OverrideDefaultValue<ComboBox>(FeedbackType.Auto);
}

/// <summary>
Expand Down Expand Up @@ -365,6 +367,7 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e)
if (_popup?.IsInsidePopup(source) != true && PseudoClasses.Contains(pcPressed))
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
this.PerformFeedback(FeedbackEffect.Click);
e.Handled = true;
}
}
Expand Down
19 changes: 15 additions & 4 deletions src/Avalonia.Controls/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Rendering;
Expand Down Expand Up @@ -37,7 +37,7 @@ public class Control : InputElement, IDataTemplateHost, IVisualBrushInitialize,
/// </summary>
public static readonly StyledProperty<object?> TagProperty =
AvaloniaProperty.Register<Control, object?>(nameof(Tag));

/// <summary>
/// Defines the <see cref="ContextMenu"/> property.
/// </summary>
Expand All @@ -57,7 +57,7 @@ public class Control : InputElement, IDataTemplateHost, IVisualBrushInitialize,
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>(
"RequestBringIntoView",
RoutingStrategies.Bubble);

/// <summary>
/// Defines the <see cref="Loaded"/> event.
/// </summary>
Expand Down Expand Up @@ -100,6 +100,11 @@ public class Control : InputElement, IDataTemplateHost, IVisualBrushInitialize,
private Control? _focusAdorner;
private AutomationPeer? _automationPeer;

static Control()
{
InputElement.HoldingEvent.AddClassHandler<Control>(OnFeedbackHoldEventHandler, handledEventsToo: true);
}

/// <summary>
/// Gets or sets the control's focus adorner.
/// </summary>
Expand Down Expand Up @@ -207,6 +212,12 @@ void ISetterValue.Initialize(SetterBase setter)
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}

private static void OnFeedbackHoldEventHandler(Control control, HoldingRoutedEventArgs args)
{
if (args.Handled && args.HoldingState == HoldingState.Started)
control.PerformFeedback(FeedbackEffect.LongPress);
}

/// <inheritdoc/>
void IVisualBrushInitialize.EnsureInitialized()
{
Expand Down Expand Up @@ -532,7 +543,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
}
}
}

/// <inheritdoc />
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Avalonia.Controls/ListBoxItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
Expand Down Expand Up @@ -29,6 +30,7 @@ static ListBoxItem()
PressedMixin.Attach<ListBoxItem>();
FocusableProperty.OverrideDefaultValue<ListBoxItem>(true);
AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<ListBoxItem>(IsOffscreenBehavior.FromClip);
PlatformFeedback.FeedbackTypeProperty.OverrideDefaultValue<ListBoxItem>(FeedbackType.Auto);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Avalonia.Controls/MenuItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ static MenuItem()
SubmenuOpenedEvent.AddClassHandler<MenuItem>((x, e) => x.OnSubmenuOpened(e));
AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<MenuItem>(IsOffscreenBehavior.FromClip);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(OnAccessKeyPressed);
PlatformFeedback.FeedbackTypeProperty.OverrideDefaultValue<MenuItem>(FeedbackType.Auto);
}

public MenuItem()
Expand Down
57 changes: 57 additions & 0 deletions src/Avalonia.Controls/Platform/IPlatformFeedback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Avalonia.Metadata;

namespace Avalonia.Controls.Platform
{
internal interface IPlatformFeedback
{
/// <summary>
/// Performs the specified <see cref="FeedbackType"/> on the platform.
/// </summary>
/// <param name="feedback">The feedback type to perform.</param>
/// <param name="type">The feedback effect relating to the action that triggered it</param>
/// <returns>true if the platform performed the requested feedback; false otherwise.</returns>
bool Perform(FeedbackEffect feedback, FeedbackType type);
}

/// <summary>
/// The feedback type to be triggered for the attached control.
/// </summary>
public enum FeedbackType
{
/// <summary>
/// Disables feedback for the attached control
/// </summary>
None,

/// <summary>
/// Triggers the most suitable feedback type based on the feedback effect on the current platform.
/// </summary>
Auto,

/// <summary>
/// If available, triggers only sound feedback for the attached control
/// </summary>
Sound,

/// <summary>
/// If available, triggers only haptic feedback for the attached control
/// </summary>
Haptic
}

/// <summary>
/// Predefined platform feedback effect.
/// </summary>
public enum FeedbackEffect
{
/// <summary>
/// The feedback is related to the Click action
/// </summary>
Click,

/// <summary>
/// The feedback is related to the Hold action
/// </summary>
LongPress,
}
}
51 changes: 51 additions & 0 deletions src/Avalonia.Controls/Platform/PlatformFeedback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Avalonia.Input;

namespace Avalonia.Controls.Platform
{
public class PlatformFeedback
{
/// <summary>
/// Defines the FeedbackType attached property.
/// </summary>
public static readonly AttachedProperty<FeedbackType> FeedbackTypeProperty =
AvaloniaProperty.RegisterAttached<PlatformFeedback, InputElement, FeedbackType>("FeedbackType", defaultValue: FeedbackType.None);

/// <summary>
/// Sets the value of the attached FeedbackType property.
/// </summary>
/// <param name="control">The attached control</param>
/// <param name="feedbackType">The feedback type</param>
public static void SetFeedbackType(InputElement control, FeedbackType feedbackType)
{
control.SetValue(FeedbackTypeProperty, feedbackType);
}

/// <summary>
/// Gets the value of the attached FeedbackType property.
/// </summary>
/// <param name="control">The feedback type</param>
/// <returns></returns>
public static FeedbackType GetFeedbackType(InputElement control)
{
return control.GetValue(FeedbackTypeProperty);
}
}

public static class PlatformFeedbackExtensions
{
/// <summary>
/// Performs the specified <see cref="FeedbackEffect"/> on this <see cref="InputElement"/>. The type of feedback to perform is defined in the <see cref="PlatformFeedback.FeedbackTypeProperty"/>
/// </summary>
/// <param name="inputElement">The element to trigger the feedback effect on</param>
/// <param name="feedbackEffect">The feedback effect relating to the action that triggered it</param>
public static void PerformFeedback(this InputElement inputElement, FeedbackEffect feedbackEffect)
{
var feedback = PlatformFeedback.GetFeedbackType(inputElement);
if (feedback != FeedbackType.None &&
TopLevel.GetTopLevel(inputElement)?.PlatformImpl?.TryGetFeature<IPlatformFeedback>() is { } platformFeedBack)
{
platformFeedBack.Perform(feedbackEffect, feedback);
}
}
}
}
7 changes: 6 additions & 1 deletion src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Threading;
Expand Down Expand Up @@ -903,6 +903,11 @@ public virtual bool UpdateSelectionFromEvent(Control container, RoutedEventArgs
eventArgs is PointerEventArgs { Properties.IsRightButtonPressed: true },
eventArgs is FocusChangedEventArgs);

if(eventArgs is PointerEventArgs)
{
container.PerformFeedback(FeedbackEffect.Click);
}

eventArgs.Handled = true;
return true;

Expand Down
4 changes: 3 additions & 1 deletion src/Avalonia.Controls/TextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
Expand Down Expand Up @@ -386,7 +387,8 @@ public UndoRedoState(string? text, int caretPosition)

static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
FocusableProperty.OverrideDefaultValue<TextBox>(true);
PlatformFeedback.FeedbackTypeProperty.OverrideDefaultValue<TextBox>(FeedbackType.Auto);
TextInputMethodClientRequestedEvent.AddClassHandler<TextBox>((tb, e) =>
{
if (!tb.IsReadOnly)
Expand Down
Loading