Changed behavior to accept keyboard overrides, added keyboard mouse state validator and indirect rules to allow keyboard and mouse navigation
This commit is contained in:
@@ -29,6 +29,12 @@ public static class CardHoverEffectBehavior
|
|||||||
typeof(CardHoverEffectBehavior),
|
typeof(CardHoverEffectBehavior),
|
||||||
0.99);
|
0.99);
|
||||||
|
|
||||||
|
public static readonly AttachedProperty<bool> SuppressPointerHoverProperty =
|
||||||
|
AvaloniaProperty.RegisterAttached<Control, bool>(
|
||||||
|
"SuppressPointerHover",
|
||||||
|
typeof(CardHoverEffectBehavior),
|
||||||
|
false);
|
||||||
|
|
||||||
static CardHoverEffectBehavior()
|
static CardHoverEffectBehavior()
|
||||||
{
|
{
|
||||||
IsEnabledProperty.Changed.AddClassHandler<Control>(OnIsEnabledChanged);
|
IsEnabledProperty.Changed.AddClassHandler<Control>(OnIsEnabledChanged);
|
||||||
@@ -43,10 +49,64 @@ public static class CardHoverEffectBehavior
|
|||||||
public static double GetScaleFactor(AvaloniaObject obj) => obj.GetValue(ScaleFactorProperty);
|
public static double GetScaleFactor(AvaloniaObject obj) => obj.GetValue(ScaleFactorProperty);
|
||||||
public static void SetScaleFactor(AvaloniaObject obj, double value) => obj.SetValue(ScaleFactorProperty, value);
|
public static void SetScaleFactor(AvaloniaObject obj, double value) => obj.SetValue(ScaleFactorProperty, value);
|
||||||
|
|
||||||
|
public static bool GetSuppressPointerHover(AvaloniaObject obj) => obj.GetValue(SuppressPointerHoverProperty);
|
||||||
|
|
||||||
|
public static void SetSuppressPointerHover(AvaloniaObject obj, bool value) => obj.SetValue(SuppressPointerHoverProperty, value);
|
||||||
|
|
||||||
|
public static void ApplyHover(Control element)
|
||||||
|
{
|
||||||
|
element.ZIndex = 10;
|
||||||
|
|
||||||
|
var visual = ElementComposition.GetElementVisual(element);
|
||||||
|
var compositor = visual?.Compositor;
|
||||||
|
|
||||||
|
if (visual is null || compositor is null) return;
|
||||||
|
|
||||||
|
var scaleFactor = (float)GetScaleFactor(element);
|
||||||
|
|
||||||
|
var width = (float)element.Bounds.Width;
|
||||||
|
var height = (float)element.Bounds.Height;
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
|
||||||
|
visual.CenterPoint = new Vector3(width / 2f, height / 2f, 0f);
|
||||||
|
|
||||||
|
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||||
|
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(scaleFactor, scaleFactor, 1f));
|
||||||
|
scaleAnimation.Duration = _animationDuration;
|
||||||
|
|
||||||
|
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
|
||||||
|
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
|
||||||
|
orientationAnimation.Duration = _animationDuration;
|
||||||
|
|
||||||
|
visual.StartAnimation("Scale", scaleAnimation);
|
||||||
|
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResetHover(Control element)
|
||||||
|
{
|
||||||
|
element.ZIndex = 0;
|
||||||
|
|
||||||
|
var visual = ElementComposition.GetElementVisual(element);
|
||||||
|
var compositor = visual?.Compositor;
|
||||||
|
|
||||||
|
if (visual is null || compositor is null) return;
|
||||||
|
|
||||||
|
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||||
|
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1f, 1f, 1f));
|
||||||
|
scaleAnimation.Duration = _animationDuration;
|
||||||
|
|
||||||
|
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
|
||||||
|
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
|
||||||
|
orientationAnimation.Duration = _animationDuration;
|
||||||
|
|
||||||
|
visual.StartAnimation("Scale", scaleAnimation);
|
||||||
|
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
private static void OnIsEnabledChanged(Control c, AvaloniaPropertyChangedEventArgs e)
|
private static void OnIsEnabledChanged(Control c, AvaloniaPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.NewValue is not bool enabled)
|
if (e.NewValue is not bool enabled) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (enabled)
|
if (enabled)
|
||||||
{
|
{
|
||||||
@@ -64,55 +124,30 @@ public static class CardHoverEffectBehavior
|
|||||||
|
|
||||||
private static void Element_PointerEntered(object? sender, PointerEventArgs e)
|
private static void Element_PointerEntered(object? sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Control element)
|
if (sender is not Control element || GetSuppressPointerHover(element)) return;
|
||||||
return;
|
ApplyHover(element);
|
||||||
|
}
|
||||||
|
|
||||||
element.ZIndex = 10;
|
private static void Element_PointerExited(object? sender, PointerEventArgs e)
|
||||||
var visual = ElementComposition.GetElementVisual(element);
|
{
|
||||||
var compositor = visual?.Compositor;
|
if (sender is not Control element || GetSuppressPointerHover(element)) return;
|
||||||
|
ResetHover(element);
|
||||||
if (visual is null || compositor is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var scaleFactor = (float)GetScaleFactor(element);
|
|
||||||
|
|
||||||
var width = (float)element.Bounds.Width;
|
|
||||||
var height = (float)element.Bounds.Height;
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
visual.CenterPoint = new Vector3(width / 2f, height / 2f, 0f);
|
|
||||||
|
|
||||||
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
||||||
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(scaleFactor, scaleFactor, 1f));
|
|
||||||
scaleAnimation.Duration = _animationDuration;
|
|
||||||
|
|
||||||
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
|
|
||||||
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
|
|
||||||
orientationAnimation.Duration = _animationDuration;
|
|
||||||
|
|
||||||
visual.StartAnimation("Scale", scaleAnimation);
|
|
||||||
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Element_PointerMoved(object? sender, PointerEventArgs e)
|
private static void Element_PointerMoved(object? sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Control element)
|
if (sender is not Control element || GetSuppressPointerHover(element)) return;
|
||||||
return;
|
|
||||||
|
|
||||||
var visual = ElementComposition.GetElementVisual(element);
|
var visual = ElementComposition.GetElementVisual(element);
|
||||||
|
|
||||||
if (visual is null)
|
if (visual is null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
var maxRotationAngle = (float)GetMaxRotationAngle(element);
|
var maxRotationAngle = (float)GetMaxRotationAngle(element);
|
||||||
|
|
||||||
var width = element.Bounds.Width;
|
var width = element.Bounds.Width;
|
||||||
var height = element.Bounds.Height;
|
var height = element.Bounds.Height;
|
||||||
|
|
||||||
if (width <= 0 || height <= 0)
|
if (width <= 0 || height <= 0) return;
|
||||||
return;
|
|
||||||
|
|
||||||
var position = e.GetPosition(element);
|
var position = e.GetPosition(element);
|
||||||
var centerX = width / 2.0;
|
var centerX = width / 2.0;
|
||||||
@@ -132,28 +167,4 @@ public static class CardHoverEffectBehavior
|
|||||||
|
|
||||||
visual.Orientation = qy * qx;
|
visual.Orientation = qy * qx;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Element_PointerExited(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not Control element)
|
|
||||||
return;
|
|
||||||
|
|
||||||
element.ZIndex = 0;
|
|
||||||
var visual = ElementComposition.GetElementVisual(element);
|
|
||||||
var compositor = visual?.Compositor;
|
|
||||||
|
|
||||||
if (visual is null || compositor is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
|
||||||
scaleAnimation.InsertKeyFrame(1.0f, new Vector3(1f, 1f, 1f));
|
|
||||||
scaleAnimation.Duration = _animationDuration;
|
|
||||||
|
|
||||||
var orientationAnimation = compositor.CreateQuaternionKeyFrameAnimation();
|
|
||||||
orientationAnimation.InsertKeyFrame(1.0f, Quaternion.Identity);
|
|
||||||
orientationAnimation.Duration = _animationDuration;
|
|
||||||
|
|
||||||
visual.StartAnimation("Scale", scaleAnimation);
|
|
||||||
visual.StartAnimation(nameof(visual.Orientation), orientationAnimation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
public async Task TryFocusImage(int index)
|
public async Task TryFocusImage(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= Photos.Count ) return;
|
if (index < 0 || index >= Photos.Count ) return;
|
||||||
// if(SelectedPhoto != null) ResetSelectedPhoto();
|
|
||||||
SelectedPhoto = Photos[index];
|
SelectedPhoto = Photos[index];
|
||||||
await SelectedPhoto.LoadFullImage();
|
await SelectedPhoto.LoadFullImage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Foreground="#EDEDED"
|
Foreground="#EDEDED"
|
||||||
Margin="5,0,0,0"
|
Margin="5,0,0,0"
|
||||||
|
IsTabStop="False"
|
||||||
|
Focusable="False"
|
||||||
>
|
>
|
||||||
<!--
|
<!--
|
||||||
<MenuItem Header="_File">
|
<MenuItem Header="_File">
|
||||||
@@ -71,8 +73,8 @@
|
|||||||
<MenuItem Header="_Close" Command="{Binding CloseDirectory}"/>
|
<MenuItem Header="_Close" Command="{Binding CloseDirectory}"/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
-->
|
-->
|
||||||
<MenuItem Header="_Open Library" IsVisible="{Binding !PhotosLoaded}" Command="{Binding OpenDirectory}"/>
|
<MenuItem Header="_Open Library" Focusable="False" IsVisible="{Binding !PhotosLoaded}" Command="{Binding OpenDirectory}"/>
|
||||||
<MenuItem Header="_Close Library" IsVisible="{Binding PhotosLoaded}" Command="{Binding CloseDirectory}"/>
|
<MenuItem Header="_Close Library" Focusable="False" IsVisible="{Binding PhotosLoaded}" Command="{Binding CloseDirectory}"/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -114,11 +116,13 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
IsVisible="{Binding PhotosLoaded}">
|
IsVisible="{Binding PhotosLoaded}">
|
||||||
<Border Padding="72,88,72,48">
|
<Border Padding="72,88,72,48">
|
||||||
<ItemsRepeater ItemsSource="{Binding Photos}"
|
<ItemsRepeater x:Name="Repeater"
|
||||||
|
ItemsSource="{Binding Photos}"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
ClipToBounds="False"
|
ClipToBounds="False"
|
||||||
IsTabStop="True">
|
IsTabStop="True"
|
||||||
|
>
|
||||||
<ItemsRepeater.Layout>
|
<ItemsRepeater.Layout>
|
||||||
<UniformGridLayout MinItemWidth="220"
|
<UniformGridLayout MinItemWidth="220"
|
||||||
MinItemHeight="290"
|
MinItemHeight="290"
|
||||||
@@ -136,7 +140,8 @@
|
|||||||
BoxShadow="0 10 24 0 #30000000"
|
BoxShadow="0 10 24 0 #30000000"
|
||||||
anim:CardHoverEffectBehavior.IsEnabled="True"
|
anim:CardHoverEffectBehavior.IsEnabled="True"
|
||||||
anim:CardHoverEffectBehavior.MaxRotationAngle="2.5"
|
anim:CardHoverEffectBehavior.MaxRotationAngle="2.5"
|
||||||
anim:CardHoverEffectBehavior.ScaleFactor="1.08">
|
anim:CardHoverEffectBehavior.ScaleFactor="1.08"
|
||||||
|
>
|
||||||
<StackPanel Orientation="Vertical"
|
<StackPanel Orientation="Vertical"
|
||||||
Spacing="10">
|
Spacing="10">
|
||||||
<i:Interaction.Behaviors>
|
<i:Interaction.Behaviors>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
using Avalonia.Xaml.Interactions.DragAndDrop;
|
using Avalonia.Xaml.Interactions.DragAndDrop;
|
||||||
|
using Photos.Animations;
|
||||||
using Photos.Models;
|
using Photos.Models;
|
||||||
using Photos.ViewModels;
|
using Photos.ViewModels;
|
||||||
|
|
||||||
@@ -9,8 +13,17 @@ namespace Photos.Views;
|
|||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
private enum NavigationInputMode
|
||||||
|
{
|
||||||
|
Mouse,
|
||||||
|
Keyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
private NavigationInputMode _navigationInputMode = NavigationInputMode.Mouse;
|
||||||
|
|
||||||
private IFolderPickerService _folderPickerService;
|
private IFolderPickerService _folderPickerService;
|
||||||
private int _currentImageIndex = 0;
|
private int _currentImageIndex = 0;
|
||||||
|
private Control? _currentHoveredCard;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -24,10 +37,25 @@ public partial class MainWindow : Window
|
|||||||
(DataContext as MainWindowViewModel)?.WindowDropHandler(e);
|
(DataContext as MainWindowViewModel)?.WindowDropHandler(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnKeyDownHandler(object sender, KeyEventArgs e)
|
private async void OnKeyDownHandler(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MainWindowViewModel vm) return;
|
if (DataContext is not MainWindowViewModel vm) return;
|
||||||
|
|
||||||
|
// input state changer
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Enter:
|
||||||
|
case Key.Escape:
|
||||||
|
case Key.Left:
|
||||||
|
case Key.Right:
|
||||||
|
case Key.Up:
|
||||||
|
case Key.Down:
|
||||||
|
_navigationInputMode = NavigationInputMode.Keyboard;
|
||||||
|
UpdatePointerHoverSuppression();
|
||||||
|
UpdateKeyboardHover();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
async Task HandleEnter()
|
async Task HandleEnter()
|
||||||
{
|
{
|
||||||
if (vm.IsPhotoFocused()) vm.UnfocusPhoto();
|
if (vm.IsPhotoFocused()) vm.UnfocusPhoto();
|
||||||
@@ -45,28 +73,98 @@ public partial class MainWindow : Window
|
|||||||
// TODO: move basic selection
|
// TODO: move basic selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int CalculateCurrentIndexHop()
|
||||||
|
{
|
||||||
|
// Values of UniformGridLayout
|
||||||
|
var minItemWidth = 220;
|
||||||
|
var minColumnSpacing = 18;
|
||||||
|
|
||||||
|
var repeaterWidth = Repeater.Bounds.Width;
|
||||||
|
|
||||||
|
var abstractedColumnCount = Math.Floor(repeaterWidth / (minItemWidth + minColumnSpacing));
|
||||||
|
return Math.Max(1, (int)abstractedColumnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleArrowKeyDown()
|
||||||
|
{
|
||||||
|
var nextIdx = CalculateCurrentIndexHop();
|
||||||
|
if (_currentImageIndex + nextIdx >= vm.Photos.Count) _currentImageIndex = vm.Photos.Count - 1;
|
||||||
|
else _currentImageIndex += nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleArrowKeyUp()
|
||||||
|
{
|
||||||
|
var nextIdx = CalculateCurrentIndexHop();
|
||||||
|
if (_currentImageIndex - nextIdx < 0) _currentImageIndex = 0;
|
||||||
|
else _currentImageIndex -= nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Control? FindCardControlForCurrentIndex()
|
||||||
|
{
|
||||||
|
if (_currentImageIndex < 0 || _currentImageIndex >= vm.Photos.Count)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var currentPhoto = vm.Photos[_currentImageIndex];
|
||||||
|
|
||||||
|
return Repeater.GetVisualDescendants()
|
||||||
|
.OfType<Control>()
|
||||||
|
.FirstOrDefault(c =>
|
||||||
|
ReferenceEquals(c.DataContext, currentPhoto) &&
|
||||||
|
CardHoverEffectBehavior.GetIsEnabled(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateKeyboardHover()
|
||||||
|
{
|
||||||
|
if (DataContext is not MainWindowViewModel vm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_currentHoveredCard is not null)
|
||||||
|
{
|
||||||
|
CardHoverEffectBehavior.ResetHover(_currentHoveredCard);
|
||||||
|
_currentHoveredCard = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCard = FindCardControlForCurrentIndex();
|
||||||
|
if (newCard is null) return;
|
||||||
|
|
||||||
|
CardHoverEffectBehavior.ApplyHover(newCard);
|
||||||
|
_currentHoveredCard = newCard;
|
||||||
|
_currentHoveredCard.BringIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.Enter:
|
case Key.Enter: case Key.Space:
|
||||||
HandleEnter().GetAwaiter();
|
if (!vm.PhotosLoaded) await vm.OpenDirectory();
|
||||||
|
else await HandleEnter();
|
||||||
break;
|
break;
|
||||||
case Key.Escape:
|
case Key.Escape:
|
||||||
HandleEscape();
|
HandleEscape();
|
||||||
break;
|
break;
|
||||||
case Key.Down:
|
case Key.Down:
|
||||||
|
HandleArrowKeyDown();
|
||||||
|
UpdateKeyboardHover();
|
||||||
|
await HandleArrowKey();
|
||||||
|
break;
|
||||||
|
case Key.Up:
|
||||||
|
HandleArrowKeyUp();
|
||||||
|
UpdateKeyboardHover();
|
||||||
|
await HandleArrowKey();
|
||||||
break;
|
break;
|
||||||
case Key.Left:
|
case Key.Left:
|
||||||
if (_currentImageIndex > 0)
|
if (_currentImageIndex > 0)
|
||||||
{
|
{
|
||||||
_currentImageIndex--;
|
_currentImageIndex--;
|
||||||
HandleArrowKey().GetAwaiter();
|
UpdateKeyboardHover();
|
||||||
|
await HandleArrowKey();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Key.Right:
|
case Key.Right:
|
||||||
if (_currentImageIndex < vm.Photos.Count - 1)
|
if (_currentImageIndex < vm.Photos.Count - 1)
|
||||||
{
|
{
|
||||||
_currentImageIndex++;
|
_currentImageIndex++;
|
||||||
HandleArrowKey().GetAwaiter();
|
UpdateKeyboardHover();
|
||||||
|
await HandleArrowKey();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -74,9 +172,34 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void PointerEnteredPhotoHandler(object sender, PointerEventArgs e)
|
private void PointerEnteredPhotoHandler(object sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Image i || DataContext is not MainWindowViewModel vm) return;
|
if (sender is not Image image || DataContext is not MainWindowViewModel vm) return;
|
||||||
var idx = vm.GetImageIndex(i);
|
|
||||||
if (idx == -1) return;
|
var photo = image.DataContext as Photo;
|
||||||
|
if (photo is null) return;
|
||||||
|
|
||||||
|
var idx = vm.Photos.IndexOf(photo);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
_navigationInputMode = NavigationInputMode.Mouse;
|
||||||
|
UpdatePointerHoverSuppression();
|
||||||
_currentImageIndex = idx;
|
_currentImageIndex = idx;
|
||||||
|
|
||||||
|
if (_currentHoveredCard is null) return;
|
||||||
|
|
||||||
|
CardHoverEffectBehavior.ResetHover(_currentHoveredCard);
|
||||||
|
_currentHoveredCard = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePointerHoverSuppression()
|
||||||
|
{
|
||||||
|
foreach (var control in Repeater.GetVisualDescendants().OfType<Control>())
|
||||||
|
{
|
||||||
|
if (CardHoverEffectBehavior.GetIsEnabled(control))
|
||||||
|
{
|
||||||
|
CardHoverEffectBehavior.SetSuppressPointerHover(
|
||||||
|
control,
|
||||||
|
_navigationInputMode == NavigationInputMode.Keyboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user