diff --git a/StreamDeckSimHub.Plugin/ActionEditor/Behaviors/OpenContextMenuOnClickBehavior.cs b/StreamDeckSimHub.Plugin/ActionEditor/Behaviors/OpenContextMenuOnClickBehavior.cs new file mode 100644 index 0000000..e5f09f5 --- /dev/null +++ b/StreamDeckSimHub.Plugin/ActionEditor/Behaviors/OpenContextMenuOnClickBehavior.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2026 Martin Renner +// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) + +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +namespace StreamDeckSimHub.Plugin.ActionEditor.Behaviors; + +/// +/// Modifies a Button to open its ContextMenu on left click instead of right click. +/// +public static class OpenContextMenuOnClickBehavior +{ + public static readonly DependencyProperty ApplyProperty = + DependencyProperty.RegisterAttached( + "Apply", + typeof(bool), + typeof(OpenContextMenuOnClickBehavior), + new PropertyMetadata(false, OnApplyChanged)); + + public static void SetApply(DependencyObject element, bool value) => element.SetValue(ApplyProperty, value); + + public static bool GetApply(DependencyObject element) => (bool)element.GetValue(ApplyProperty); + + private static void OnApplyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (e.NewValue is not true) return; + if (d is not Button button) return; + + ContextMenuService.SetIsEnabled(button, false); + button.Click += Button_Click; + button.PreviewMouseRightButtonDown += SuppressRightClick; + } + + private static void Button_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.PlacementTarget = button; + button.ContextMenu.Placement = PlacementMode.Bottom; + button.ContextMenu.IsOpen = true; + } + } + + private static void SuppressRightClick(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + } +} \ No newline at end of file diff --git a/StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml b/StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml index 6e11d38..86f2622 100644 --- a/StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml +++ b/StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml @@ -57,14 +57,16 @@ - - + - - - - - + - - - ParentViewModel.ParentWindow.FindResource("DiKeyboardOutlinedGray") as ImageSource; + public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource(CommandItemKeypress.UiIcon) as ImageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] @@ -107,7 +107,8 @@ public partial class CommandItemSimHubControlViewModel( IViewModel parentViewModel, StreamDeckAction parentAction) : CommandItemViewModel(model, parentViewModel, parentAction) { - public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource("DiSimHubControlGray") as ImageSource; + public override ImageSource? Icon => + ParentViewModel.ParentWindow.FindResource(CommandItemSimHubControl.UiIcon) as ImageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] // see CommandItemSimHubControl.RawDisplayName which uses Control @@ -137,7 +138,7 @@ public partial class CommandItemSimHubRoleViewModel( StreamDeckAction parentAction) : CommandItemViewModel(model, parentViewModel, parentAction) { - public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource("DiSimHubRoleGray") as ImageSource; + public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource(CommandItemSimHubRole.UiIcon) as ImageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] // see CommandItemSimHubRole.RawDisplayName which uses Control diff --git a/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs b/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs index caf4924..7115f0d 100644 --- a/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs +++ b/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Martin Renner +// Copyright (C) 2026 Martin Renner // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) using System.ComponentModel; @@ -13,7 +13,6 @@ using StreamDeckSimHub.Plugin.ActionEditor.Tools; using StreamDeckSimHub.Plugin.ActionEditor.Views.Controls; using StreamDeckSimHub.Plugin.Actions.GenericButton.Model; -using StreamDeckSimHub.Plugin.PropertyLogic; using StreamDeckSimHub.Plugin.Tools; using Color = SixLabors.ImageSharp.Color; using Point = SixLabors.ImageSharp.Point; @@ -24,9 +23,15 @@ namespace StreamDeckSimHub.Plugin.ActionEditor.ViewModels; /// /// Base ViewModel for all DisplayItems /// -public abstract partial class DisplayItemViewModel(DisplayItem model, IViewModel parentViewModel) +public abstract partial class DisplayItemViewModel(DisplayItem model, IViewModel parentViewModel, byte? _) : ItemViewModel(model, parentViewModel), IDataErrorInfo { + protected DisplayItemViewModel(DisplayItem model, IViewModel parentViewModel) : this(model, parentViewModel, null) + { + } + + #region Element Data + [ObservableProperty] private float _transparency = model.DisplayParameters.Transparency; [ObservableProperty] @@ -94,6 +99,8 @@ partial void OnRotationChanged(int value) model.DisplayParameters.Rotation = value; } + #endregion + public string Error => string.Empty; public string this[string columnName] @@ -122,7 +129,7 @@ public string this[string columnName] public partial class DisplayItemImageViewModel(DisplayItemImage model, ImageManager imageManager, IViewModel parentViewModel) : DisplayItemViewModel(model, parentViewModel) { - public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource("DiInsertPhotoOutlinedGray") as ImageSource; + public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource(DisplayItemImage.UiIcon) as ImageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] // see DisplayItemImage.DisplayName which uses RelativePath @@ -158,7 +165,7 @@ private void SelectImage() public partial class DisplayItemTextViewModel(DisplayItemText model, IViewModel parentViewModel) : DisplayItemViewModel(model, parentViewModel), IFontSelectable, IColorSelectable { - public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource("DiTextFieldsGray") as ImageSource; + public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource(DisplayItemText.UiIcon) as ImageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] // see DisplayItemText.DisplayName which uses Text @@ -200,7 +207,6 @@ partial void OnImageSharpColorChanged(Color value) /// public partial class DisplayItemValueViewModel : DisplayItemViewModel, IFontSelectable, IColorSelectable { - private readonly NCalcHandler _ncalcHandler = new(); private readonly DisplayItemValue _model; public DisplayItemValueViewModel(DisplayItemValue model, IViewModel parentViewModel) : base(model, parentViewModel) @@ -210,7 +216,7 @@ public DisplayItemValueViewModel(DisplayItemValue model, IViewModel parentViewMo { ExpressionLabel = "Expression:", ExpressionToolTip = "Please enter a valid NCalc expression, that returns a value", - Example="round( [DataCorePlugin.GameData.Fuel], 1)", + Example = "round( [DataCorePlugin.GameData.Fuel], 1)", FetchShakeItProfilesCallback = FetchShakeItProfilesCallback }; _displayFormat = model.DisplayFormat; @@ -218,7 +224,7 @@ public DisplayItemValueViewModel(DisplayItemValue model, IViewModel parentViewMo _imageSharpColor = model.Color; } - public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource("DiAttachMoneyGray") as ImageSource; + public override ImageSource? Icon => ParentViewModel.ParentWindow.FindResource(DisplayItemValue.UiIcon) as ImageSource; [ObservableProperty] private ExpressionControlViewModel _expressionControlPropertyViewModel; diff --git a/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/SettingsViewModel.cs b/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/SettingsViewModel.cs index 91ca870..f3dee68 100644 --- a/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/SettingsViewModel.cs +++ b/StreamDeckSimHub.Plugin/ActionEditor/ViewModels/SettingsViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Windows; +using System.Windows.Controls; using System.Windows.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -52,11 +53,15 @@ partial void OnNameChanged(string value) public ObservableCollection FlatCommandItems { get; } = []; [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(AddSelectedCommandItemCommand))] [NotifyPropertyChangedFor(nameof(SelectedItem))] [NotifyPropertyChangedFor(nameof(IsAnyItemSelected))] private IFlatCommandItemsViewModel? _selectedFlatCommandItem; + partial void OnSelectedFlatCommandItemChanged(IFlatCommandItemsViewModel? oldValue, IFlatCommandItemsViewModel? newValue) + { + CanAddCommandItem = newValue != null; + } + /// /// Returns the currently selected DisplayItem or CommandItem, or null if none is selected. /// @@ -113,6 +118,23 @@ public SettingsViewModel(Settings settings, ImageManager imageManager, ISimHubCo NewVersion = "You are using the latest version."; NewVersionBrush = new SolidColorBrush(Color.FromRgb(30, 120, 30)); } + + DisplayItemTypes = + [ + // @formatter:off + new MenuItem { Header = DisplayItemImage.UiName, Icon = new Image { Source = ParentWindow.FindResource(DisplayItemImage.UiIcon) as ImageSource }, Command = AddDisplayItemCommand, CommandParameter = DisplayItemImage.UiName}, + new MenuItem { Header = DisplayItemText.UiName, Icon = new Image { Source = ParentWindow.FindResource(DisplayItemText.UiIcon) as ImageSource }, Command = AddDisplayItemCommand, CommandParameter = DisplayItemText.UiName}, + new MenuItem { Header = DisplayItemValue.UiName, Icon = new Image { Source = ParentWindow.FindResource(DisplayItemValue.UiIcon) as ImageSource }, Command = AddDisplayItemCommand, CommandParameter = DisplayItemValue.UiName} + // @formatter:on + ]; + CommandItemTypes = + [ + // @formatter:off + new MenuItem { Header = CommandItemKeypress.UiName, Icon = new Image { Source = ParentWindow.FindResource(CommandItemKeypress.UiIcon) as ImageSource }, Command = AddCommandItemCommand, CommandParameter = CommandItemKeypress.UiName}, + new MenuItem { Header = CommandItemSimHubControl.UiName, Icon = new Image { Source = ParentWindow.FindResource(CommandItemSimHubControl.UiIcon) as ImageSource }, Command = AddCommandItemCommand, CommandParameter = CommandItemSimHubControl.UiName}, + new MenuItem { Header = CommandItemSimHubRole.UiName, Icon = new Image { Source = ParentWindow.FindResource(CommandItemSimHubRole.UiIcon) as ImageSource }, Command = AddCommandItemCommand, CommandParameter = CommandItemSimHubRole.UiName} + // @formatter:on + ]; } #region IViewModel @@ -148,17 +170,12 @@ public async Task> FetchShakeItMotorsProfiles() #region AddDisplayItem /// List of available display item types - public ObservableCollection DisplayItemTypes { get; } = - [ - DisplayItemImage.UiName, DisplayItemText.UiName, DisplayItemValue.UiName - ]; - - [ObservableProperty] private string _selectedAddDisplayItemType = DisplayItemImage.UiName; + public ObservableCollection DisplayItemTypes { get; } [RelayCommand] - private void AddSelectedDisplayItem() + private void AddDisplayItem(string type) { - switch (SelectedAddDisplayItemType) + switch (type) { case DisplayItemImage.UiName: AddDisplayItem(DisplayItemImage.Create()); @@ -196,17 +213,14 @@ private DisplayItemViewModel DisplayItemToViewModel(DisplayItem displayItem) #region AddCommandItem /// List of available command item types - public ObservableCollection CommandItemTypes { get; } = - [ - CommandItemKeypress.UiName, CommandItemSimHubControl.UiName, CommandItemSimHubRole.UiName - ]; + public ObservableCollection CommandItemTypes { get; } - [ObservableProperty] private string _selectedAddCommandItemType = CommandItemKeypress.UiName; + [ObservableProperty] private bool _canAddCommandItem; - [RelayCommand(CanExecute = nameof(CanExecuteAddCommandItem))] - private void AddSelectedCommandItem() + [RelayCommand] + private void AddCommandItem(string type) { - switch (SelectedAddCommandItemType) + switch (type) { case CommandItemKeypress.UiName: AddCommandItem(CommandItemKeypress.Create()); @@ -220,8 +234,6 @@ private void AddSelectedCommandItem() } } - private bool CanExecuteAddCommandItem() => SelectedFlatCommandItem != null; - private void AddCommandItem(CommandItem newItem) { // Determine the action of the currently selected list item diff --git a/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml b/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml new file mode 100644 index 0000000..cb640c3 --- /dev/null +++ b/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml.cs b/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml.cs new file mode 100644 index 0000000..6692c56 --- /dev/null +++ b/StreamDeckSimHub.Plugin/ActionEditor/Views/Controls/DisplayElementControl.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace StreamDeckSimHub.Plugin.ActionEditor.Views.Controls; + +public partial class DisplayElementControl : UserControl +{ + public DisplayElementControl() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/StreamDeckSimHub.Plugin/ActionEditor/Views/DisplayItemView.xaml b/StreamDeckSimHub.Plugin/ActionEditor/Views/DisplayItemView.xaml index 5ad8ca2..4cfd1aa 100644 --- a/StreamDeckSimHub.Plugin/ActionEditor/Views/DisplayItemView.xaml +++ b/StreamDeckSimHub.Plugin/ActionEditor/Views/DisplayItemView.xaml @@ -22,42 +22,6 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemKeypress.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemKeypress.cs index 1c1a925..fb4db44 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemKeypress.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemKeypress.cs @@ -11,6 +11,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class CommandItemKeypress : CommandItem, ICommandItemLong { public const string UiName = "Keypress"; + public const string UiIcon = "DiKeyboardOutlinedGray"; [ObservableProperty] private string _key = string.Empty; [ObservableProperty] private bool _modifierCtrl; diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubControl.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubControl.cs index 3e80760..3e78610 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubControl.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubControl.cs @@ -9,6 +9,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class CommandItemSimHubControl : CommandItem, ICommandItemLong { public const string UiName = "SimHub Control"; + public const string UiIcon = "DiSimHubControlGray"; [ObservableProperty] private string _control = string.Empty; diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubRole.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubRole.cs index 15f2233..7d1f85a 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubRole.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/CommandItemSimHubRole.cs @@ -9,6 +9,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class CommandItemSimHubRole : CommandItem, ICommandItemLong { public const string UiName = "SimHub Role"; + public const string UiIcon = "DiSimHubRoleGray"; [ObservableProperty] private string _role = string.Empty; diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemImage.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemImage.cs index 226e579..afab5cf 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemImage.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemImage.cs @@ -11,6 +11,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class DisplayItemImage : DisplayItem { public const string UiName = "Image"; + public const string UiIcon = "DiInsertPhotoOutlinedGray"; // Image is being updated centrally by GenericButtonAction from the value of RelativePath. public Image Image { get; set; } = ImageUtils.EmptyImage; diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemText.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemText.cs index 29722ac..001eaf9 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemText.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemText.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Martin Renner +// Copyright (C) 2026 Martin Renner // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) using CommunityToolkit.Mvvm.ComponentModel; @@ -10,6 +10,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class DisplayItemText : DisplayItem { public const string UiName = "Text"; + public const string UiIcon = "DiTextFieldsGray"; [ObservableProperty] private string _text = string.Empty; [ObservableProperty] private Font _font = SystemFonts.CreateFont("Arial", 16, FontStyle.Regular); diff --git a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemValue.cs b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemValue.cs index 3ad6f67..60b60ef 100644 --- a/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemValue.cs +++ b/StreamDeckSimHub.Plugin/Actions/GenericButton/Model/DisplayItemValue.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Martin Renner +// Copyright (C) 2026 Martin Renner // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) using CommunityToolkit.Mvvm.ComponentModel; @@ -11,6 +11,7 @@ namespace StreamDeckSimHub.Plugin.Actions.GenericButton.Model; public partial class DisplayItemValue : DisplayItem { public const string UiName = "Value"; + public const string UiIcon = "DiAttachMoneyGray"; [ObservableProperty] private NCalcHolder _nCalcPropertyHolder; @@ -44,5 +45,4 @@ public override async Task Accept(IDisplayItemVisitor displayItemVisitor, IVisit { await displayItemVisitor.Visit(this, args); } -} - +} \ No newline at end of file diff --git a/StreamDeckSimHub.Plugin/App.xaml.cs b/StreamDeckSimHub.Plugin/App.xaml.cs index e5ac8fe..e415b34 100644 --- a/StreamDeckSimHub.Plugin/App.xaml.cs +++ b/StreamDeckSimHub.Plugin/App.xaml.cs @@ -65,7 +65,7 @@ private async void Application_Startup(object sender, StartupEventArgs e) }; settings.SettingsChanged += (sender, e) => { - Console.WriteLine($"sender: {sender} / {e.PropertyName}"); + Console.WriteLine($"Dev mode - settings changed. Sender: {sender} / Property: {e.PropertyName}"); }; var actionEditorManager = _host.Services.GetService(); diff --git a/StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj b/StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj index 82b38ed..75a2b9b 100644 --- a/StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj +++ b/StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj @@ -12,7 +12,6 @@ disable enable StreamDeckSimHub - 12