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