Skip to content
Merged
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
47 changes: 46 additions & 1 deletion StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
d:DataContext="{d:DesignInstance Type=viewModels:SettingsViewModelDesignTime, IsDesignTimeCreatable=True}"
Loaded="OnLoaded"
Closed="OnClosed"
Deactivated="OnDeactivated"
Icon="/ActionEditor/GenericButtonEditor.ico"
Title="{Binding NameForTitle}"
Height="800" Width="1000" MinHeight="750" MinWidth="800">
Height="810" Width="1000" MinHeight="810" MinWidth="800">

<Window.Resources>
<ResourceDictionary>
Expand Down Expand Up @@ -183,13 +184,21 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="120" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Label Grid.Column="0" Content="Button Name:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" MinWidth="150"
VerticalContentAlignment="Center"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
ToolTip="Is only used for visualization" />
<ToggleButton x:Name="SettingsButton" Grid.Column="2" Margin="12,0,0,0" Padding="2"
Background="Transparent" BorderBrush="Transparent"
IsChecked="{Binding IsSettingsOverlayVisible, Mode=OneWay}"
Click="SettingsButton_OnClick"
ToolTip="Settings">
<Image Source="{StaticResource DiSettingsOutlinedGray}" Width="16" Height="16" />
</ToggleButton>
</Grid>

<!-- Separator line -->
Expand All @@ -211,6 +220,42 @@
</DataTemplate>
</ContentControl.Resources>
</ContentControl>

<!-- Settings popup: anchored to SettingsButton, right-aligned with it. -->
<Popup x:Name="SettingsPopup" Grid.Row="0"
PlacementTarget="{Binding ElementName=SettingsButton}"
Placement="Bottom"
HorizontalOffset="-218"
IsOpen="{Binding IsSettingsOverlayVisible, Mode=TwoWay}"
AllowsTransparency="True">
<Border Background="White" BorderBrush="#AAAAAA" BorderThickness="1"
CornerRadius="4" Padding="12" Width="240">
<StackPanel>
<TextBlock Text="Blink Override" FontWeight="Bold" Margin="0,0,0,10" />
<CheckBox Content="Enabled"
IsChecked="{Binding BlinkOverride.Enabled}"
Margin="0,0,0,8" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Duration On:" VerticalAlignment="Center" />
<TextBox Grid.Row="0" Grid.Column="1" Margin="0,2"
VerticalContentAlignment="Center"
Text="{Binding BlinkOverride.DurationOn, UpdateSourceTrigger=PropertyChanged}" />
<Label Grid.Row="1" Grid.Column="0" Content="Duration Off:" VerticalAlignment="Center" />
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,2"
VerticalContentAlignment="Center"
Text="{Binding BlinkOverride.DurationOff, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</StackPanel>
</Border>
</Popup>
</Grid>
</Grid>
</DockPanel>
Expand Down
49 changes: 49 additions & 0 deletions StreamDeckSimHub.Plugin/ActionEditor/GenericButtonEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Xaml.Behaviors;
using NLog;
Expand Down Expand Up @@ -55,6 +57,9 @@ private void SetupDragDropBehaviors()

private async void OnLoaded(object sender, RoutedEventArgs e)
{
GetWindow(this)?.AddHandler(
PreviewMouseDownEvent,
new MouseButtonEventHandler(CloseSettingsOnOutsideClick));
try
{
await ((SettingsViewModel)DataContext).FetchControlMapperRoles();
Expand All @@ -68,8 +73,16 @@ private async void OnLoaded(object sender, RoutedEventArgs e)
}
}

private void OnDeactivated(object? sender, EventArgs e)
{
((SettingsViewModel)DataContext).IsSettingsOverlayVisible = false;
}

private void OnClosed(object? sender, EventArgs e)
{
GetWindow(this)?.RemoveHandler(
PreviewMouseDownEvent,
new MouseButtonEventHandler(CloseSettingsOnOutsideClick));
WeakReferenceMessenger.Default.Send(new GenericButtonEditorClosedEvent(_actionUuid));
}

Expand Down Expand Up @@ -149,4 +162,40 @@ private void OnCommandItemDropped(object draggedItem, object targetItem, int sou
((SettingsViewModel)DataContext).FlatCommandItems.Move(sourceIndex, targetIndex);
((SettingsViewModel)DataContext).UpdateCommandItemsOrder(commandItem.ParentAction);
}

/// <summary>
/// Closes the Settings popup when the user clicks anywhere in the window outside the toggle button.
/// </summary>
private void CloseSettingsOnOutsideClick(object sender, MouseButtonEventArgs e)
{
var vm = (SettingsViewModel)DataContext;
if (!vm.IsSettingsOverlayVisible) return;

if (e.OriginalSource is DependencyObject source)
{
// Click on the toggle button itself: let the Click handler toggle the state.
if (IsDescendantOf(source, SettingsButton)) return;
// Click inside the popup content: keep the popup open.
if (SettingsPopup.Child != null && IsDescendantOf(source, SettingsPopup.Child)) return;
}

vm.IsSettingsOverlayVisible = false;
}

private void SettingsButton_OnClick(object sender, RoutedEventArgs e)
{
var vm = (SettingsViewModel)DataContext;
vm.IsSettingsOverlayVisible = !vm.IsSettingsOverlayVisible;
}

private static bool IsDescendantOf(DependencyObject element, DependencyObject ancestor)
{
var current = element;
while (current != null)
{
if (current == ancestor) return true;
current = VisualTreeHelper.GetParent(current);
}
return false;
}
}
14 changes: 14 additions & 0 deletions StreamDeckSimHub.Plugin/ActionEditor/IconResources.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,18 @@
</DrawingGroup>
<DrawingImage Drawing="{StaticResource Cached}" x:Key="DiCached" />

<DrawingGroup x:Key="SettingsOutlinedGray">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0.0,0.0,24.0,24.0" />
</DrawingGroup.ClipGeometry>
<GeometryDrawing Brush="#ff808080">
<GeometryDrawing.Geometry>
<PathGeometry
Figures="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
FillRule="EvenOdd" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingImage Drawing="{StaticResource SettingsOutlinedGray}" x:Key="DiSettingsOutlinedGray" />

</ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (C) 2026 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
Expand All @@ -13,6 +14,7 @@
using StreamDeckSimHub.Plugin.ActionEditor.Tools;
using StreamDeckSimHub.Plugin.ActionEditor.Views.Controls;
using StreamDeckSimHub.Plugin.Actions.GenericButton.Model;
using StreamDeckSimHub.Plugin.Actions.GenericButton.Model.Modifiers;
using StreamDeckSimHub.Plugin.Tools;
using Color = SixLabors.ImageSharp.Color;
using Point = SixLabors.ImageSharp.Point;
Expand All @@ -23,13 +25,21 @@
/// <summary>
/// Base ViewModel for all DisplayItems
/// </summary>
public abstract partial class DisplayItemViewModel(DisplayItem model, IViewModel parentViewModel, byte? _)

Check warning on line 28 in StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs

View workflow job for this annotation

GitHub Actions / build

Parameter '_' is unread.

Check warning on line 28 in StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs

View workflow job for this annotation

GitHub Actions / build

Parameter '_' is unread.

Check warning on line 28 in StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs

View workflow job for this annotation

GitHub Actions / build

Parameter '_' is unread.

Check warning on line 28 in StreamDeckSimHub.Plugin/ActionEditor/ViewModels/DisplayItemViewModels.cs

View workflow job for this annotation

GitHub Actions / build

Parameter '_' is unread.
: ItemViewModel(model, parentViewModel), IDataErrorInfo
{
protected DisplayItemViewModel(DisplayItem model, IViewModel parentViewModel) : this(model, parentViewModel, null)
{
Modifiers = new ObservableCollection<ModifierViewModel>(model.Modifiers.Select(ModifierToViewModel));
Modifiers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasModifiers));

if (model is IAcceptsModifierBlink) AvailableModifiers.Add(ModifierBlink.UiName);
if (model is IAcceptsModifierColor) AvailableModifiers.Add(ModifierColor.UiName);
CanAddModifier = AvailableModifiers.Count > 0;
}

[ObservableProperty] private int _selectedTabIndex;

#region Element Data

[ObservableProperty] private float _transparency = model.DisplayParameters.Transparency;
Expand Down Expand Up @@ -101,6 +111,94 @@

#endregion

#region Modifiers

public ObservableCollection<ModifierViewModel> Modifiers { get; } = [];

public bool HasModifiers => Modifiers.Count > 0;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsModifierSelected))]
private ModifierViewModel? _selectedModifier;

public bool IsModifierSelected => SelectedModifier != null;

public ObservableCollection<string> AvailableModifiers { get; } = [];

[ObservableProperty] private bool _canAddModifier;

[RelayCommand]
private void AddModifier(string type)
{
switch (type)
{
case ModifierBlink.UiName:
AddModifier(ModifierBlink.Create());
break;
case ModifierColor.UiName:
AddModifier(ModifierColor.Create());
break;
}
}

private void AddModifier(Modifier modifier)
{
model.Modifiers.Add(modifier);
var vm = ModifierToViewModel(modifier);
Modifiers.Add(vm);
SelectedModifier = vm;
}

private ModifierViewModel ModifierToViewModel(Modifier modifier)
{
return modifier switch
{
ModifierBlink modifierBlink => new ModifierBlinkViewModel(modifierBlink, ParentViewModel),
ModifierColor colorModifier => new ModifierColorViewModel(colorModifier, ParentViewModel),
_ => throw new InvalidOperationException($"Unknown Modifier type: {modifier.GetType().FullName}")
};
}

public void RemoveModifier(ModifierViewModel item)
{
// Remove from the underlying model
var modifier = item.GetModel();
model.Modifiers.Remove(modifier);

// Remove from the ViewModel collection
Modifiers.Remove(item);

// Clear selection if this was the selected item
if (SelectedModifier == item)
{
SelectedModifier = null;
}

}

#endregion

#region DragDrop

/// <summary>
/// Updates the underlying model when Modifiers are reordered
/// </summary>
public void UpdateModifiersOrder()
{
// Update the underlying model's Modifiers list to match the order in the ViewModel
// We'll create a new list with the same items but in the new order
var newList = Modifiers.Select(modifierVm => modifierVm.GetModel()).ToList();

// Clear and repopulate the original list to maintain the reference
model.Modifiers.Clear();
foreach (var item in newList)
{
model.Modifiers.Add(item);
}
}

#endregion

public string Error => string.Empty;

public string this[string columnName]
Expand All @@ -109,7 +207,7 @@
{
if (columnName == nameof(TransparencyText))
{
if (string.IsNullOrWhiteSpace(TransparencyText)) return string.Empty;
if (string.IsNullOrWhiteSpace(TransparencyText)) return "Transparency value is required.";

if (!float.TryParse(TransparencyText, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|| parsed < 0f || parsed > 1f)
Expand Down
Loading
Loading