diff --git a/BrickController2/BrickController2/App.xaml b/BrickController2/BrickController2/App.xaml index 771d13baa..9077772bd 100644 --- a/BrickController2/BrickController2/App.xaml +++ b/BrickController2/BrickController2/App.xaml @@ -3,6 +3,7 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:BrickController2.UI.Controls" + xmlns:extensions="clr-namespace:BrickController2.UI.MarkupExtensions" x:Class="BrickController2.App"> @@ -232,6 +233,22 @@ + diff --git a/BrickController2/BrickController2/Resources/TranslationResources.Designer.cs b/BrickController2/BrickController2/Resources/TranslationResources.Designer.cs index a889b9165..6652cecd4 100644 --- a/BrickController2/BrickController2/Resources/TranslationResources.Designer.cs +++ b/BrickController2/BrickController2/Resources/TranslationResources.Designer.cs @@ -114,6 +114,24 @@ internal static string Add { } } + /// + /// Looks up a localized string similar to Add a new controller axis event. + /// + internal static string AddControllerAxisEvent { + get { + return ResourceManager.GetString("AddControllerAxisEvent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add a new controller button event. + /// + internal static string AddControllerButtonEvent { + get { + return ResourceManager.GetString("AddControllerButtonEvent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add a new controller event. /// @@ -690,6 +708,15 @@ internal static string ChooseJsonFileToImport { } } + /// + /// Looks up a localized string similar to Collapse. + /// + internal static string Collapse { + get { + return ResourceManager.GetString("Collapse", resourceCulture); + } + } + /// /// Looks up a localized string similar to Confirm. /// @@ -1050,6 +1077,15 @@ internal static string Executing { } } + /// + /// Looks up a localized string similar to Expand. + /// + internal static string Expand { + get { + return ResourceManager.GetString("Expand", resourceCulture); + } + } + /// /// Looks up a localized string similar to Export the controller profile. /// @@ -1644,6 +1680,15 @@ internal static string PlayVmEnabled { } } + /// + /// Looks up a localized string similar to Press a button on the game controller. + /// + internal static string PressButton { + get { + return ResourceManager.GetString("PressButton", resourceCulture); + } + } + /// /// Looks up a localized string similar to Press a button or move a joy on the game controller. /// @@ -1662,6 +1707,15 @@ internal static string PressButtonsOrMoveJoys { } } + /// + /// Looks up a localized string similar to Move a joystick on the game controller. + /// + internal static string PressOrMoveJoy { + get { + return ResourceManager.GetString("PressOrMoveJoy", resourceCulture); + } + } + /// /// Looks up a localized string similar to Press scan to discover devices. /// diff --git a/BrickController2/BrickController2/Resources/TranslationResources.de.resx b/BrickController2/BrickController2/Resources/TranslationResources.de.resx index b1cb442eb..a10e5e357 100644 --- a/BrickController2/BrickController2/Resources/TranslationResources.de.resx +++ b/BrickController2/BrickController2/Resources/TranslationResources.de.resx @@ -819,4 +819,22 @@ Wiederholbar + + Drücken Sie eine Taste auf dem Controller. + + + Bewegen Sie einen Joystick auf dem Controller. + + + Expandieren + + + Einklappen + + + neues Controller-Achsenereignis erstellen + + + neues Controller-Tastenereignis erstellen + \ No newline at end of file diff --git a/BrickController2/BrickController2/Resources/TranslationResources.hu.resx b/BrickController2/BrickController2/Resources/TranslationResources.hu.resx index 31ab6e04b..d935fc16c 100644 --- a/BrickController2/BrickController2/Resources/TranslationResources.hu.resx +++ b/BrickController2/BrickController2/Resources/TranslationResources.hu.resx @@ -810,4 +810,22 @@ Ismételhető + + Nyomj meg egy gombot a játékvezérlőn + + + Mozgasd a joystickot a játékvezérlőn + + + Bontsa ki + + + Csukja össze + + + Új vezérlőtengely esemény hozzáadása + + + Új kontrollergomb esemény hozzáadása + \ No newline at end of file diff --git a/BrickController2/BrickController2/Resources/TranslationResources.resx b/BrickController2/BrickController2/Resources/TranslationResources.resx index 0c0aec5fc..f21ea1255 100644 --- a/BrickController2/BrickController2/Resources/TranslationResources.resx +++ b/BrickController2/BrickController2/Resources/TranslationResources.resx @@ -819,4 +819,22 @@ Repeatable + + Press a button on the game controller + + + Move a joystick on the game controller + + + Expand + + + Collapse + + + Add a new controller axis event + + + Add a new controller button event + \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Controls/Dialogs.xaml.cs b/BrickController2/BrickController2/UI/Controls/Dialogs.xaml.cs index e3db554ab..9c5428c94 100644 --- a/BrickController2/BrickController2/UI/Controls/Dialogs.xaml.cs +++ b/BrickController2/BrickController2/UI/Controls/Dialogs.xaml.cs @@ -237,7 +237,7 @@ void buttonHandler(object sender, EventArgs args) } } - public async Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token) + public async Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token, Func? eventFilter = null) { GameControllerEventDialogTitle.Text = title ?? string.Empty; GameControllerEventDialogMessage.Text = message ?? string.Empty; @@ -277,6 +277,11 @@ async void inputDeviceEventHandler(object sender, InputDeviceEventArgs args) foreach (var controllerEvent in args.InputDeviceEvents) { + if (eventFilter is not null && !eventFilter(controllerEvent.Key.EventType, controllerEvent.Key.EventCode)) + { + continue; + } + if ((controllerEvent.Key.EventType == InputDeviceEventType.Axis && Math.Abs(controllerEvent.Value) > AXIS_PRESSED_THRESHOLD) || (controllerEvent.Key.EventType == InputDeviceEventType.Button && Math.Abs(controllerEvent.Value) < BUTTON_RELEASED_THRESHOLD)) { diff --git a/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml b/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml new file mode 100644 index 000000000..dbb61e0ee --- /dev/null +++ b/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml.cs b/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml.cs new file mode 100644 index 000000000..3d95c0a69 --- /dev/null +++ b/BrickController2/BrickController2/UI/Controls/ExpandableFloatingActionButton.xaml.cs @@ -0,0 +1,189 @@ +using BrickController2.Helpers; +using Microsoft.Maui; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Threading.Tasks; + +namespace BrickController2.UI.Controls; + +[ContentProperty(nameof(SecondaryButtons))] +public partial class ExpandableFloatingActionButton : ContentView +{ + private bool _isMenuOpen = false; + private int _animationToken; + + public ExpandableFloatingActionButton() + { + InitializeComponent(); + + SyncSecondaryContainer(); + + SecondaryButtons.CollectionChanged += OnSecondaryButtonsChanged; + + ApplyState(); + } + + public ObservableCollection SecondaryButtons { get; } = []; + + public static readonly BindableProperty IconProperty = + BindableProperty.Create(nameof(Icon), typeof(string), typeof(ExpandableFloatingActionButton), "menu", propertyChanged: OnAppearanceChanged); + + public string Icon + { + get => (string)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly BindableProperty IconColorProperty = + BindableProperty.Create(nameof(IconColor), typeof(Color), typeof(ExpandableFloatingActionButton), null, propertyChanged: OnAppearanceChanged); + + public Color IconColor + { + get => (Color)GetValue(IconColorProperty); + set => SetValue(IconColorProperty, value); + } + + public static readonly BindableProperty IconBackgroundColorProperty = + BindableProperty.Create(nameof(IconBackgroundColor), typeof(Color), typeof(ExpandableFloatingActionButton), null, propertyChanged: OnAppearanceChanged); + + public Color? IconBackgroundColor + { + get => (Color?)GetValue(IconBackgroundColorProperty); + set => SetValue(IconBackgroundColorProperty, value); + } + + public static readonly BindableProperty IsExpandedProperty = + BindableProperty.Create(nameof(IsExpanded), typeof(bool), typeof(ExpandableFloatingActionButton), false, propertyChanged: OnAppearanceChanged); + + public bool IsExpanded + { + get => (bool)GetValue(IsExpandedProperty); + private set => SetValue(IsExpandedProperty, value); + } + + public static readonly BindableProperty TooltipKeyProperty = + BindableProperty.Create(nameof(TooltipKey), typeof(string), typeof(ExpandableFloatingActionButton), null, propertyChanged: OnAppearanceChanged); + + /// + /// Translation resource key resolved at apply-time. Use this instead of + /// when the value comes from a style in App.xaml, because markup extensions there are + /// evaluated once at startup and would not reflect the selected language. + /// + public string? TooltipKey + { + get => (string?)GetValue(TooltipKeyProperty); + set => SetValue(TooltipKeyProperty, value); + } + + private static void OnAppearanceChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is ExpandableFloatingActionButton fab) + { + fab.ApplyState(); + } + } + + private void ApplyState() + { + ImageSource.Glyph = Icon; + SetColor(ImageSource, FontImageSource.ColorProperty, IconColor); + SetColor(Button, BackgroundColorProperty, IconBackgroundColor); + + var tooltip = string.IsNullOrEmpty(TooltipKey) ? "" : TranslationHelper.Translate(TooltipKey); + ToolTipProperties.SetText(Button, tooltip); + } + + private static void SetColor(BindableObject target, BindableProperty property, Color? color) + { + if (color is null) + { + // No override provided, fall back to whatever the applied style defines + target.ClearValue(property); + } + else + { + target.SetValue(property, color); + } + } + + private void OnFabClicked(object sender, EventArgs e) + { + _isMenuOpen = !_isMenuOpen; + + if (_isMenuOpen) + { + IsExpanded = true; + } + + AnimateMenu(); + } + + private void OnSecondaryButtonsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + SyncSecondaryContainer(); + } + + private void SyncSecondaryContainer() + { + SecondaryContainer.Children.Clear(); + + foreach (var view in SecondaryButtons) + { + SecondaryContainer.Children.Add(view); + } + } + + private void OnOverlayTapped(object sender, EventArgs e) + { + if (_isMenuOpen) + { + _isMenuOpen = false; + AnimateMenu(); + } + } + + private async void AnimateMenu() + { + var token = ++_animationToken; + + if (_isMenuOpen) + { + // Grow to fill the parent so the dismiss overlay can cover the whole page + HorizontalOptions = LayoutOptions.Fill; + VerticalOptions = LayoutOptions.Fill; + + Overlay.IsVisible = true; + SecondaryContainer.IsVisible = true; + + await Task.WhenAll( + SecondaryContainer.FadeToAsync(1, 500, Easing.CubicOut), + SecondaryContainer.TranslateToAsync(0, 0, 500, Easing.CubicOut), + Button.RotateToAsync(45, 500, Easing.CubicOut) + ); + } + else + { + await Task.WhenAll( + SecondaryContainer.FadeToAsync(0, 500, Easing.CubicIn), + SecondaryContainer.TranslateToAsync(20, 0, 500, Easing.CubicIn), + Button.RotateToAsync(0, 500, Easing.CubicIn) + ); + + if (token != _animationToken || _isMenuOpen) + { + return; + } + + Overlay.IsVisible = false; + SecondaryContainer.IsVisible = false; + IsExpanded = false; + + // Shrink back to the button footprint so the list underneath stays interactive + HorizontalOptions = LayoutOptions.End; + VerticalOptions = LayoutOptions.End; + } + } +} \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Converters/GameControllerEventTypeToImageConverter.cs b/BrickController2/BrickController2/UI/Converters/GameControllerEventTypeToImageConverter.cs index c863dfc0f..dfb0dad03 100644 --- a/BrickController2/BrickController2/UI/Converters/GameControllerEventTypeToImageConverter.cs +++ b/BrickController2/BrickController2/UI/Converters/GameControllerEventTypeToImageConverter.cs @@ -17,7 +17,7 @@ public class GameControllerEventTypeToImageConverter : IValueConverter { return eventType switch { - InputDeviceEventType.Button => "abc", + InputDeviceEventType.Button => "hdr_auto", InputDeviceEventType.Axis => "gamepad", _ => null, }; diff --git a/BrickController2/BrickController2/UI/Pages/ControllerProfilePage.xaml b/BrickController2/BrickController2/UI/Pages/ControllerProfilePage.xaml index 47118299a..092e25b44 100644 --- a/BrickController2/BrickController2/UI/Pages/ControllerProfilePage.xaml +++ b/BrickController2/BrickController2/UI/Pages/ControllerProfilePage.xaml @@ -7,7 +7,9 @@ xmlns:extensions="clr-namespace:BrickController2.UI.MarkupExtensions" xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls" xmlns:local="clr-namespace:BrickController2.UI.Pages" + xmlns:viewmodels="clr-namespace:BrickController2.UI.ViewModels" x:Class="BrickController2.UI.Pages.ControllerProfilePage" + x:DataType="viewmodels:ControllerProfilePageViewModel" x:Name="Page" Title="{extensions:Translate ControllerProfile}" ios:Page.UseSafeArea="True" @@ -49,7 +51,7 @@ - + @@ -79,7 +81,7 @@ - + @@ -121,7 +123,11 @@ - + + + + + diff --git a/BrickController2/BrickController2/UI/Services/Dialog/DialogService.cs b/BrickController2/BrickController2/UI/Services/Dialog/DialogService.cs index 889e67869..90f9eca19 100644 --- a/BrickController2/BrickController2/UI/Services/Dialog/DialogService.cs +++ b/BrickController2/BrickController2/UI/Services/Dialog/DialogService.cs @@ -45,14 +45,11 @@ public Task ShowProgressDialogAsync(bool isDeterministic, return _dialogServer?.ShowProgressDialogAsync(isDeterministic, action, title, message, cancelButtonText, token) ?? Task.FromResult(new ProgressDialogResult(false)); } - public Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token) + public Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token, Func? eventFilter = null) { - if (_dialogServer is not null) - { - _dialogServer.InputDeviceEventService = _gameControllerService; - } + _dialogServer?.InputDeviceEventService = _gameControllerService; - return _dialogServer?.ShowGameControllerEventDialogAsync(title, message, cancelButtonText, token) ?? Task.FromResult(new GameControllerEventDialogResult(false, InputDeviceEventType.Axis, string.Empty)); + return _dialogServer?.ShowGameControllerEventDialogAsync(title, message, cancelButtonText, token, eventFilter) ?? Task.FromResult(new GameControllerEventDialogResult(false, InputDeviceEventType.Axis, string.Empty)); } public void RegisterDialogServer(IDialogServer dialogServer) diff --git a/BrickController2/BrickController2/UI/Services/Dialog/IDialogService.cs b/BrickController2/BrickController2/UI/Services/Dialog/IDialogService.cs index 42eb871db..40609d76d 100644 --- a/BrickController2/BrickController2/UI/Services/Dialog/IDialogService.cs +++ b/BrickController2/BrickController2/UI/Services/Dialog/IDialogService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using BrickController2.PlatformServices.InputDevice; namespace BrickController2.UI.Services.Dialog { @@ -13,6 +14,6 @@ public interface IDialogService Task ShowInputDialogAsync(string initialValue, string placeHolder, string positiveButtonText, string negativeButtonText, KeyboardType keyboardType, Predicate valuePredicate, CancellationToken token); Task> ShowSelectionDialogAsync(IEnumerable items, string title, string cancelButtonText, CancellationToken token) where T : notnull; Task ShowProgressDialogAsync(bool isDeterministic, Func action, string title, string? message = null, string? cancelButtonText = null, CancellationToken token = default); - Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token); + Task ShowGameControllerEventDialogAsync(string title, string message, string cancelButtonText, CancellationToken token, Func? eventFilter = null); } } diff --git a/BrickController2/BrickController2/UI/Themes/DarkTheme.xaml b/BrickController2/BrickController2/UI/Themes/DarkTheme.xaml index 636a34ec1..70bd3d73e 100644 --- a/BrickController2/BrickController2/UI/Themes/DarkTheme.xaml +++ b/BrickController2/BrickController2/UI/Themes/DarkTheme.xaml @@ -41,6 +41,7 @@ Green LightBlue Red + DarkRed Green \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/Themes/LightTheme.xaml b/BrickController2/BrickController2/UI/Themes/LightTheme.xaml index 767008aa6..1bd3b2323 100644 --- a/BrickController2/BrickController2/UI/Themes/LightTheme.xaml +++ b/BrickController2/BrickController2/UI/Themes/LightTheme.xaml @@ -41,6 +41,7 @@ DarkGreen DarkBlue Red + DarkRed Green \ No newline at end of file diff --git a/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs b/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs index d1b32e5d8..6adc97d35 100644 --- a/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs +++ b/BrickController2/BrickController2/UI/ViewModels/ControllerProfilePageViewModel.cs @@ -10,7 +10,7 @@ using BrickController2.DeviceManagement; using BrickController2.Extensions; using BrickController2.Helpers; -using BrickController2.PlatformServices.InputDeviceService; +using BrickController2.PlatformServices.InputDevice; using BrickController2.PlatformServices.SharedFileStorage; using BrickController2.UI.Commands; using BrickController2.UI.Services.Dialog; @@ -55,6 +55,8 @@ public ControllerProfilePageViewModel( CopyControllerProfileCommand = new SafeCommand(CopyControllerProfileAsync); RenameProfileCommand = new SafeCommand(async () => await RenameControllerProfileAsync()); AddControllerEventCommand = new SafeCommand(async () => await AddControllerEventAsync(false)); + AddControllerAxisEventCommand = new SafeCommand(async () => await AddControllerEventAsync(false, InputDeviceEventType.Axis)); + AddControllerButtonEventCommand = new SafeCommand(async () => await AddControllerEventAsync(false, InputDeviceEventType.Button)); AddControllerEventForSpecificControllerIdCommand = new SafeCommand(async () => await AddControllerEventAsync(true)); PlayCommand = new SafeCommand(async () => await PlayAsync()); ControllerActionTappedCommand = new SafeCommand(ShowActionAsync); @@ -87,6 +89,8 @@ public List ControllerEvents public ICommand CopyControllerProfileCommand { get; } public ICommand RenameProfileCommand { get; } public ICommand AddControllerEventCommand { get; } + public ICommand AddControllerAxisEventCommand { get; } + public ICommand AddControllerButtonEventCommand { get; } public ICommand AddControllerEventForSpecificControllerIdCommand { get; } public ICommand PlayCommand { get; } public ICommand ControllerActionTappedCommand { get; } @@ -205,7 +209,7 @@ await _dialogService.ShowProgressDialogAsync( } } - private async Task AddControllerEventAsync(bool addControllerId) + private async Task AddControllerEventAsync(bool addControllerId, InputDeviceEventType? requiredEventType = null) { try { @@ -219,11 +223,18 @@ await _dialogService.ShowMessageBoxAsync( return; } + var message = requiredEventType switch + { + InputDeviceEventType.Button => Translate("PressButton"), + InputDeviceEventType.Axis => Translate("PressOrMoveJoy"), + _ => Translate("PressButtonOrMoveJoy") + }; var result = await _dialogService.ShowGameControllerEventDialogAsync( Translate("Controller"), - Translate("PressButtonOrMoveJoy"), + message, Translate("Cancel"), - DisappearingToken); + DisappearingToken, + (eventType, eventCode) => requiredEventType is null || eventType == requiredEventType); if (result.IsOk) { ControllerEvent? controllerEvent = null;