diff --git a/RGBSync+/App.config b/RGBSync+/App.config
new file mode 100644
index 0000000..8e15646
--- /dev/null
+++ b/RGBSync+/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/App.xaml b/RGBSync+/App.xaml
new file mode 100644
index 0000000..b8e1030
--- /dev/null
+++ b/RGBSync+/App.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RGBSync+/App.xaml.cs b/RGBSync+/App.xaml.cs
new file mode 100644
index 0000000..cb651cb
--- /dev/null
+++ b/RGBSync+/App.xaml.cs
@@ -0,0 +1,78 @@
+using System;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using Hardcodet.Wpf.TaskbarNotification;
+using Newtonsoft.Json;
+using RGBSyncPlus.Configuration;
+using RGBSyncPlus.Configuration.Legacy;
+using RGBSyncPlus.Helper;
+
+namespace RGBSyncPlus
+{
+ public partial class App : Application
+ {
+ #region Constants
+
+ private const string PATH_SETTINGS = "Settings.json";
+
+ #endregion
+
+ #region Properties & Fields
+
+ private TaskbarIcon _taskbarIcon;
+
+ #endregion
+
+ #region Methods
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+
+ try
+ {
+ ToolTipService.ShowDurationProperty.OverrideMetadata(typeof(DependencyObject), new FrameworkPropertyMetadata(int.MaxValue));
+
+ _taskbarIcon = (TaskbarIcon)FindResource("TaskbarIcon");
+ _taskbarIcon.DoubleClickCommand = ApplicationManager.Instance.OpenConfigurationCommand;
+
+ Settings settings = null;
+ try { settings = JsonConvert.DeserializeObject(File.ReadAllText(PATH_SETTINGS), new ColorSerializer()); }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.Message);
+ /* File doesn't exist or is corrupt - just create a new one. */
+ }
+
+ if (settings == null)
+ {
+ settings = new Settings { Version = Settings.CURRENT_VERSION };
+ _taskbarIcon.ShowBalloonTip("RGBSync+ is starting in the tray!", "Click on the icon to open the configuration.", BalloonIcon.Info);
+ }
+ else if (settings.Version != Settings.CURRENT_VERSION)
+ ConfigurationUpdates.PerformOn(settings);
+
+ ApplicationManager.Instance.Settings = settings;
+ ApplicationManager.Instance.Initialize();
+ }
+ catch (Exception ex)
+ {
+ File.WriteAllText("error.log", $"[{DateTime.Now:G}] Exception!\r\n\r\nMessage:\r\n{ex.GetFullMessage()}\r\n\r\nStackTrace:\r\n{ex.StackTrace}\r\n\r\n");
+ MessageBox.Show("An error occured while starting RGBSync+.\r\nMore information can be found in the error.log file in the application directory.", "Can't start RGBSync+.");
+
+ try { ApplicationManager.Instance.ExitCommand.Execute(null); }
+ catch { Environment.Exit(0); }
+ }
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ base.OnExit(e);
+
+ File.WriteAllText(PATH_SETTINGS, JsonConvert.SerializeObject(ApplicationManager.Instance.Settings, new ColorSerializer()));
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/ApplicationManager.cs b/RGBSync+/ApplicationManager.cs
new file mode 100644
index 0000000..c5fb75d
--- /dev/null
+++ b/RGBSync+/ApplicationManager.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Windows;
+using RGB.NET.Core;
+using RGB.NET.Groups;
+using RGBSyncPlus.Brushes;
+using RGBSyncPlus.Configuration;
+using RGBSyncPlus.Helper;
+using RGBSyncPlus.Model;
+using RGBSyncPlus.UI;
+
+namespace RGBSyncPlus
+{
+ public class ApplicationManager
+ {
+ #region Constants
+
+ private const string DEVICEPROVIDER_DIRECTORY = "DeviceProvider";
+
+ #endregion
+
+ #region Properties & Fields
+
+ public static ApplicationManager Instance { get; } = new ApplicationManager();
+
+ private ConfigurationWindow _configurationWindow;
+
+ public Settings Settings { get; set; }
+ public TimerUpdateTrigger UpdateTrigger { get; private set; }
+
+ #endregion
+
+ #region Commands
+
+ private ActionCommand _openConfiguration;
+ public ActionCommand OpenConfigurationCommand => _openConfiguration ?? (_openConfiguration = new ActionCommand(OpenConfiguration));
+
+ private ActionCommand _exitCommand;
+ public ActionCommand ExitCommand => _exitCommand ?? (_exitCommand = new ActionCommand(Exit));
+
+ #endregion
+
+ #region Constructors
+
+ private ApplicationManager() { }
+
+ #endregion
+
+ #region Methods
+
+ public void Initialize()
+ {
+ RGBSurface surface = RGBSurface.Instance;
+ LoadDeviceProviders();
+ surface.AlignDevices();
+
+ foreach (IRGBDevice device in surface.Devices)
+ device.UpdateMode = DeviceUpdateMode.Sync | DeviceUpdateMode.SyncBack;
+
+ UpdateTrigger = new TimerUpdateTrigger { UpdateFrequency = 1.0 / MathHelper.Clamp(Settings.UpdateRate, 1, 100) };
+ surface.RegisterUpdateTrigger(UpdateTrigger);
+ UpdateTrigger.Start();
+
+ foreach (SyncGroup syncGroup in Settings.SyncGroups)
+ RegisterSyncGroup(syncGroup);
+ }
+
+ private void LoadDeviceProviders()
+ {
+ string deviceProvierDir = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? string.Empty, DEVICEPROVIDER_DIRECTORY);
+ if (!Directory.Exists(deviceProvierDir)) return;
+
+ foreach (string file in Directory.GetFiles(deviceProvierDir, "*.dll"))
+ {
+ try
+ {
+ Assembly assembly = Assembly.LoadFrom(file);
+ foreach (Type loaderType in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface && t.IsClass
+ && typeof(IRGBDeviceProviderLoader).IsAssignableFrom(t)))
+ {
+ if (Activator.CreateInstance(loaderType) is IRGBDeviceProviderLoader deviceProviderLoader)
+ {
+ //TODO DarthAffe 03.06.2018: Support Initialization
+ if (deviceProviderLoader.RequiresInitialization) continue;
+
+ RGBSurface.Instance.LoadDevices(deviceProviderLoader);
+ }
+ }
+ }
+ catch { /* #sadprogrammer */ }
+ }
+ }
+
+ public void AddSyncGroup(SyncGroup syncGroup)
+ {
+ Settings.SyncGroups.Add(syncGroup);
+ RegisterSyncGroup(syncGroup);
+ }
+
+ private void RegisterSyncGroup(SyncGroup syncGroup)
+ {
+ syncGroup.LedGroup = new ListLedGroup(syncGroup.Leds.GetLeds()) { Brush = new SyncBrush(syncGroup) };
+ syncGroup.LedsChangedEventHandler = (sender, args) => UpdateLedGroup(syncGroup.LedGroup, args);
+ syncGroup.Leds.CollectionChanged += syncGroup.LedsChangedEventHandler;
+ }
+
+ public void RemoveSyncGroup(SyncGroup syncGroup)
+ {
+ Settings.SyncGroups.Remove(syncGroup);
+ syncGroup.Leds.CollectionChanged -= syncGroup.LedsChangedEventHandler;
+ syncGroup.LedGroup.Detach();
+ syncGroup.LedGroup = null;
+ }
+
+ private void UpdateLedGroup(ListLedGroup group, NotifyCollectionChangedEventArgs args)
+ {
+ if (args.Action == NotifyCollectionChangedAction.Reset)
+ {
+ List leds = group.GetLeds().ToList();
+ group.RemoveLeds(leds);
+ }
+ else
+ {
+ if (args.NewItems != null)
+ group.AddLeds(args.NewItems.Cast().GetLeds());
+
+ if (args.OldItems != null)
+ group.RemoveLeds(args.OldItems.Cast().GetLeds());
+ }
+ }
+
+ private void OpenConfiguration()
+ {
+ if (_configurationWindow == null) _configurationWindow = new ConfigurationWindow();
+ _configurationWindow.Show();
+ }
+
+ private void Exit()
+ {
+ try { RGBSurface.Instance?.Dispose(); } catch { /* well, we're shuting down anyway ... */ }
+ Application.Current.Shutdown();
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Attached/SliderValue.cs b/RGBSync+/Attached/SliderValue.cs
new file mode 100644
index 0000000..ab43c1d
--- /dev/null
+++ b/RGBSync+/Attached/SliderValue.cs
@@ -0,0 +1,111 @@
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace RGBSyncPlus.Attached
+{
+ public static class SliderValue
+ {
+ #region Properties & Fields
+ // ReSharper disable InconsistentNaming
+
+ public static readonly DependencyProperty UnitProperty = DependencyProperty.RegisterAttached(
+ "Unit", typeof(string), typeof(SliderValue), new PropertyMetadata(default(string)));
+
+ public static void SetUnit(DependencyObject element, string value) => element.SetValue(UnitProperty, value);
+ public static string GetUnit(DependencyObject element) => (string)element.GetValue(UnitProperty);
+
+ public static readonly DependencyProperty IsShownProperty = DependencyProperty.RegisterAttached(
+ "IsShown", typeof(bool), typeof(SliderValue), new PropertyMetadata(default(bool), IsShownChanged));
+
+ public static void SetIsShown(DependencyObject element, bool value) => element.SetValue(IsShownProperty, value);
+ public static bool GetIsShown(DependencyObject element) => (bool)element.GetValue(IsShownProperty);
+
+ public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.RegisterAttached(
+ "BorderBrush", typeof(Brush), typeof(SliderValue), new PropertyMetadata(default(Brush)));
+
+ public static void SetBorderBrush(DependencyObject element, Brush value) => element.SetValue(BorderBrushProperty, value);
+ public static Brush GetBorderBrush(DependencyObject element) => (Brush)element.GetValue(BorderBrushProperty);
+
+ public static readonly DependencyProperty BackgroundProperty = DependencyProperty.RegisterAttached(
+ "Background", typeof(Brush), typeof(SliderValue), new PropertyMetadata(default(Brush)));
+
+ public static void SetBackground(DependencyObject element, Brush value) => element.SetValue(BackgroundProperty, value);
+ public static Brush GetBackground(DependencyObject element) => (Brush)element.GetValue(BackgroundProperty);
+
+ public static readonly DependencyProperty ForegroundProperty = DependencyProperty.RegisterAttached(
+ "Foreground", typeof(Brush), typeof(SliderValue), new PropertyMetadata(default(Brush)));
+
+ public static void SetForeground(DependencyObject element, Brush value) => element.SetValue(ForegroundProperty, value);
+ public static Brush GetForeground(DependencyObject element) => (Brush)element.GetValue(ForegroundProperty);
+
+ public static readonly DependencyProperty FontProperty = DependencyProperty.RegisterAttached(
+ "Font", typeof(FontFamily), typeof(SliderValue), new PropertyMetadata(default(FontFamily)));
+
+ public static void SetFont(DependencyObject element, FontFamily value) => element.SetValue(FontProperty, value);
+ public static FontFamily GetFont(DependencyObject element) => (FontFamily)element.GetValue(FontProperty);
+
+ public static readonly DependencyProperty FontSizeProperty = DependencyProperty.RegisterAttached(
+ "FontSize", typeof(double), typeof(SliderValue), new PropertyMetadata(default(double)));
+
+ public static void SetFontSize(DependencyObject element, double value) => element.SetValue(FontSizeProperty, value);
+ public static double GetFontSize(DependencyObject element) => (double)element.GetValue(FontSizeProperty);
+
+ // ReSharper enable InconsistentNaming
+ #endregion
+
+ #region Methods
+
+ private static void IsShownChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ if (!(dependencyObject is Slider slider)) return;
+
+ if (dependencyPropertyChangedEventArgs.NewValue as bool? == true)
+ {
+ slider.MouseEnter += SliderOnMouseEnter;
+ slider.MouseLeave += SliderOnMouseLeave;
+ }
+ else
+ {
+ slider.MouseEnter -= SliderOnMouseEnter;
+ slider.MouseLeave -= SliderOnMouseLeave;
+ RemoveAdorner(slider);
+ }
+ }
+
+ private static void SliderOnMouseEnter(object sender, MouseEventArgs mouseEventArgs)
+ {
+ if (!(sender is Slider slider)) return;
+ AdornerLayer.GetAdornerLayer(slider)?.Add(new SliderValueAdorner(slider, GetUnit(slider))
+ {
+ BorderBrush = GetBorderBrush(slider),
+ Background = GetBackground(slider),
+ Foreground = GetForeground(slider),
+ Font = GetFont(slider),
+ FontSize = GetFontSize(slider)
+ });
+ }
+
+ private static void SliderOnMouseLeave(object sender, MouseEventArgs mouseEventArgs)
+ {
+ if (!(sender is Slider slider)) return;
+ RemoveAdorner(slider);
+ }
+
+ private static void RemoveAdorner(Slider slider)
+ {
+ AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(slider);
+ Adorner adorner = adornerLayer?.GetAdorners(slider)?.FirstOrDefault(x => x is SliderValueAdorner);
+ if (adorner != null)
+ {
+ adornerLayer.Remove(adorner);
+ (adorner as SliderValueAdorner)?.Cleanup();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Attached/SliderValueAdorner.cs b/RGBSync+/Attached/SliderValueAdorner.cs
new file mode 100644
index 0000000..bc44983
--- /dev/null
+++ b/RGBSync+/Attached/SliderValueAdorner.cs
@@ -0,0 +1,99 @@
+using System.Globalization;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using Point = System.Windows.Point;
+
+namespace RGBSyncPlus.Attached
+{
+ public class SliderValueAdorner : System.Windows.Documents.Adorner
+ {
+ #region Properties & Fields
+
+ private readonly string _unit;
+ private readonly Slider _slider;
+ private readonly Thumb _thumb;
+ private readonly RepeatButton _decreaseRepeatButton;
+
+ public Brush BorderBrush { get; set; } = System.Windows.Media.Brushes.Black;
+ public Brush Background { get; set; } = System.Windows.Media.Brushes.Black;
+ public Brush Foreground { get; set; } = System.Windows.Media.Brushes.White;
+ public FontFamily Font { get; set; } = new FontFamily("Verdana");
+ public double FontSize { get; set; } = 14;
+
+ #endregion
+
+ #region Constructors
+
+ public SliderValueAdorner(UIElement adornedElement, string unit)
+ : base(adornedElement)
+ {
+ this._unit = unit;
+
+ _slider = (Slider)adornedElement;
+ Track track = (Track)_slider.Template.FindName("PART_Track", _slider);
+
+ _thumb = track.Thumb;
+ _decreaseRepeatButton = track.DecreaseRepeatButton;
+ _decreaseRepeatButton.SizeChanged += OnButtonSizeChanged;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public void Cleanup()
+ {
+ _decreaseRepeatButton.SizeChanged -= OnButtonSizeChanged;
+ }
+
+ private void OnButtonSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs) => InvalidateVisual();
+
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ double offset = _decreaseRepeatButton.ActualWidth + (_thumb.ActualWidth / 2.0);
+
+ FormattedText text = new FormattedText(GetText(), CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface(Font, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal), FontSize, Foreground);
+ Geometry border = CreateBorder(offset, text.Width, text.Height);
+
+ drawingContext.DrawGeometry(Background, new Pen(BorderBrush, 1), border);
+ drawingContext.DrawText(text, new Point(offset - (text.Width / 2.0), -26));
+ }
+
+ private string GetText()
+ {
+ string valueText = _slider.Value.ToString();
+ if (!string.IsNullOrWhiteSpace(_unit))
+ valueText += " " + _unit;
+
+ return valueText;
+ }
+
+ private Geometry CreateBorder(double offset, double width, double height)
+ {
+ double halfWidth = width / 2.0;
+
+ PathGeometry borderGeometry = new PathGeometry();
+ PathFigure border = new PathFigure
+ {
+ StartPoint = new Point(offset, 0),
+ IsClosed = true,
+ IsFilled = true
+ };
+
+ border.Segments.Add(new LineSegment(new Point(offset + 4, -6), true));
+ border.Segments.Add(new LineSegment(new Point(offset + 4 + halfWidth, -6), true));
+ border.Segments.Add(new LineSegment(new Point(offset + 4 + halfWidth, -10 - height), true));
+ border.Segments.Add(new LineSegment(new Point(offset - 4 - halfWidth, -10 - height), true));
+ border.Segments.Add(new LineSegment(new Point(offset - 4 - halfWidth, -6), true));
+ border.Segments.Add(new LineSegment(new Point(offset - 4, -6), true));
+
+ borderGeometry.Figures.Add(border);
+
+ return borderGeometry;
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Brushes/SyncBrush.cs b/RGBSync+/Brushes/SyncBrush.cs
new file mode 100644
index 0000000..9f64245
--- /dev/null
+++ b/RGBSync+/Brushes/SyncBrush.cs
@@ -0,0 +1,48 @@
+using System.ComponentModel;
+using RGB.NET.Core;
+using RGBSyncPlus.Helper;
+using RGBSyncPlus.Model;
+
+namespace RGBSyncPlus.Brushes
+{
+ public class SyncBrush : AbstractBrush
+ {
+ #region Properties & Fields
+
+ private readonly SyncGroup _syncGroup;
+
+ private Led _syncLed;
+
+ #endregion
+
+ #region Constructors
+
+ public SyncBrush(SyncGroup syncGroup)
+ {
+ this._syncGroup = syncGroup;
+
+ syncGroup.PropertyChanged += SyncGroupOnPropertyChanged;
+ _syncLed = syncGroup.SyncLed?.GetLed();
+ }
+
+ #endregion
+
+ #region Methods
+
+ private void SyncGroupOnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SyncGroup.SyncLed))
+ _syncLed = _syncGroup.SyncLed?.GetLed();
+ }
+
+ protected override Color GetColorAtPoint(Rectangle rectangle, BrushRenderTarget renderTarget)
+ {
+ if (renderTarget.Led == _syncLed)
+ return Color.Transparent;
+
+ return _syncLed?.Color ?? Color.Transparent;
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Configuration/AbstractConfiguration.cs b/RGBSync+/Configuration/AbstractConfiguration.cs
new file mode 100644
index 0000000..69f897f
--- /dev/null
+++ b/RGBSync+/Configuration/AbstractConfiguration.cs
@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using RGB.NET.Core;
+
+namespace RGBSyncPlus.Configuration
+{
+ public class AbstractConfiguration : AbstractBindable, IConfiguration, INotifyPropertyChanged
+ {
+ #region Methods
+
+ protected override bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
+ {
+ if ((typeof(T) == typeof(double)) || (typeof(T) == typeof(float)))
+ {
+ if (Math.Abs((double)(object)storage - (double)(object)value) < 0.000001) return false;
+ }
+ else
+ {
+ if (Equals(storage, value)) return false;
+ }
+
+ storage = value;
+ // ReSharper disable once ExplicitCallerInfoArgument
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Configuration/ColorSerializer.cs b/RGBSync+/Configuration/ColorSerializer.cs
new file mode 100644
index 0000000..7f2bae3
--- /dev/null
+++ b/RGBSync+/Configuration/ColorSerializer.cs
@@ -0,0 +1,41 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RGB.NET.Core;
+
+namespace RGBSyncPlus.Configuration
+{
+ public class ColorSerializer : JsonConverter
+ {
+ #region Methods
+
+ public override bool CanConvert(Type objectType) => objectType == typeof(Color);
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ if (!(value is Color color)) return;
+
+ writer.WriteStartObject();
+ writer.WritePropertyName("A");
+ writer.WriteValue(color.A);
+ writer.WritePropertyName("R");
+ writer.WriteValue(color.R);
+ writer.WritePropertyName("G");
+ writer.WriteValue(color.G);
+ writer.WritePropertyName("B");
+ writer.WriteValue(color.B);
+ writer.WriteEndObject();
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JObject jsonObject = JObject.Load(reader);
+ return new Color(jsonObject.Property("A").Value.ToObject(),
+ jsonObject.Property("R").Value.ToObject(),
+ jsonObject.Property("G").Value.ToObject(),
+ jsonObject.Property("B").Value.ToObject());
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Configuration/IConfiguration.cs b/RGBSync+/Configuration/IConfiguration.cs
new file mode 100644
index 0000000..65361f7
--- /dev/null
+++ b/RGBSync+/Configuration/IConfiguration.cs
@@ -0,0 +1,7 @@
+using System.ComponentModel;
+
+namespace RGBSyncPlus.Configuration
+{
+ public interface IConfiguration : INotifyPropertyChanged
+ { }
+}
diff --git a/RGBSync+/Configuration/Legacy/ConfigurationUpdates.cs b/RGBSync+/Configuration/Legacy/ConfigurationUpdates.cs
new file mode 100644
index 0000000..34d52b4
--- /dev/null
+++ b/RGBSync+/Configuration/Legacy/ConfigurationUpdates.cs
@@ -0,0 +1,12 @@
+namespace RGBSyncPlus.Configuration.Legacy
+{
+ public static class ConfigurationUpdates
+ {
+ #region Methods
+
+ public static void PerformOn(Settings settings)
+ { }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Configuration/Settings.cs b/RGBSync+/Configuration/Settings.cs
new file mode 100644
index 0000000..c9f4981
--- /dev/null
+++ b/RGBSync+/Configuration/Settings.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using RGBSyncPlus.Model;
+
+namespace RGBSyncPlus.Configuration
+{
+ public class Settings
+ {
+ #region Constants
+
+ public const int CURRENT_VERSION = 1;
+
+ #endregion
+
+ #region Properties & Fields
+
+ public int Version { get; set; } = 0;
+
+ public double UpdateRate { get; set; } = 30.0;
+
+ public List SyncGroups { get; set; } = new List();
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Controls/BlurredDecorationWindow.cs b/RGBSync+/Controls/BlurredDecorationWindow.cs
new file mode 100644
index 0000000..ec0efc8
--- /dev/null
+++ b/RGBSync+/Controls/BlurredDecorationWindow.cs
@@ -0,0 +1,87 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace RGBSyncPlus.Controls
+{
+ [TemplatePart(Name = "PART_Decoration", Type = typeof(FrameworkElement))]
+ [TemplatePart(Name = "PART_Content", Type = typeof(FrameworkElement))]
+ [TemplatePart(Name = "PART_CloseButton", Type = typeof(Button))]
+ [TemplatePart(Name = "PART_MinimizeButton", Type = typeof(Button))]
+ [TemplatePart(Name = "PART_IconButton", Type = typeof(Button))]
+ public class BlurredDecorationWindow : Window
+ {
+ #region DependencyProperties
+ // ReSharper disable InconsistentNaming
+
+ public static readonly DependencyProperty BackgroundImageProperty = DependencyProperty.Register(
+ "BackgroundImage", typeof(ImageSource), typeof(BlurredDecorationWindow), new PropertyMetadata(default(ImageSource)));
+
+ public ImageSource BackgroundImage
+ {
+ get => (ImageSource)GetValue(BackgroundImageProperty);
+ set => SetValue(BackgroundImageProperty, value);
+ }
+
+ public static readonly DependencyProperty DecorationHeightProperty = DependencyProperty.Register(
+ "DecorationHeight", typeof(double), typeof(BlurredDecorationWindow), new PropertyMetadata(20.0));
+
+ public double DecorationHeight
+ {
+ get => (double)GetValue(DecorationHeightProperty);
+ set => SetValue(DecorationHeightProperty, value);
+ }
+
+ public static readonly DependencyProperty IconToolTipProperty = DependencyProperty.Register(
+ "IconToolTip", typeof(string), typeof(BlurredDecorationWindow), new PropertyMetadata(default(string)));
+
+ public string IconToolTip
+ {
+ get => (string)GetValue(IconToolTipProperty);
+ set => SetValue(IconToolTipProperty, value);
+ }
+
+ public static readonly DependencyProperty IconCommandProperty = DependencyProperty.Register(
+ "IconCommand", typeof(ICommand), typeof(BlurredDecorationWindow), new PropertyMetadata(default(ICommand)));
+
+ public ICommand IconCommand
+ {
+ get => (ICommand)GetValue(IconCommandProperty);
+ set => SetValue(IconCommandProperty, value);
+ }
+
+ // ReSharper restore InconsistentNaming
+ #endregion
+
+ #region Constructors
+
+ static BlurredDecorationWindow()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(BlurredDecorationWindow), new FrameworkPropertyMetadata(typeof(BlurredDecorationWindow)));
+ }
+
+ #endregion
+
+ #region Methods
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ if (GetTemplateChild("PART_Decoration") is FrameworkElement decoration)
+ decoration.MouseLeftButtonDown += (sender, args) => DragMove();
+
+ if (GetTemplateChild("PART_CloseButton") is Button closeButton)
+ closeButton.Click += (sender, args) => ApplicationManager.Instance.ExitCommand.Execute(null);
+
+ if (GetTemplateChild("PART_MinimizeButton") is Button minimizeButton)
+ minimizeButton.Click += (sender, args) => Hide();
+
+ if (GetTemplateChild("PART_IconButton") is Button iconButton)
+ iconButton.Click += (sender, args) => IconCommand?.Execute(null);
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Controls/ColorSelector.cs b/RGBSync+/Controls/ColorSelector.cs
new file mode 100644
index 0000000..1b7a635
--- /dev/null
+++ b/RGBSync+/Controls/ColorSelector.cs
@@ -0,0 +1,483 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using RGB.NET.Core;
+using Color = RGB.NET.Core.Color;
+using Point = System.Windows.Point;
+using Rectangle = System.Windows.Shapes.Rectangle;
+using WpfColor = System.Windows.Media.Color;
+
+namespace RGBSyncPlus.Controls
+{
+ [TemplatePart(Name = "PART_Selector", Type = typeof(Panel))]
+ [TemplatePart(Name = "PART_SliderAlpha", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderRed", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderGreen", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderBlue", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderHue", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderSaturation", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_SliderValue", Type = typeof(Slider))]
+ [TemplatePart(Name = "PART_Preview", Type = typeof(Rectangle))]
+ public class ColorSelector : Control
+ {
+ #region Properties & Fields
+
+ private bool _ignorePropertyChanged;
+ private bool _dragSelector;
+
+ private byte _a;
+ private byte _r;
+ private byte _g;
+ private byte _b;
+ private double _hue;
+ private double _saturation;
+ private double _value;
+
+ private Panel _selector;
+ private Rectangle _selectorColor;
+ private Grid _selectorGrip;
+ private Slider _sliderAlpha;
+ private Slider _sliderRed;
+ private Slider _sliderGreen;
+ private Slider _sliderBlue;
+ private Slider _sliderHue;
+ private Slider _sliderSaturation;
+ private Slider _sliderValue;
+ private Rectangle _preview;
+
+ private SolidColorBrush _previewBrush;
+ private SolidColorBrush _selectorBrush;
+ private LinearGradientBrush _alphaBrush;
+ private LinearGradientBrush _redBrush;
+ private LinearGradientBrush _greenBrush;
+ private LinearGradientBrush _blueBrush;
+ private LinearGradientBrush _hueBrush;
+ private LinearGradientBrush _saturationBrush;
+ private LinearGradientBrush _valueBrush;
+
+ #endregion
+
+ #region DependencyProperties
+
+ public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(
+ "SelectedColor", typeof(Color), typeof(ColorSelector), new FrameworkPropertyMetadata(new Color(255, 0, 0),
+ FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
+ SelectedColorChanged));
+
+ public Color SelectedColor
+ {
+ get => (Color)GetValue(SelectedColorProperty);
+ set => SetValue(SelectedColorProperty, value);
+ }
+
+ #endregion
+
+ #region Methods
+
+ public override void OnApplyTemplate()
+ {
+ if ((_selector = GetTemplateChild("PART_Selector") as Panel) != null)
+ {
+ _selectorBrush = new SolidColorBrush();
+ _selectorColor = new Rectangle
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ SnapsToDevicePixels = true,
+ StrokeThickness = 0,
+ Fill = _selectorBrush
+ };
+ _selector.Children.Add(_selectorColor);
+
+ Rectangle selectorWhite = new Rectangle
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ SnapsToDevicePixels = true,
+ StrokeThickness = 0,
+ Fill = new LinearGradientBrush(WpfColor.FromRgb(255, 255, 255), WpfColor.FromArgb(0, 255, 255, 255), new Point(0, 0.5), new Point(1, 0.5))
+ };
+ _selector.Children.Add(selectorWhite);
+
+ Rectangle selectorBlack = new Rectangle
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ SnapsToDevicePixels = true,
+ StrokeThickness = 0,
+ Fill = new LinearGradientBrush(WpfColor.FromRgb(0, 0, 0), WpfColor.FromArgb(0, 0, 0, 0), new Point(0.5, 1), new Point(0.5, 0))
+ };
+ _selector.Children.Add(selectorBlack);
+
+ _selectorGrip = new Grid
+ {
+ VerticalAlignment = VerticalAlignment.Bottom,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ SnapsToDevicePixels = true
+ };
+ _selectorGrip.Children.Add(new Ellipse
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ SnapsToDevicePixels = true,
+ Stroke = new SolidColorBrush(WpfColor.FromRgb(0, 0, 0)),
+ StrokeThickness = 2,
+ Fill = null,
+ Width = 12,
+ Height = 12
+ });
+ _selectorGrip.Children.Add(new Ellipse
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ SnapsToDevicePixels = true,
+ Stroke = new SolidColorBrush(WpfColor.FromRgb(255, 255, 255)),
+ StrokeThickness = 1,
+ Fill = null,
+ Width = 10,
+ Height = 10
+ });
+ _selector.Children.Add(_selectorGrip);
+
+ _selector.SizeChanged += (sender, args) => UpdateSelector();
+ _selector.MouseLeftButtonDown += (sender, args) =>
+ {
+ _dragSelector = true;
+ UpdateSelectorValue(args.GetPosition(_selector));
+ };
+ _selector.MouseEnter += (sender, args) =>
+ {
+ if (args.LeftButton == MouseButtonState.Pressed)
+ {
+ _dragSelector = true;
+ UpdateSelectorValue(args.GetPosition(_selector));
+ }
+ };
+ _selector.MouseLeftButtonUp += (sender, args) => _dragSelector = false;
+ _selector.MouseLeave += (sender, args) => _dragSelector = false;
+ _selector.MouseMove += (sender, args) => UpdateSelectorValue(args.GetPosition(_selector));
+ _selector.ClipToBounds = true;
+ }
+
+ if ((_sliderAlpha = GetTemplateChild("PART_SliderAlpha") as Slider) != null)
+ {
+ _alphaBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderAlpha.Background = _alphaBrush;
+ _sliderAlpha.ValueChanged += AChanged;
+ }
+
+ if ((_sliderRed = GetTemplateChild("PART_SliderRed") as Slider) != null)
+ {
+ _redBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderRed.Background = _redBrush;
+ _sliderRed.ValueChanged += RChanged;
+ }
+
+ if ((_sliderGreen = GetTemplateChild("PART_SliderGreen") as Slider) != null)
+ {
+ _greenBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderGreen.Background = _greenBrush;
+ _sliderGreen.ValueChanged += GChanged;
+ }
+
+ if ((_sliderBlue = GetTemplateChild("PART_SliderBlue") as Slider) != null)
+ {
+ _blueBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderBlue.Background = _blueBrush;
+ _sliderBlue.ValueChanged += BChanged;
+ }
+
+ if ((_sliderHue = GetTemplateChild("PART_SliderHue") as Slider) != null)
+ {
+ _hueBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1.0 / 6.0),
+ new GradientStop(new WpfColor(), 2.0 / 6.0),
+ new GradientStop(new WpfColor(), 3.0 / 6.0),
+ new GradientStop(new WpfColor(), 4.0 / 6.0),
+ new GradientStop(new WpfColor(), 5.0 / 6.0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderHue.Background = _hueBrush;
+ _sliderHue.ValueChanged += HueChanged;
+ }
+
+ if ((_sliderSaturation = GetTemplateChild("PART_SliderSaturation") as Slider) != null)
+ {
+ _saturationBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderSaturation.Background = _saturationBrush;
+ _sliderSaturation.ValueChanged += SaturationChanged;
+ }
+
+ if ((_sliderValue = GetTemplateChild("PART_SliderValue") as Slider) != null)
+ {
+ _valueBrush = new LinearGradientBrush(new GradientStopCollection(new[] { new GradientStop(new WpfColor(), 0),
+ new GradientStop(new WpfColor(), 1) }));
+ _sliderValue.Background = _valueBrush;
+ _sliderValue.ValueChanged += ValueChanged;
+ }
+
+ if ((_preview = GetTemplateChild("PART_Preview") as Rectangle) != null)
+ {
+ _previewBrush = new SolidColorBrush();
+ _preview.Fill = _previewBrush;
+ }
+
+ SetColor(SelectedColor);
+ }
+
+ private static void SelectedColorChanged(DependencyObject dependencyObject,
+ DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ if (!(dependencyObject is ColorSelector cs) || !(dependencyPropertyChangedEventArgs.NewValue is Color color)) return;
+ cs.SetColor(color);
+ }
+
+ private void SetColor(Color color)
+ {
+ if (_ignorePropertyChanged) return;
+
+ SetA(color);
+ SetRGB(color);
+ SetHSV(color);
+
+ UpdateSelector();
+ UpdateUIColors();
+ }
+
+ private void AChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _a = (byte)routedPropertyChangedEventArgs.NewValue.Clamp(0, byte.MaxValue);
+ Color color = new Color(_a, _r, _g, _b);
+ UpdateSelectedColor(color);
+ UpdateUIColors();
+ UpdateSelector();
+ }
+
+ private void RChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _r = (byte)routedPropertyChangedEventArgs.NewValue.Clamp(0, byte.MaxValue);
+ RGBChanged();
+ }
+
+ private void GChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _g = (byte)routedPropertyChangedEventArgs.NewValue.Clamp(0, byte.MaxValue);
+ RGBChanged();
+ }
+
+ private void BChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _b = (byte)routedPropertyChangedEventArgs.NewValue.Clamp(0, byte.MaxValue);
+ RGBChanged();
+ }
+
+ private void RGBChanged()
+ {
+ Color color = new Color(_a, _r, _g, _b);
+ UpdateSelectedColor(color);
+ SetHSV(color);
+ UpdateUIColors();
+ UpdateSelector();
+ }
+
+ private void HueChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _hue = routedPropertyChangedEventArgs.NewValue.Clamp(0, 360);
+ HSVChanged();
+ }
+
+ private void SaturationChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _saturation = routedPropertyChangedEventArgs.NewValue.Clamp(0, 1);
+ HSVChanged();
+ }
+
+ private void ValueChanged(object sender, RoutedPropertyChangedEventArgs routedPropertyChangedEventArgs)
+ {
+ if (_ignorePropertyChanged) return;
+
+ _value = routedPropertyChangedEventArgs.NewValue.Clamp(0, 1);
+ HSVChanged();
+ }
+
+ private void HSVChanged()
+ {
+ Color color = Color.FromHSV(_a, _hue, _saturation, _value);
+ UpdateSelectedColor(color);
+ SetRGB(color);
+ UpdateUIColors();
+ UpdateSelector();
+ }
+
+ private void SetA(Color color)
+ {
+ _ignorePropertyChanged = true;
+
+ _a = color.A;
+ if (_sliderAlpha != null)
+ _sliderAlpha.Value = _a;
+
+ _ignorePropertyChanged = false;
+ }
+
+ private void SetRGB(Color color)
+ {
+ _ignorePropertyChanged = true;
+
+ _r = color.R;
+ if (_sliderRed != null)
+ _sliderRed.Value = _r;
+
+ _g = color.G;
+ if (_sliderGreen != null)
+ _sliderGreen.Value = _g;
+
+ _b = color.B;
+ if (_sliderBlue != null)
+ _sliderBlue.Value = _b;
+
+ _ignorePropertyChanged = false;
+ }
+
+ private void SetHSV(Color color)
+ {
+ _ignorePropertyChanged = true;
+
+ _hue = color.Hue;
+ if (_sliderHue != null)
+ _sliderHue.Value = _hue;
+
+ _saturation = color.Saturation;
+ if (_sliderSaturation != null)
+ _sliderSaturation.Value = _saturation;
+
+ _value = color.Value;
+ if (_sliderValue != null)
+ _sliderValue.Value = _value;
+
+ _ignorePropertyChanged = false;
+ }
+
+ private void UpdateSelectedColor(Color color)
+ {
+ _ignorePropertyChanged = true;
+
+ SelectedColor = color;
+
+ _ignorePropertyChanged = false;
+ }
+
+ private void UpdateSelector()
+ {
+ if (_selector == null) return;
+
+ double selectorX = (_selector.ActualWidth * _saturation) - (_selectorGrip.ActualWidth / 2);
+ double selectorY = (_selector.ActualHeight * _value) - (_selectorGrip.ActualHeight / 2);
+ if (!double.IsNaN(selectorX) && !double.IsNaN(selectorY))
+ _selectorGrip.Margin = new Thickness(selectorX, 0, 0, selectorY);
+ }
+
+ private void UpdateSelectorValue(Point mouseLocation)
+ {
+ if (!_dragSelector) return;
+
+ double saturation = mouseLocation.X / _selector.ActualWidth;
+ double value = 1 - (mouseLocation.Y / _selector.ActualHeight);
+ if (!double.IsNaN(saturation) && !double.IsNaN(value))
+ {
+ _saturation = saturation;
+ _value = value;
+ HSVChanged();
+ }
+ }
+
+ private void UpdateUIColors()
+ {
+ Color hueColor = Color.FromHSV(_hue, 1, 1);
+
+ if (_previewBrush != null)
+ _previewBrush.Color = WpfColor.FromArgb(_a, _r, _g, _b);
+
+ if (_selectorBrush != null)
+ _selectorBrush.Color = WpfColor.FromRgb(hueColor.R, hueColor.G, hueColor.B);
+
+ if (_alphaBrush != null)
+ {
+ _alphaBrush.GradientStops[0].Color = WpfColor.FromArgb(0, _r, _g, _b);
+ _alphaBrush.GradientStops[1].Color = WpfColor.FromArgb(255, _r, _g, _b);
+ }
+
+ if (_redBrush != null)
+ {
+ _redBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, 0, _g, _b);
+ _redBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, 255, _g, _b);
+ }
+
+ if (_greenBrush != null)
+ {
+ _greenBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, _r, 0, _b);
+ _greenBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, _r, 255, _b);
+ }
+
+ if (_blueBrush != null)
+ {
+ _blueBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, _r, _g, 0);
+ _blueBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, _r, _g, 255);
+ }
+
+ if (_hueBrush != null)
+ {
+ Color referenceColor1 = Color.FromHSV(0, _saturation, _value);
+ Color referenceColor2 = Color.FromHSV(60, _saturation, _value);
+ Color referenceColor3 = Color.FromHSV(120, _saturation, _value);
+ Color referenceColor4 = Color.FromHSV(180, _saturation, _value);
+ Color referenceColor5 = Color.FromHSV(240, _saturation, _value);
+ Color referenceColor6 = Color.FromHSV(300, _saturation, _value);
+
+ _hueBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, referenceColor1.R, referenceColor1.G, referenceColor1.B);
+ _hueBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, referenceColor2.R, referenceColor2.G, referenceColor2.B);
+ _hueBrush.GradientStops[2].Color = WpfColor.FromArgb(_a, referenceColor3.R, referenceColor3.G, referenceColor3.B);
+ _hueBrush.GradientStops[3].Color = WpfColor.FromArgb(_a, referenceColor4.R, referenceColor4.G, referenceColor4.B);
+ _hueBrush.GradientStops[4].Color = WpfColor.FromArgb(_a, referenceColor5.R, referenceColor5.G, referenceColor5.B);
+ _hueBrush.GradientStops[5].Color = WpfColor.FromArgb(_a, referenceColor6.R, referenceColor6.G, referenceColor6.B);
+ _hueBrush.GradientStops[6].Color = WpfColor.FromArgb(_a, referenceColor1.R, referenceColor1.G, referenceColor1.B);
+ }
+
+ if (_saturationBrush != null)
+ {
+ Color referenceColor = Color.FromHSV(_hue, 1, _value);
+
+ _saturationBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, 255, 255, 255);
+ _saturationBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, referenceColor.R, referenceColor.G, referenceColor.B);
+ }
+
+ if (_valueBrush != null)
+ {
+ Color referenceColor = Color.FromHSV(_hue, _saturation, 1);
+
+ _valueBrush.GradientStops[0].Color = WpfColor.FromArgb(_a, 0, 0, 0);
+ _valueBrush.GradientStops[1].Color = WpfColor.FromArgb(_a, referenceColor.R, referenceColor.G, referenceColor.B);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Controls/Form.cs b/RGBSync+/Controls/Form.cs
new file mode 100644
index 0000000..2d8e939
--- /dev/null
+++ b/RGBSync+/Controls/Form.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace RGBSyncPlus.Controls
+{
+ public class Form : Panel
+ {
+ #region DependencyProperties
+ // ReSharper disable InconsistentNaming
+
+ public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register("RowHeight", typeof(double), typeof(Form),
+ new FrameworkPropertyMetadata(24.0, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public double RowHeight
+ {
+ get => (double)GetValue(RowHeightProperty);
+ set
+ {
+ if (value < 0) throw new ArgumentOutOfRangeException(nameof(RowHeight), "Row height can't be negative");
+ SetValue(RowHeightProperty, value);
+ }
+ }
+
+ public static readonly DependencyProperty LabelWidthProperty = DependencyProperty.Register("LabelWidth", typeof(double), typeof(Form),
+ new FrameworkPropertyMetadata(100.0, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public double LabelWidth
+ {
+ get => (double)GetValue(LabelWidthProperty);
+ set
+ {
+ if (value < 0) throw new ArgumentOutOfRangeException(nameof(RowHeight), "Label width can't be negative");
+ SetValue(LabelWidthProperty, value);
+ }
+ }
+
+ public static readonly DependencyProperty ElementSpacingProperty = DependencyProperty.Register("ElementSpacing", typeof(double), typeof(Form),
+ new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public double ElementSpacing
+ {
+ get => (double)GetValue(ElementSpacingProperty);
+ set => SetValue(ElementSpacingProperty, value);
+ }
+
+ public static readonly DependencyProperty RowSpacingProperty = DependencyProperty.Register("RowSpacing", typeof(double), typeof(Form),
+ new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public double RowSpacing
+ {
+ get => (double)GetValue(RowSpacingProperty);
+ set => SetValue(RowSpacingProperty, value);
+ }
+
+ // ReSharper restore InconsistentNaming
+ #endregion
+
+ #region AttachedProperties
+ // ReSharper disable InconsistentNaming
+
+ public static readonly DependencyProperty IsLabelProperty = DependencyProperty.RegisterAttached("IsLabel", typeof(bool), typeof(Form),
+ new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public static void SetIsLabel(UIElement element, bool value) => element.SetValue(IsLabelProperty, value);
+ public static bool GetIsLabel(UIElement element) => (bool)element.GetValue(IsLabelProperty);
+
+ public static readonly DependencyProperty LineBreaksProperty = DependencyProperty.RegisterAttached("LineBreaks", typeof(int), typeof(Form),
+ new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public static void SetLineBreaks(UIElement element, int value) => element.SetValue(LineBreaksProperty, value);
+ public static int GetLineBreaks(UIElement element) => (int)element.GetValue(LineBreaksProperty);
+
+ public static readonly DependencyProperty RowSpanProperty = DependencyProperty.RegisterAttached("RowSpan", typeof(int), typeof(Form),
+ new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public static void SetRowSpan(DependencyObject element, int value) => element.SetValue(RowSpanProperty, value);
+ public static int GetRowSpan(DependencyObject element) => (int)element.GetValue(RowSpanProperty);
+
+ public static readonly DependencyProperty FillProperty = DependencyProperty.RegisterAttached("Fill", typeof(bool), typeof(Form),
+ new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));
+
+ public static void SetFill(DependencyObject element, bool value) => element.SetValue(FillProperty, value);
+ public static bool GetFill(DependencyObject element) => (bool)element.GetValue(FillProperty);
+
+ // ReSharper restore InconsistentNaming
+ #endregion
+
+ #region Methods
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (InternalChildren.Count == 0) return new Size(0, 0);
+
+ FormLayout layout = new FormLayout(RowHeight, LabelWidth, ElementSpacing, RowSpacing);
+
+ foreach (UIElement child in InternalChildren)
+ {
+ child.Measure(availableSize);
+ layout.AddElement(child, 0);
+ }
+
+ return new Size(layout.Width, layout.Height);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (InternalChildren.Count == 0) return new Size(0, 0);
+
+ FormLayout layout = new FormLayout(RowHeight, LabelWidth, ElementSpacing, RowSpacing);
+
+ foreach (UIElement child in InternalChildren)
+ child.Arrange(layout.AddElement(child, finalSize.Width));
+
+ return new Size(finalSize.Width, layout.Height);
+ }
+
+ #endregion
+
+ #region Data
+
+ private class FormLayout
+ {
+ #region Properties & Fields
+
+ private readonly double _rowHeight;
+ private readonly double _labelWidth;
+ private readonly double _elementSpacing;
+ private readonly double _rowSpacing;
+
+ private double _currentRowWidth;
+
+ private int _newRows = 0;
+ private int _rows = -1;
+ private double _currentMaxWidth;
+ public double Width => Math.Max((Math.Max(_currentMaxWidth, _currentRowWidth) - _elementSpacing), 0);
+ public double Height => ((_rows + 1) * _rowHeight) + (_rows * _rowSpacing);
+
+ #endregion
+
+ #region Constructors
+
+ public FormLayout(double rowHeight, double labelWidth, double elementSpacing, double rowSpacing)
+ {
+ this._rowHeight = rowHeight;
+ this._labelWidth = labelWidth;
+ this._elementSpacing = elementSpacing;
+ this._rowSpacing = rowSpacing;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public Rect AddElement(UIElement element, double targetWidth)
+ {
+ bool isLabel = GetIsLabel(element);
+ int lineBreaks = GetLineBreaks(element);
+ int rowSpan = GetRowSpan(element);
+
+ double elementWidth = isLabel ? _labelWidth : element.DesiredSize.Width;
+ double height = _rowHeight;
+
+ if (_newRows > 0)
+ {
+ AddLineBreaks(_newRows);
+ _newRows = 0;
+ }
+
+ if (lineBreaks > 0) AddLineBreaks(lineBreaks);
+ else if (isLabel) AddLineBreaks(1);
+ else if (_rows < 0) _rows = 0;
+
+ if (!isLabel && (_currentRowWidth < _labelWidth))
+ _currentRowWidth = _labelWidth + _elementSpacing;
+
+ if (rowSpan > 1)
+ {
+ height = (rowSpan * _rowHeight) + ((rowSpan - 1) * _rowSpacing);
+ _newRows = Math.Max(_newRows, rowSpan - 1);
+ }
+
+ if (element is FrameworkElement fe)
+ fe.MaxHeight = height;
+
+ double width = elementWidth;
+ if ((targetWidth >= 1) && GetFill(element))
+ width = targetWidth - _currentRowWidth;
+
+ Rect rect = new Rect(new Point(_currentRowWidth, (_rows * _rowHeight) + (_rows * _rowSpacing)), new Size(width, height));
+
+ _currentRowWidth += width + _elementSpacing;
+
+ return rect;
+ }
+
+ private void AddLineBreaks(int count)
+ {
+ if (count <= 0) return;
+
+ _currentMaxWidth = Math.Max(_currentMaxWidth, _currentRowWidth);
+
+ _currentRowWidth = 0;
+ _rows += count;
+ }
+
+ #endregion
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Controls/GradientEditor.cs b/RGBSync+/Controls/GradientEditor.cs
new file mode 100644
index 0000000..854250d
--- /dev/null
+++ b/RGBSync+/Controls/GradientEditor.cs
@@ -0,0 +1,424 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using RGB.NET.Brushes.Gradients;
+using RGB.NET.Core;
+using Color = System.Windows.Media.Color;
+using Point = System.Windows.Point;
+using Size = System.Windows.Size;
+using Rectangle = System.Windows.Shapes.Rectangle;
+using GradientStop = RGB.NET.Brushes.Gradients.GradientStop;
+
+namespace RGBSyncPlus.Controls
+{
+ [TemplatePart(Name = "PART_Gradient", Type = typeof(Canvas))]
+ [TemplatePart(Name = "PART_Stops", Type = typeof(Canvas))]
+ public class GradientEditor : Control
+ {
+ #region Properties & Fields
+
+ private Canvas _gradientContainer;
+ private Canvas _stopContainer;
+ private readonly List _previewRectangles = new List();
+ private readonly Dictionary _stops = new Dictionary();
+ private ContentControl _draggingStop;
+ private AdornerLayer _adornerLayer;
+ private ColorPickerAdorner _adorner;
+ private Window _window;
+
+ #endregion
+
+ #region DepdencyProperties
+
+ public static readonly DependencyProperty GradientProperty = DependencyProperty.Register(
+ "Gradient", typeof(LinearGradient), typeof(GradientEditor), new FrameworkPropertyMetadata(null,
+ FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
+ OnGradientChanged));
+
+ public LinearGradient Gradient
+ {
+ get => (LinearGradient)GetValue(GradientProperty);
+ set => SetValue(GradientProperty, value);
+ }
+
+ public static readonly DependencyProperty GradientStopStyleProperty = DependencyProperty.Register(
+ "GradientStopStyle", typeof(Style), typeof(GradientEditor), new PropertyMetadata(default(Style)));
+
+ public Style GradientStopStyle
+ {
+ get => (Style)GetValue(GradientStopStyleProperty);
+ set => SetValue(GradientStopStyleProperty, value);
+ }
+
+ public static readonly DependencyProperty SelectedStopProperty = DependencyProperty.Register(
+ "SelectedStop", typeof(GradientStop), typeof(GradientEditor), new PropertyMetadata(default(GradientStop), SelectedStopChanged));
+
+ public GradientStop SelectedStop
+ {
+ get => (GradientStop)GetValue(SelectedStopProperty);
+ set => SetValue(SelectedStopProperty, value);
+ }
+
+ public static readonly DependencyProperty ColorSelectorTemplateProperty = DependencyProperty.Register(
+ "ColorSelectorTemplate", typeof(DataTemplate), typeof(GradientEditor), new PropertyMetadata(default(DataTemplate)));
+
+ public DataTemplate ColorSelectorTemplate
+ {
+ get => (DataTemplate)GetValue(ColorSelectorTemplateProperty);
+ set => SetValue(ColorSelectorTemplateProperty, value);
+ }
+
+ public static readonly DependencyProperty CanAddOrDeleteStopsProperty = DependencyProperty.Register(
+ "CanAddOrDeleteStops", typeof(bool), typeof(GradientEditor), new PropertyMetadata(true));
+
+ public bool CanAddOrDeleteStops
+ {
+ get => (bool)GetValue(CanAddOrDeleteStopsProperty);
+ set => SetValue(CanAddOrDeleteStopsProperty, value);
+ }
+
+ #endregion
+
+ #region AttachedProperties
+
+ public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.RegisterAttached(
+ "IsSelected", typeof(bool), typeof(GradientEditor), new PropertyMetadata(default(bool)));
+
+ public static void SetIsSelected(DependencyObject element, bool value) => element.SetValue(IsSelectedProperty, value);
+ public static bool GetIsSelected(DependencyObject element) => (bool)element.GetValue(IsSelectedProperty);
+
+ #endregion
+
+ #region Constructors
+
+ public GradientEditor()
+ {
+ if (Gradient == null)
+ Gradient = new LinearGradient();
+ }
+
+ #endregion
+
+ #region Methods
+
+ public override void OnApplyTemplate()
+ {
+ if ((_gradientContainer = GetTemplateChild("PART_Gradient") as Canvas) != null)
+ {
+ _gradientContainer.SizeChanged += (sender, args) => UpdateGradientPreview();
+ _gradientContainer.MouseDown += GradientContainerOnMouseDown;
+ }
+
+ if ((_stopContainer = GetTemplateChild("PART_Stops") as Canvas) != null)
+ _stopContainer.SizeChanged += (sender, args) => UpdateGradientStops();
+
+ _adornerLayer = AdornerLayer.GetAdornerLayer(this);
+ _window = Window.GetWindow(this);
+ if (_window != null)
+ {
+ _window.PreviewMouseDown += WindowMouseDown;
+ _window.PreviewKeyDown += (sender, args) =>
+ {
+ if (args.Key == Key.Escape)
+ SelectedStop = null;
+ };
+ }
+
+ UpdateGradientPreview();
+ UpdateGradientStops();
+ }
+
+ private void UpdateGradientPreview()
+ {
+ if ((_gradientContainer == null) || (Gradient == null)) return;
+
+ List gradientStops = Gradient.GradientStops.OrderBy(x => x.Offset).ToList();
+ if (gradientStops.Count == 0)
+ UpdatePreviewRectangleCount(gradientStops.Count);
+ else if (gradientStops.Count == 1)
+ {
+ UpdatePreviewRectangleCount(gradientStops.Count);
+ GradientStop firstStop = gradientStops[0];
+ UpdatePreviewRectangle(_previewRectangles[0], _gradientContainer.ActualWidth, _gradientContainer.ActualHeight, 0, 1, firstStop.Color, firstStop.Color);
+ }
+ else
+ {
+ UpdatePreviewRectangleCount(gradientStops.Count + 1);
+
+ GradientStop firstStop = gradientStops[0];
+ UpdatePreviewRectangle(_previewRectangles[0], _gradientContainer.ActualWidth, _gradientContainer.ActualHeight, 0, firstStop.Offset, firstStop.Color, firstStop.Color);
+ for (int i = 0; i < (gradientStops.Count - 1); i++)
+ {
+ GradientStop stop = gradientStops[i];
+ GradientStop nextStop = gradientStops[i + 1];
+ Rectangle rect = _previewRectangles[i + 1];
+ UpdatePreviewRectangle(rect, _gradientContainer.ActualWidth, _gradientContainer.ActualHeight, stop.Offset, nextStop.Offset, stop.Color, nextStop.Color);
+ }
+ GradientStop lastStop = gradientStops[gradientStops.Count - 1];
+ UpdatePreviewRectangle(_previewRectangles[_previewRectangles.Count - 1], _gradientContainer.ActualWidth, _gradientContainer.ActualHeight, lastStop.Offset, 1, lastStop.Color, lastStop.Color);
+ }
+ }
+
+ private void UpdatePreviewRectangle(Rectangle rect, double referenceWidth, double referenceHeight, double from, double to,
+ RGB.NET.Core.Color startColor, RGB.NET.Core.Color endColor)
+ {
+ rect.Fill = new LinearGradientBrush(Color.FromArgb(startColor.A, startColor.R, startColor.G, startColor.B),
+ Color.FromArgb(endColor.A, endColor.R, endColor.G, endColor.B),
+ new Point(0, 0.5), new Point(1, 0.5));
+
+ //DarthAffe 09.02.2018: Forced rounding to prevent render issues on resize
+ Canvas.SetLeft(rect, Math.Floor(referenceWidth * from.Clamp(0, 1)));
+ rect.Width = Math.Ceiling(referenceWidth * (to.Clamp(0, 1) - from.Clamp(0, 1)));
+
+ Canvas.SetTop(rect, 0);
+ rect.Height = referenceHeight;
+ }
+
+ private void UpdatePreviewRectangleCount(int gradientCount)
+ {
+ int countDiff = gradientCount - _previewRectangles.Count;
+ if (countDiff > 0)
+ for (int i = 0; i < countDiff; i++)
+ {
+ Rectangle rect = new Rectangle { VerticalAlignment = VerticalAlignment.Stretch };
+ _previewRectangles.Add(rect);
+ _gradientContainer.Children.Add(rect);
+ }
+
+ if (countDiff < 0)
+ for (int i = 0; i < Math.Abs(countDiff); i++)
+ {
+ int index = _previewRectangles.Count - i - 1;
+ Rectangle rect = _previewRectangles[index];
+ _previewRectangles.RemoveAt(index);
+ _gradientContainer.Children.Remove(rect);
+ }
+ }
+
+ private void UpdateGradientStops()
+ {
+ if (Gradient == null) return;
+
+ List gradientStops = Gradient.GradientStops.OrderBy(x => x.Offset).ToList();
+ UpdateGradientStopsCount(gradientStops);
+ foreach (GradientStop stop in gradientStops)
+ UpdateGradientStop(_stops[stop], _stopContainer.ActualWidth, _stopContainer.ActualHeight, stop);
+ }
+
+ private void UpdateGradientStop(ContentControl control, double referenceWidth, double referenceHeight, GradientStop stop)
+ {
+ control.Background = new SolidColorBrush(Color.FromArgb(stop.Color.A, stop.Color.R, stop.Color.G, stop.Color.B));
+
+ Canvas.SetLeft(control, (referenceWidth * stop.Offset.Clamp(0, 1)) - (control.Width / 2.0));
+
+ Canvas.SetTop(control, 0);
+ control.Height = referenceHeight;
+ }
+
+ private void UpdateGradientStopsCount(List gradientStops)
+ {
+ foreach (GradientStop stop in gradientStops)
+ {
+ if (!_stops.ContainsKey(stop))
+ {
+ ContentControl control = new ContentControl
+ {
+ VerticalAlignment = VerticalAlignment.Stretch,
+ Style = GradientStopStyle,
+ Content = stop
+ };
+ control.MouseDown += GradientStopOnMouseDown;
+ _stops.Add(stop, control);
+ _stopContainer.Children.Add(control);
+ }
+ }
+
+ List stopsToRemove = new List();
+ foreach (KeyValuePair stopPair in _stops)
+ if (!gradientStops.Contains(stopPair.Key))
+ {
+ ContentControl control = stopPair.Value;
+ control.MouseDown -= GradientStopOnMouseDown;
+ stopsToRemove.Add(stopPair.Key);
+ _stopContainer.Children.Remove(control);
+ }
+
+ foreach (GradientStop stop in stopsToRemove)
+ _stops.Remove(stop);
+ }
+
+ private void AttachGradient(AbstractGradient gradient) => gradient.GradientChanged += GradientChanged;
+ private void DetachGradient(AbstractGradient gradient) => gradient.GradientChanged -= GradientChanged;
+
+ private void GradientChanged(object o, EventArgs eventArgs)
+ {
+ UpdateGradientPreview();
+ UpdateGradientStops();
+ }
+
+ private static void OnGradientChanged(DependencyObject dependencyObject,
+ DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ if (!(dependencyObject is GradientEditor ge)) return;
+
+ if (dependencyPropertyChangedEventArgs.OldValue is AbstractGradient oldGradient)
+ ge.DetachGradient(oldGradient);
+
+ if (dependencyPropertyChangedEventArgs.NewValue is AbstractGradient newGradient)
+ ge.AttachGradient(newGradient);
+ }
+
+ private void GradientContainerOnMouseDown(object o, MouseButtonEventArgs mouseButtonEventArgs)
+ {
+ if ((mouseButtonEventArgs.ChangedButton != MouseButton.Left) || (Gradient == null) || !CanAddOrDeleteStops) return;
+
+ double offset = mouseButtonEventArgs.GetPosition(_gradientContainer).X / _gradientContainer.ActualWidth;
+ RGB.NET.Core.Color color = Gradient.GetColor(offset);
+ GradientStop newStop = new GradientStop(offset, color);
+ Gradient.GradientStops.Add(newStop);
+ SelectedStop = newStop;
+ }
+
+ private void GradientStopOnMouseDown(object o, MouseButtonEventArgs mouseButtonEventArgs)
+ {
+ if (!((o as ContentControl)?.Content is GradientStop stop) || (Gradient == null)) return;
+
+ if (mouseButtonEventArgs.ChangedButton == MouseButton.Right)
+ {
+ if (CanAddOrDeleteStops)
+ Gradient.GradientStops.Remove(stop);
+ }
+ else if (mouseButtonEventArgs.ChangedButton == MouseButton.Left)
+ {
+ SelectedStop = stop;
+ _draggingStop = (ContentControl)o;
+ }
+ }
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ if (_draggingStop?.Content is GradientStop stop)
+ {
+ double location = e.GetPosition(_gradientContainer).X;
+ stop.Offset = (location / _gradientContainer.ActualWidth).Clamp(0, 1);
+ }
+ }
+
+ protected override void OnMouseLeave(MouseEventArgs e)
+ {
+ base.OnMouseLeave(e);
+
+ _draggingStop = null;
+ }
+
+ protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
+ {
+ base.OnMouseLeftButtonUp(e);
+
+ _draggingStop = null;
+ }
+
+ private static void SelectedStopChanged(DependencyObject dependencyObject,
+ DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
+ {
+ if (!(dependencyObject is GradientEditor gradientEditor)) return;
+
+ if (gradientEditor._adorner != null)
+ gradientEditor._adornerLayer.Remove(gradientEditor._adorner);
+
+ if (dependencyPropertyChangedEventArgs.OldValue is GradientStop oldStop)
+ {
+ if (gradientEditor._stops.TryGetValue(oldStop, out ContentControl oldcontrol))
+ SetIsSelected(oldcontrol, false);
+ }
+
+ if (dependencyPropertyChangedEventArgs.NewValue is GradientStop stop)
+ {
+ ContentControl stopContainer = gradientEditor._stops[stop];
+ SetIsSelected(stopContainer, true);
+
+ if (gradientEditor._adornerLayer != null)
+ {
+ ContentControl contentControl = new ContentControl
+ {
+ ContentTemplate = gradientEditor.ColorSelectorTemplate,
+ Content = stop
+ };
+
+ ColorPickerAdorner adorner = new ColorPickerAdorner(stopContainer, contentControl);
+ gradientEditor._adorner = adorner;
+ gradientEditor._adornerLayer.Add(adorner);
+ }
+ }
+ }
+
+ private void WindowMouseDown(object o, MouseButtonEventArgs mouseButtonEventArgs)
+ {
+ if ((_adorner != null) && (VisualTreeHelper.HitTest(_adorner, mouseButtonEventArgs.GetPosition(_adorner)) == null))
+ SelectedStop = null;
+ }
+
+ #endregion
+ }
+
+ public class ColorPickerAdorner : Adorner
+ {
+ #region Properties & Fields
+
+ private readonly VisualCollection _visualChildren;
+ private readonly FrameworkElement _colorSelector;
+ protected override int VisualChildrenCount => 1;
+ protected override Visual GetVisualChild(int index) => _colorSelector;
+
+ #endregion
+
+ #region Constructors
+
+ public ColorPickerAdorner(UIElement adornedElement, FrameworkElement colorSelector)
+ : base(adornedElement)
+ {
+ this._colorSelector = colorSelector;
+
+ _visualChildren = new VisualCollection(this) { colorSelector };
+ }
+
+ #endregion
+
+ #region Methods
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ Window referenceWindow = Window.GetWindow(AdornedElement);
+ Point referenceLocation = AdornedElement.TranslatePoint(new Point(0, 0), referenceWindow);
+
+ double referenceWidth = ((FrameworkElement)AdornedElement).ActualWidth / 2.0;
+ double referenceHeight = ((FrameworkElement)AdornedElement).Height;
+ double referenceX = referenceLocation.X + referenceWidth;
+ double halfWidth = finalSize.Width / 2.0;
+ double maxOffset = referenceWindow.Width - halfWidth;
+
+ double offset = (referenceX < halfWidth ? referenceX
+ : (((referenceX + (referenceWidth * 2)) > maxOffset)
+ ? halfWidth - ((maxOffset - referenceX) - (referenceWidth * 2))
+ : halfWidth));
+
+ _colorSelector.Arrange(new Rect(new Point(referenceWidth - offset, referenceHeight), finalSize));
+ return _colorSelector.RenderSize;
+ }
+
+ protected override Size MeasureOverride(Size constraint)
+ {
+ _colorSelector.Measure(constraint);
+ return _colorSelector.DesiredSize;
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Controls/ImageButton.cs b/RGBSync+/Controls/ImageButton.cs
new file mode 100644
index 0000000..2bd0f4e
--- /dev/null
+++ b/RGBSync+/Controls/ImageButton.cs
@@ -0,0 +1,51 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace RGBSyncPlus.Controls
+{
+ public class ImageButton : Button
+ {
+ #region Properties & Fields
+ // ReSharper disable InconsistentNaming
+
+ public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(
+ "Image", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(default(ImageSource)));
+
+ public ImageSource Image
+ {
+ get => (ImageSource)GetValue(ImageProperty);
+ set => SetValue(ImageProperty, value);
+ }
+
+ public static readonly DependencyProperty HoverImageProperty = DependencyProperty.Register(
+ "HoverImage", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(default(ImageSource)));
+
+ public ImageSource HoverImage
+ {
+ get => (ImageSource)GetValue(HoverImageProperty);
+ set => SetValue(HoverImageProperty, value);
+ }
+
+ public static readonly DependencyProperty PressedImageProperty = DependencyProperty.Register(
+ "PressedImage", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(default(ImageSource)));
+
+ public ImageSource PressedImage
+ {
+ get => (ImageSource)GetValue(PressedImageProperty);
+ set => SetValue(PressedImageProperty, value);
+ }
+
+ // ReSharper restore InconsistentNaming
+ #endregion
+
+ #region Constructors
+
+ static ImageButton()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton), new FrameworkPropertyMetadata(typeof(ImageButton)));
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Converter/BoolToVisibilityConverter.cs b/RGBSync+/Converter/BoolToVisibilityConverter.cs
new file mode 100644
index 0000000..04f4f2b
--- /dev/null
+++ b/RGBSync+/Converter/BoolToVisibilityConverter.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RGBSyncPlus.Converter
+{
+ [ValueConversion(typeof(bool), typeof(Visibility))]
+ public class BoolToVisibilityConverter : IValueConverter
+ {
+ #region Methods
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ => (value as bool?) == true ? Visibility.Visible
+ : (string.Equals(parameter?.ToString(), "true", StringComparison.OrdinalIgnoreCase) ? Visibility.Hidden : Visibility.Collapsed);
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value as Visibility? == Visibility.Visible;
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Converter/EqualsToBoolConverter.cs b/RGBSync+/Converter/EqualsToBoolConverter.cs
new file mode 100644
index 0000000..32373c9
--- /dev/null
+++ b/RGBSync+/Converter/EqualsToBoolConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace RGBSyncPlus.Converter
+{
+ [ValueConversion(typeof(object), typeof(bool))]
+ public class EqualsToBoolConverter : IValueConverter
+ {
+ #region Methods
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => Equals(value, parameter);
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException();
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Converter/NullToVisibilityConverter.cs b/RGBSync+/Converter/NullToVisibilityConverter.cs
new file mode 100644
index 0000000..6fc33f9
--- /dev/null
+++ b/RGBSync+/Converter/NullToVisibilityConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RGBSyncPlus.Converter
+{
+ [ValueConversion(typeof(object), typeof(Visibility))]
+ public class NullToVisibilityConverter : IValueConverter
+ {
+ #region Methods
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ => (value == null) == (string.Equals(parameter?.ToString(), "true", StringComparison.OrdinalIgnoreCase)) ? Visibility.Visible : Visibility.Hidden;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException();
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Converter/ScrollOffsetToOpacityMaskConverter.cs b/RGBSync+/Converter/ScrollOffsetToOpacityMaskConverter.cs
new file mode 100644
index 0000000..cc0252f
--- /dev/null
+++ b/RGBSync+/Converter/ScrollOffsetToOpacityMaskConverter.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace RGBSyncPlus.Converter
+{
+ public class ScrollOffsetToOpacityMaskConverter : IMultiValueConverter
+ {
+ #region Constants
+
+ private static readonly Color TRANSPARENT = Color.FromArgb(0, 0, 0, 0);
+ private static readonly Color OPAQUE = Color.FromArgb(255, 0, 0, 0);
+
+ #endregion
+
+ #region Methods
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ double offset = double.Parse(values[0].ToString());
+ double maxHeight = double.Parse(values[1].ToString());
+ double height = double.Parse(values[2].ToString());
+
+ double transparencyHeight = double.Parse(parameter.ToString());
+ double transparencyFactor = (transparencyHeight - 6) / height;
+ double transparencyFadeFactor = (transparencyHeight + 4) / height;
+
+ bool top = !(Math.Abs(offset) < float.Epsilon);
+ bool bot = !(Math.Abs(offset - maxHeight) < float.Epsilon);
+
+ if (!top && !bot) return new SolidColorBrush(OPAQUE);
+
+ GradientStopCollection gradientStops = new GradientStopCollection();
+ if (top)
+ {
+ gradientStops.Add(new GradientStop(TRANSPARENT, 0.0));
+ gradientStops.Add(new GradientStop(TRANSPARENT, transparencyFactor));
+ gradientStops.Add(new GradientStop(OPAQUE, transparencyFadeFactor));
+ }
+ else
+ gradientStops.Add(new GradientStop(OPAQUE, 0.0));
+
+ if (bot)
+ {
+ gradientStops.Add(new GradientStop(OPAQUE, 1.0 - transparencyFadeFactor));
+ gradientStops.Add(new GradientStop(TRANSPARENT, 1.0 - transparencyFactor));
+ gradientStops.Add(new GradientStop(TRANSPARENT, 1.0));
+ }
+ else
+ gradientStops.Add(new GradientStop(OPAQUE, 1.0));
+
+ return new LinearGradientBrush(gradientStops, new Point(0.5, 0.0), new Point(0.5, 1.0));
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Converter/ScrollOffsetToVisibilityConverter.cs b/RGBSync+/Converter/ScrollOffsetToVisibilityConverter.cs
new file mode 100644
index 0000000..0eb286f
--- /dev/null
+++ b/RGBSync+/Converter/ScrollOffsetToVisibilityConverter.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace RGBSyncPlus.Converter
+{
+ // Based on: http://stackoverflow.com/a/28679767
+ public class ScrollOffsetToVisibilityConverter : IMultiValueConverter
+ {
+ #region Methods
+
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ bool top = "top".Equals(parameter?.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ double offset = double.Parse(values[0].ToString());
+ double maxHeight = double.Parse(values[1].ToString());
+
+ return (top && Math.Abs(offset) < float.Epsilon) || (!top && Math.Abs(offset - maxHeight) < float.Epsilon)
+ ? Visibility.Collapsed
+ : Visibility.Visible;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotSupportedException();
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Helper/ActionCommand.cs b/RGBSync+/Helper/ActionCommand.cs
new file mode 100644
index 0000000..537357b
--- /dev/null
+++ b/RGBSync+/Helper/ActionCommand.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Windows.Input;
+
+namespace RGBSyncPlus.Helper
+{
+ public class ActionCommand : ICommand
+ {
+ #region Properties & Fields
+
+ private readonly Func _canExecute;
+ private readonly Action _command;
+
+ #endregion
+
+ #region Events
+
+ public event EventHandler CanExecuteChanged;
+
+ #endregion
+
+ #region Constructors
+
+ public ActionCommand(Action command, Func canExecute = null)
+ {
+ this._command = command;
+ this._canExecute = canExecute;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
+
+ public void Execute(object parameter) => _command?.Invoke();
+
+ public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, new EventArgs());
+
+ #endregion
+ }
+
+ public class ActionCommand : ICommand
+ {
+ #region Properties & Fields
+
+ private readonly Func _canExecute;
+ private readonly Action _command;
+
+ #endregion
+
+ #region Events
+
+ public event EventHandler CanExecuteChanged;
+
+ #endregion
+
+ #region Constructors
+
+ public ActionCommand(Action command, Func canExecute = null)
+ {
+ this._command = command;
+ this._canExecute = canExecute;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public bool CanExecute(object parameter) => _canExecute?.Invoke((T)parameter) ?? true;
+
+ public void Execute(object parameter) => _command?.Invoke((T)parameter);
+
+ public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, new EventArgs());
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Helper/ExceptionExtension.cs b/RGBSync+/Helper/ExceptionExtension.cs
new file mode 100644
index 0000000..e2d566f
--- /dev/null
+++ b/RGBSync+/Helper/ExceptionExtension.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace RGBSyncPlus.Helper
+{
+ public static class ExceptionExtension
+ {
+ #region Methods
+
+ public static string GetFullMessage(this Exception ex, string message = "")
+ {
+ if (ex == null) return string.Empty;
+
+ message += ex.Message;
+
+ if (ex.InnerException != null)
+ message += "\r\nInnerException: " + GetFullMessage(ex.InnerException);
+
+ return message;
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Helper/MathHelper.cs b/RGBSync+/Helper/MathHelper.cs
new file mode 100644
index 0000000..6ef4415
--- /dev/null
+++ b/RGBSync+/Helper/MathHelper.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace RGBSyncPlus.Helper
+{
+ public static class MathHelper
+ {
+ #region Methods
+
+ public static double Clamp(double value, double min, double max) => Math.Max(min, Math.Min(max, value));
+ public static float Clamp(float value, float min, float max) => (float)Clamp((double)value, min, max);
+ public static int Clamp(int value, int min, int max) => Math.Max(min, Math.Min(max, value));
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Helper/RGBNetExtension.cs b/RGBSync+/Helper/RGBNetExtension.cs
new file mode 100644
index 0000000..1a5ea8c
--- /dev/null
+++ b/RGBSync+/Helper/RGBNetExtension.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Linq;
+using RGB.NET.Core;
+using RGBSyncPlus.Model;
+
+namespace RGBSyncPlus.Helper
+{
+ public static class RGBNetExtension
+ {
+ public static string GetDeviceName(this IRGBDevice device) => $"{device.DeviceInfo.Manufacturer} {device.DeviceInfo.Model} ({device.DeviceInfo.DeviceType})";
+
+ public static IEnumerable GetLeds(this IEnumerable syncLeds)
+ => syncLeds.Select(GetLed).Where(led => led != null);
+
+ public static Led GetLed(this SyncLed syncLed)
+ {
+ if (syncLed == null) return null;
+ return RGBSurface.Instance.Leds.FirstOrDefault(l => (l.Id == syncLed.LedId) && (l.Device.GetDeviceName() == syncLed.Device));
+ }
+ }
+}
diff --git a/RGBSync+/Model/SyncGroup.cs b/RGBSync+/Model/SyncGroup.cs
new file mode 100644
index 0000000..827772d
--- /dev/null
+++ b/RGBSync+/Model/SyncGroup.cs
@@ -0,0 +1,48 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using Newtonsoft.Json;
+using RGB.NET.Core;
+using RGB.NET.Groups;
+
+namespace RGBSyncPlus.Model
+{
+ public class SyncGroup : AbstractBindable
+ {
+ #region Properties & Fields
+
+ public string DisplayName => string.IsNullOrWhiteSpace(Name) ? "(unnamed)" : Name;
+
+ private string _name;
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (SetProperty(ref _name, value))
+ OnPropertyChanged(nameof(DisplayName));
+ }
+ }
+
+ private SyncLed _syncLed;
+ public SyncLed SyncLed
+ {
+ get => _syncLed;
+ set => SetProperty(ref _syncLed, value);
+ }
+
+ private ObservableCollection _leds = new ObservableCollection();
+ public ObservableCollection Leds
+ {
+ get => _leds;
+ set => SetProperty(ref _leds, value);
+ }
+
+ [JsonIgnore]
+ public ListLedGroup LedGroup { get; set; }
+
+ [JsonIgnore]
+ public NotifyCollectionChangedEventHandler LedsChangedEventHandler { get; set; }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Model/SyncLed.cs b/RGBSync+/Model/SyncLed.cs
new file mode 100644
index 0000000..fdd5dee
--- /dev/null
+++ b/RGBSync+/Model/SyncLed.cs
@@ -0,0 +1,80 @@
+using Newtonsoft.Json;
+using RGB.NET.Core;
+using RGBSyncPlus.Helper;
+
+namespace RGBSyncPlus.Model
+{
+ public class SyncLed : AbstractBindable
+ {
+ #region Properties & Fields
+
+ private string _device;
+ public string Device
+ {
+ get => _device;
+ set => SetProperty(ref _device, value);
+ }
+
+ private LedId _ledId;
+ public LedId LedId
+ {
+ get => _ledId;
+ set => SetProperty(ref _ledId, value);
+ }
+
+ private Led _led;
+ [JsonIgnore]
+ public Led Led
+ {
+ get => _led;
+ set => SetProperty(ref _led, value);
+ }
+
+ #endregion
+
+ #region Constructors
+
+ public SyncLed()
+ { }
+
+ public SyncLed(string device, LedId ledId)
+ {
+ this.Device = device;
+ this.LedId = ledId;
+ }
+
+ public SyncLed(Led led)
+ {
+ this.Device = led.Device.GetDeviceName();
+ this.LedId = led.Id;
+ this.Led = led;
+ }
+
+ #endregion
+
+ #region Methods
+
+ protected bool Equals(SyncLed other) => string.Equals(_device, other._device) && (_ledId == other._ledId);
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((SyncLed)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((_device != null ? _device.GetHashCode() : 0) * 397) ^ (int)_ledId;
+ }
+ }
+
+ public static bool operator ==(SyncLed left, SyncLed right) => Equals(left, right);
+ public static bool operator !=(SyncLed left, SyncLed right) => !Equals(left, right);
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Properties/AssemblyInfo.cs b/RGBSync+/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..c1f69b1
--- /dev/null
+++ b/RGBSync+/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("RGBSync+")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("RGBSync+")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/RGBSync+/Properties/Resources.Designer.cs b/RGBSync+/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..03689b5
--- /dev/null
+++ b/RGBSync+/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace RGBSyncPlus.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RGBSyncPlus.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/RGBSync+/Properties/Resources.resx b/RGBSync+/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/RGBSync+/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/RGBSync+/Properties/Settings.Designer.cs b/RGBSync+/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..5ca9da0
--- /dev/null
+++ b/RGBSync+/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace RGBSyncPlus.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/RGBSync+/Properties/Settings.settings b/RGBSync+/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/RGBSync+/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/RGBSync+.csproj b/RGBSync+/RGBSync+.csproj
new file mode 100644
index 0000000..b1c4716
--- /dev/null
+++ b/RGBSync+/RGBSync+.csproj
@@ -0,0 +1,236 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C6BF4357-07D9-496B-9630-A26568D30723}
+ WinExe
+ RGBSyncPlus
+ RGBSync+
+ v4.5
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+
+
+ x86
+ true
+ full
+ false
+ ..\bin\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ x86
+ pdbonly
+ true
+ ..\bin\
+ TRACE
+ prompt
+ 4
+
+
+ RGBSyncPlus.App
+
+
+ Resources\argebee.ico
+
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ConfigurationWindow.xaml
+
+
+
+
+
+
+
+
+
+
+ App.xaml
+ Code
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
+
+
+ 1.1.0
+
+
+ 1.0.8
+
+
+ 11.0.2
+
+
+ 0.0.1.54
+
+
+ 0.0.1.54
+
+
+ 0.0.1.54
+
+
+ 0.0.1.54
+
+
+ 4.5.0
+
+
+ 1.0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/RGBSync+.sln b/RGBSync+/RGBSync+.sln
new file mode 100644
index 0000000..78c7140
--- /dev/null
+++ b/RGBSync+/RGBSync+.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27703.2018
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RGBSync+", "RGBSync+.csproj", "{C6BF4357-07D9-496B-9630-A26568D30723}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C6BF4357-07D9-496B-9630-A26568D30723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C6BF4357-07D9-496B-9630-A26568D30723}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6BF4357-07D9-496B-9630-A26568D30723}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C6BF4357-07D9-496B-9630-A26568D30723}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3AD1163D-9F77-43AB-A3BA-38C1B9CCBA87}
+ EndGlobalSection
+EndGlobal
diff --git a/RGBSync+/Resources/RGBSync+.xaml b/RGBSync+/Resources/RGBSync+.xaml
new file mode 100644
index 0000000..22b2895
--- /dev/null
+++ b/RGBSync+/Resources/RGBSync+.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RGBSync+/Resources/argebee.ico b/RGBSync+/Resources/argebee.ico
new file mode 100644
index 0000000..d9af7a0
Binary files /dev/null and b/RGBSync+/Resources/argebee.ico differ
diff --git a/RGBSync+/Resources/arrow_down.png b/RGBSync+/Resources/arrow_down.png
new file mode 100644
index 0000000..d55d81b
Binary files /dev/null and b/RGBSync+/Resources/arrow_down.png differ
diff --git a/RGBSync+/Resources/arrow_up.png b/RGBSync+/Resources/arrow_up.png
new file mode 100644
index 0000000..c424296
Binary files /dev/null and b/RGBSync+/Resources/arrow_up.png differ
diff --git a/RGBSync+/Resources/background.png b/RGBSync+/Resources/background.png
new file mode 100644
index 0000000..3ab6966
Binary files /dev/null and b/RGBSync+/Resources/background.png differ
diff --git a/RGBSync+/Resources/close.png b/RGBSync+/Resources/close.png
new file mode 100644
index 0000000..e166bd7
Binary files /dev/null and b/RGBSync+/Resources/close.png differ
diff --git a/RGBSync+/Resources/font.ttf b/RGBSync+/Resources/font.ttf
new file mode 100644
index 0000000..65d7240
Binary files /dev/null and b/RGBSync+/Resources/font.ttf differ
diff --git a/RGBSync+/Resources/minimize.png b/RGBSync+/Resources/minimize.png
new file mode 100644
index 0000000..78910ae
Binary files /dev/null and b/RGBSync+/Resources/minimize.png differ
diff --git a/RGBSync+/Resources/navigation.ttf b/RGBSync+/Resources/navigation.ttf
new file mode 100644
index 0000000..6161253
Binary files /dev/null and b/RGBSync+/Resources/navigation.ttf differ
diff --git a/RGBSync+/Styles/BlurredDecorationWindow.xaml b/RGBSync+/Styles/BlurredDecorationWindow.xaml
new file mode 100644
index 0000000..4738851
--- /dev/null
+++ b/RGBSync+/Styles/BlurredDecorationWindow.xaml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/Button.xaml b/RGBSync+/Styles/Button.xaml
new file mode 100644
index 0000000..4393b6c
--- /dev/null
+++ b/RGBSync+/Styles/Button.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/CachedResourceDictionary.cs b/RGBSync+/Styles/CachedResourceDictionary.cs
new file mode 100644
index 0000000..c35a946
--- /dev/null
+++ b/RGBSync+/Styles/CachedResourceDictionary.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Windows;
+
+namespace RGBSyncPlus.Styles
+{
+ public class CachedResourceDictionary : ResourceDictionary
+ {
+ #region Properties & Fields
+
+ // ReSharper disable InconsistentNaming
+ private static readonly List _cachedDictionaries = new List();
+ private static readonly ResourceDictionary _innerDictionary = new ResourceDictionary();
+ // ReSharper restore
+
+ public new Uri Source
+ {
+ get => null;
+ set
+ {
+ lock (_innerDictionary)
+ {
+ UpdateCache(value);
+
+ MergedDictionaries.Clear();
+ MergedDictionaries.Add(_innerDictionary);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Methods
+
+ private static void UpdateCache(Uri source)
+ {
+ string uriPath = source.OriginalString;
+ if (_cachedDictionaries.Contains(uriPath)) return;
+
+ _cachedDictionaries.Add(uriPath);
+
+ ResourceDictionary newDictionary = new ResourceDictionary { Source = new Uri(uriPath, source.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative) };
+ CopyDictionaryEntries(newDictionary, _innerDictionary);
+ }
+
+ private static void CopyDictionaryEntries(IDictionary source, IDictionary target)
+ {
+ foreach (object key in source.Keys)
+ if (!target.Contains(key))
+ target.Add(key, source[key]);
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/Styles/ColorSelector.xaml b/RGBSync+/Styles/ColorSelector.xaml
new file mode 100644
index 0000000..1175f00
--- /dev/null
+++ b/RGBSync+/Styles/ColorSelector.xaml
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/ComboBox.xaml b/RGBSync+/Styles/ComboBox.xaml
new file mode 100644
index 0000000..f8a2811
--- /dev/null
+++ b/RGBSync+/Styles/ComboBox.xaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/Form.xaml b/RGBSync+/Styles/Form.xaml
new file mode 100644
index 0000000..5a47e2e
--- /dev/null
+++ b/RGBSync+/Styles/Form.xaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/FrameworkElement.xaml b/RGBSync+/Styles/FrameworkElement.xaml
new file mode 100644
index 0000000..107ef2e
--- /dev/null
+++ b/RGBSync+/Styles/FrameworkElement.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/RGBSync+/Styles/GradientEditor.xaml b/RGBSync+/Styles/GradientEditor.xaml
new file mode 100644
index 0000000..bc28fb6
--- /dev/null
+++ b/RGBSync+/Styles/GradientEditor.xaml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/GroupBox.xaml b/RGBSync+/Styles/GroupBox.xaml
new file mode 100644
index 0000000..beed6d2
--- /dev/null
+++ b/RGBSync+/Styles/GroupBox.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/ImageButton.xaml b/RGBSync+/Styles/ImageButton.xaml
new file mode 100644
index 0000000..68edb06
--- /dev/null
+++ b/RGBSync+/Styles/ImageButton.xaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/ListBox.xaml b/RGBSync+/Styles/ListBox.xaml
new file mode 100644
index 0000000..16d8bef
--- /dev/null
+++ b/RGBSync+/Styles/ListBox.xaml
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/Navigation.xaml b/RGBSync+/Styles/Navigation.xaml
new file mode 100644
index 0000000..94339c0
--- /dev/null
+++ b/RGBSync+/Styles/Navigation.xaml
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/Slider.xaml b/RGBSync+/Styles/Slider.xaml
new file mode 100644
index 0000000..21eeea1
--- /dev/null
+++ b/RGBSync+/Styles/Slider.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/TextBox.xaml b/RGBSync+/Styles/TextBox.xaml
new file mode 100644
index 0000000..02b7861
--- /dev/null
+++ b/RGBSync+/Styles/TextBox.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/Styles/Theme.xaml b/RGBSync+/Styles/Theme.xaml
new file mode 100644
index 0000000..520c7f9
--- /dev/null
+++ b/RGBSync+/Styles/Theme.xaml
@@ -0,0 +1,75 @@
+
+
+ #FFDCDCDC
+ #FF2A2A2A
+ #B82A2A2A
+ #111111
+ #B8111111
+ #60111111
+ #50000000
+ #FFE135
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #60111111
+ #B8111111
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 40
+
+
+
+ 14
+ 14
+ 22
+
diff --git a/RGBSync+/Styles/ToolTip.xaml b/RGBSync+/Styles/ToolTip.xaml
new file mode 100644
index 0000000..2b823da
--- /dev/null
+++ b/RGBSync+/Styles/ToolTip.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RGBSync+/UI/ConfigurationViewModel.cs b/RGBSync+/UI/ConfigurationViewModel.cs
new file mode 100644
index 0000000..3109938
--- /dev/null
+++ b/RGBSync+/UI/ConfigurationViewModel.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Windows;
+using System.Windows.Data;
+using GongSolutions.Wpf.DragDrop;
+using RGB.NET.Core;
+using RGBSyncPlus.Helper;
+using RGBSyncPlus.Model;
+
+namespace RGBSyncPlus.UI
+{
+ public sealed class ConfigurationViewModel : AbstractBindable, IDropTarget
+ {
+ #region Properties & Fields
+
+ public Version Version => Assembly.GetEntryAssembly().GetName().Version;
+
+ public double UpdateRate
+ {
+ get => 1.0 / ApplicationManager.Instance.UpdateTrigger.UpdateFrequency;
+ set
+ {
+ double val = MathHelper.Clamp(value, 1, 100);
+ ApplicationManager.Instance.Settings.UpdateRate = val;
+ ApplicationManager.Instance.UpdateTrigger.UpdateFrequency = 1.0 / val;
+ OnPropertyChanged();
+ }
+ }
+
+ private ObservableCollection _syncGroups;
+ public ObservableCollection SyncGroups
+ {
+ get => _syncGroups;
+ set => SetProperty(ref _syncGroups, value);
+ }
+
+ private SyncGroup _selectedSyncGroup;
+ public SyncGroup SelectedSyncGroup
+ {
+ get => _selectedSyncGroup;
+ set
+ {
+ if (SetProperty(ref _selectedSyncGroup, value))
+ UpdateLedLists();
+ }
+ }
+
+ private ListCollectionView _availableSyncLeds;
+ public ListCollectionView AvailableSyncLeds
+ {
+ get => _availableSyncLeds;
+ set => SetProperty(ref _availableSyncLeds, value);
+ }
+
+ private ListCollectionView _availableLeds;
+ public ListCollectionView AvailableLeds
+ {
+ get => _availableLeds;
+ set => SetProperty(ref _availableLeds, value);
+ }
+
+ private ListCollectionView _synchronizedLeds;
+ public ListCollectionView SynchronizedLeds
+ {
+ get => _synchronizedLeds;
+ set => SetProperty(ref _synchronizedLeds, value);
+ }
+
+ #endregion
+
+ #region Commands
+
+ private ActionCommand _openHomepageCommand;
+ public ActionCommand OpenHomepageCommand => _openHomepageCommand ?? (_openHomepageCommand = new ActionCommand(OpenHomepage));
+
+ private ActionCommand _addSyncGroupCommand;
+ public ActionCommand AddSyncGroupCommand => _addSyncGroupCommand ?? (_addSyncGroupCommand = new ActionCommand(AddSyncGroup));
+
+ private ActionCommand _removeSyncGroupCommand;
+ public ActionCommand RemoveSyncGroupCommand => _removeSyncGroupCommand ?? (_removeSyncGroupCommand = new ActionCommand(RemoveSyncGroup));
+
+ #endregion
+
+ #region Constructors
+
+ public ConfigurationViewModel()
+ {
+ SyncGroups = new ObservableCollection(ApplicationManager.Instance.Settings.SyncGroups);
+
+ AvailableSyncLeds = GetGroupedLedList(RGBSurface.Instance.Leds.Where(x => x.Device.DeviceInfo.SupportsSyncBack));
+ OnPropertyChanged(nameof(AvailableSyncLeds));
+ }
+
+ #endregion
+
+ #region Methods
+
+ private ListCollectionView GetGroupedLedList(IEnumerable leds) => GetGroupedLedList(leds.Select(led => new SyncLed(led)).ToList());
+
+ private ListCollectionView GetGroupedLedList(IList syncLeds)
+ {
+ ListCollectionView collectionView = new ListCollectionView(syncLeds);
+ collectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(SyncLed.Device)));
+ collectionView.SortDescriptions.Add(new SortDescription(nameof(SyncLed.Device), ListSortDirection.Ascending));
+ collectionView.SortDescriptions.Add(new SortDescription(nameof(SyncLed.LedId), ListSortDirection.Ascending));
+ collectionView.Refresh();
+ return collectionView;
+ }
+
+ private void UpdateLedLists()
+ {
+ SynchronizedLeds = GetGroupedLedList(SelectedSyncGroup.Leds);
+ OnPropertyChanged(nameof(SynchronizedLeds));
+
+ AvailableLeds = GetGroupedLedList(RGBSurface.Instance.Leds.Where(led => !SelectedSyncGroup.Leds.Any(sc => (sc.LedId == led.Id) && (sc.Device == led.Device.GetDeviceName()))));
+ OnPropertyChanged(nameof(AvailableLeds));
+ }
+
+ private void OpenHomepage() => Process.Start("https://github.com/DarthAffe/RGBSyncPlus");
+
+ private void AddSyncGroup()
+ {
+ SyncGroup syncGroup = new SyncGroup();
+ SyncGroups.Add(syncGroup);
+ ApplicationManager.Instance.AddSyncGroup(syncGroup);
+ }
+
+ private void RemoveSyncGroup(SyncGroup syncGroup)
+ {
+ if (syncGroup == null) return;
+
+ if (MessageBox.Show($"Are you sure that you want to delete the group '{syncGroup.DisplayName}'", "Remove Sync-Group", MessageBoxButton.YesNo) == MessageBoxResult.No)
+ return;
+
+ SyncGroups.Remove(syncGroup);
+ ApplicationManager.Instance.RemoveSyncGroup(syncGroup);
+ }
+
+ void IDropTarget.DragOver(IDropInfo dropInfo)
+ {
+ if ((dropInfo.Data is SyncLed || dropInfo.Data is IEnumerable) && (dropInfo.TargetCollection is ListCollectionView))
+ {
+ dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
+ dropInfo.Effects = DragDropEffects.Copy;
+ }
+ }
+
+ void IDropTarget.Drop(IDropInfo dropInfo)
+ {
+ if (!(dropInfo.TargetCollection is ListCollectionView targetList)) return;
+
+ //HACK DarthAffe 04.06.2018: Super ugly hack - I've no idea how to do this correctly ...
+ ListCollectionView sourceList = targetList == AvailableLeds ? SynchronizedLeds : AvailableLeds;
+
+ if (dropInfo.Data is SyncLed syncLed)
+ {
+ targetList.AddNewItem(syncLed);
+ sourceList.Remove(syncLed);
+
+ targetList.CommitNew();
+ sourceList.CommitEdit();
+ }
+ else if (dropInfo.Data is IEnumerable syncLeds)
+ {
+ foreach (SyncLed led in syncLeds)
+ {
+ targetList.AddNewItem(led);
+ sourceList.Remove(led);
+ }
+ targetList.CommitNew();
+ sourceList.CommitEdit();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/RGBSync+/UI/ConfigurationWindow.xaml b/RGBSync+/UI/ConfigurationWindow.xaml
new file mode 100644
index 0000000..819e8bd
--- /dev/null
+++ b/RGBSync+/UI/ConfigurationWindow.xaml
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RGBSync+/UI/ConfigurationWindow.xaml.cs b/RGBSync+/UI/ConfigurationWindow.xaml.cs
new file mode 100644
index 0000000..5b983bb
--- /dev/null
+++ b/RGBSync+/UI/ConfigurationWindow.xaml.cs
@@ -0,0 +1,16 @@
+using System;
+using RGBSyncPlus.Controls;
+
+namespace RGBSyncPlus.UI
+{
+ public partial class ConfigurationWindow : BlurredDecorationWindow
+ {
+ public ConfigurationWindow() => InitializeComponent();
+
+ //DarthAffe 07.02.2018: This prevents the applicaiton from not shutting down and crashing afterwards if 'close' is selected in the taskbar-context-menu
+ private void ConfigurationWindow_OnClosed(object sender, EventArgs e)
+ {
+ ApplicationManager.Instance.ExitCommand.Execute(null);
+ }
+ }
+}