1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-31 09:43:46 +00:00

Added Timer-Based ActiveWindow-detection

This commit is contained in:
Darth Affe 2017-04-23 13:00:23 +02:00
parent 0cf9b3547e
commit 7d7d4298d3
13 changed files with 238 additions and 41 deletions

View File

@ -670,7 +670,11 @@
<Compile Include="Settings\OffsetSettings.cs" /> <Compile Include="Settings\OffsetSettings.cs" />
<Compile Include="Styles\DropTargetAdorners\DropTargetMetroHighlightAdorner.cs" /> <Compile Include="Styles\DropTargetAdorners\DropTargetMetroHighlightAdorner.cs" />
<Compile Include="Styles\DropTargetAdorners\DropTargetMetroInsertionAdorner.cs" /> <Compile Include="Styles\DropTargetAdorners\DropTargetMetroInsertionAdorner.cs" />
<Compile Include="Utilities\ActiveWindowHelper.cs" /> <Compile Include="Utilities\ActiveWindowDetection\ActiveWindowDetectionType.cs" />
<Compile Include="Utilities\ActiveWindowDetection\ActiveWindowHelper.cs" />
<Compile Include="Utilities\ActiveWindowDetection\EventActiveWindowDetector.cs" />
<Compile Include="Utilities\ActiveWindowDetection\IActiveWindowDetector.cs" />
<Compile Include="Utilities\ActiveWindowDetection\TimerActiveWindowDetector.cs" />
<Compile Include="Utilities\ColorHelpers.cs" /> <Compile Include="Utilities\ColorHelpers.cs" />
<Compile Include="Utilities\Converters\JsonConverters.cs" /> <Compile Include="Utilities\Converters\JsonConverters.cs" />
<Compile Include="Utilities\Converters\NinjectCustomConverter.cs" /> <Compile Include="Utilities\Converters\NinjectCustomConverter.cs" />

View File

@ -10,6 +10,7 @@ using Artemis.DAL;
using Artemis.InjectionModules; using Artemis.InjectionModules;
using Artemis.Settings; using Artemis.Settings;
using Artemis.Utilities; using Artemis.Utilities;
using Artemis.Utilities.ActiveWindowDetection;
using Artemis.Utilities.Converters; using Artemis.Utilities.Converters;
using Artemis.Utilities.DataReaders; using Artemis.Utilities.DataReaders;
using Artemis.ViewModels; using Artemis.ViewModels;
@ -113,7 +114,7 @@ namespace Artemis
//TODO DarthAffe 17.12.2016: Is this the right location for this? //TODO DarthAffe 17.12.2016: Is this the right location for this?
//TODO Move to Mainmanager and make disposable //TODO Move to Mainmanager and make disposable
ActiveWindowHelper.Initialize(); ActiveWindowHelper.SetActiveWindowDetectionType(SettingsProvider.Load<GeneralSettings>().ActiveWindowDetection);
} }
protected override void OnExit(object sender, EventArgs e) protected override void OnExit(object sender, EventArgs e)

View File

@ -5,6 +5,7 @@ using Artemis.DAL;
using Artemis.Modules.Abstract; using Artemis.Modules.Abstract;
using Artemis.Settings; using Artemis.Settings;
using Artemis.Utilities; using Artemis.Utilities;
using Artemis.Utilities.ActiveWindowDetection;
using Ninject.Extensions.Logging; using Ninject.Extensions.Logging;
namespace Artemis.Managers namespace Artemis.Managers

View File

@ -11,6 +11,7 @@ using Artemis.Events;
using Artemis.Managers; using Artemis.Managers;
using Artemis.Modules.Abstract; using Artemis.Modules.Abstract;
using Artemis.Utilities; using Artemis.Utilities;
using Artemis.Utilities.ActiveWindowDetection;
using CSCore.CoreAudioAPI; using CSCore.CoreAudioAPI;
using Newtonsoft.Json; using Newtonsoft.Json;
using SpotifyAPI.Local; using SpotifyAPI.Local;

View File

@ -5,9 +5,11 @@ using System.Windows;
using Artemis.DAL; using Artemis.DAL;
using Artemis.Profiles.Layers.Types.AmbientLight.ScreenCapturing; using Artemis.Profiles.Layers.Types.AmbientLight.ScreenCapturing;
using Artemis.Utilities; using Artemis.Utilities;
using Artemis.Utilities.ActiveWindowDetection;
using Caliburn.Micro; using Caliburn.Micro;
using MahApps.Metro; using MahApps.Metro;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squirrel; using Squirrel;
namespace Artemis.Settings namespace Artemis.Settings
@ -68,6 +70,11 @@ namespace Artemis.Settings
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public string LogLevel { get; set; } public string LogLevel { get; set; }
[DefaultValue(ActiveWindowDetectionType.Events)]
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public ActiveWindowDetectionType ActiveWindowDetection { get; set; }
public Version LastRanVersion { get; set; } public Version LastRanVersion { get; set; }
public void Save() public void Save()
@ -75,6 +82,7 @@ namespace Artemis.Settings
SettingsProvider.Save(this); SettingsProvider.Save(this);
Logging.SetupLogging(LogLevel); Logging.SetupLogging(LogLevel);
ActiveWindowHelper.SetActiveWindowDetectionType(ActiveWindowDetection);
ApplyAutorun(); ApplyAutorun();
ApplyTheme(); ApplyTheme();
ApplyGamestatePort(); ApplyGamestatePort();

View File

@ -0,0 +1,9 @@
namespace Artemis.Utilities.ActiveWindowDetection
{
public enum ActiveWindowDetectionType
{
Disabled = -1,
Events = 0,
Timer = 1,
}
}

View File

@ -0,0 +1,48 @@
namespace Artemis.Utilities.ActiveWindowDetection
{
public static class ActiveWindowHelper
{
#region Properties & Fields
private static ActiveWindowDetectionType _currentDetectionType = ActiveWindowDetectionType.Disabled;
private static IActiveWindowDetector _activeWindowDetector;
public static string ActiveWindowProcessName => _activeWindowDetector?.ActiveWindowProcessName ?? string.Empty;
public static string ActiveWindowWindowTitle => _activeWindowDetector?.ActiveWindowWindowTitle ?? string.Empty;
public static bool MainWindowActive => ActiveWindowProcessName.Contains("Artemis");
#endregion
#region Methods
public static void Dispose()
{
_activeWindowDetector?.Dispose();
}
public static void SetActiveWindowDetectionType(ActiveWindowDetectionType detectionType)
{
if (detectionType == _currentDetectionType) return;
_activeWindowDetector?.Dispose();
switch (detectionType)
{
case ActiveWindowDetectionType.Events:
_activeWindowDetector = new EventActiveWindowDetector();
break;
case ActiveWindowDetectionType.Timer:
_activeWindowDetector = new TimerActiveWindowDetector();
break;
case ActiveWindowDetectionType.Disabled:
_activeWindowDetector = null;
break;
}
_activeWindowDetector?.Initialize();
_currentDetectionType = detectionType;
}
#endregion
}
}

View File

@ -2,9 +2,9 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Artemis.Utilities namespace Artemis.Utilities.ActiveWindowDetection
{ {
public static class ActiveWindowHelper public class EventActiveWindowDetector : IActiveWindowDetector
{ {
#region DLL-Imports #region DLL-Imports
@ -14,7 +14,7 @@ namespace Artemis.Utilities
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook); private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
@ -41,47 +41,46 @@ namespace Artemis.Utilities
#region Properties & Fields #region Properties & Fields
// DarthAffe 17.12.2016: We need to keep a reference to this or it might get collected by the garbage collector and cause some random crashes afterwards. // DarthAffe 17.12.2016: We need to keep a reference to this or it might get collected by the garbage collector and cause some random crashes afterwards.
private static WinEventDelegate _activeWindowChangedDelegate; private WinEventDelegate _activeWindowChangedDelegate;
private static IntPtr _activeWindowEventHook; private IntPtr _activeWindowEventHook;
private static WinEventDelegate _windowTitleChangedDelegate; private WinEventDelegate _windowTitleChangedDelegate;
private static IntPtr _windowTitleEventHook; private IntPtr _windowTitleEventHook;
private static WinEventDelegate _windowMinimizedChangedDelegate; private WinEventDelegate _windowMinimizedChangedDelegate;
private static IntPtr _windowMinimizedEventHook; private IntPtr _windowMinimizedEventHook;
private static IntPtr _activeWindow; private IntPtr _activeWindow;
public static string ActiveWindowProcessName { get; private set; } = string.Empty; public string ActiveWindowProcessName { get; private set; } = string.Empty;
public static string ActiveWindowWindowTitle { get; private set; } = string.Empty; public string ActiveWindowWindowTitle { get; private set; } = string.Empty;
public static bool MainWindowActive => ActiveWindowProcessName.Contains("Artemis");
#endregion #endregion
#region Methods #region Methods
private static void ActiveWindowChanged(IntPtr hWinEventHook, uint eventType, private void ActiveWindowChanged(IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
UpdateForWindow(hwnd); UpdateForWindow(hwnd);
} }
private static void WindowTitleChanged(IntPtr hWinEventHook, uint eventType, private void WindowTitleChanged(IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
if (_activeWindow == hwnd) if (_activeWindow == hwnd)
UpdateForWindow(hwnd); UpdateForWindow(hwnd);
} }
private static void WindowMinimizedChanged(IntPtr hWinEventHook, uint eventType, private void WindowMinimizedChanged(IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
// DarthAffe 19.12.2016: We expect currently un-minimized windows to be active. // DarthAffe 19.12.2016: We expect currently un-minimized windows to be active.
// DarthAffe 19.12.2016: The result of the API-function GetActiveWindow at this moment is 'idle' so we can't use this to validate this estimation. // DarthAffe 19.12.2016: The result of the API-function GetActiveWindow at this moment is 'idle' so we can't use this to validate this estimation.
UpdateForWindow(hwnd); UpdateForWindow(hwnd);
} }
private static void UpdateForWindow(IntPtr hwnd) private void UpdateForWindow(IntPtr hwnd)
{ {
_activeWindow = hwnd; _activeWindow = hwnd;
@ -89,13 +88,13 @@ namespace Artemis.Utilities
ActiveWindowWindowTitle = GetActiveWindowTitle(hwnd) ?? string.Empty; ActiveWindowWindowTitle = GetActiveWindowTitle(hwnd) ?? string.Empty;
} }
private static string GetActiveWindowProcessName(IntPtr hwnd) private string GetActiveWindowProcessName(IntPtr hwnd)
{ {
try try
{ {
uint pid; uint pid;
GetWindowThreadProcessId(hwnd, out pid); GetWindowThreadProcessId(hwnd, out pid);
return System.Diagnostics.Process.GetProcessById((int) pid).ProcessName; return System.Diagnostics.Process.GetProcessById((int)pid).ProcessName;
} }
catch catch
{ {
@ -103,7 +102,7 @@ namespace Artemis.Utilities
} }
} }
private static string GetActiveWindowTitle(IntPtr hwnd) private string GetActiveWindowTitle(IntPtr hwnd)
{ {
try try
{ {
@ -116,7 +115,7 @@ namespace Artemis.Utilities
} }
} }
public static void Initialize() public void Initialize()
{ {
try try
{ {
@ -124,21 +123,21 @@ namespace Artemis.Utilities
{ {
_activeWindowChangedDelegate = ActiveWindowChanged; _activeWindowChangedDelegate = ActiveWindowChanged;
_activeWindowEventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, _activeWindowEventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
IntPtr.Zero, _activeWindowChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT); IntPtr.Zero, _activeWindowChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT);
} }
if (_windowTitleEventHook == IntPtr.Zero) if (_windowTitleEventHook == IntPtr.Zero)
{ {
_windowTitleChangedDelegate = WindowTitleChanged; _windowTitleChangedDelegate = WindowTitleChanged;
_windowTitleEventHook = SetWinEventHook(EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE, _windowTitleEventHook = SetWinEventHook(EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE,
IntPtr.Zero, _windowTitleChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT); IntPtr.Zero, _windowTitleChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT);
} }
if (_windowMinimizedEventHook == IntPtr.Zero) if (_windowMinimizedEventHook == IntPtr.Zero)
{ {
_windowMinimizedChangedDelegate = WindowMinimizedChanged; _windowMinimizedChangedDelegate = WindowMinimizedChanged;
_windowMinimizedEventHook = SetWinEventHook(EVENT_SYSTEM_MINIMIZEEND, EVENT_SYSTEM_MINIMIZEEND, _windowMinimizedEventHook = SetWinEventHook(EVENT_SYSTEM_MINIMIZEEND, EVENT_SYSTEM_MINIMIZEEND,
IntPtr.Zero, _windowMinimizedChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT); IntPtr.Zero, _windowMinimizedChangedDelegate, 0, 0, WINEVENT_OUTOFCONTEXT);
} }
} }
catch catch
@ -147,7 +146,7 @@ namespace Artemis.Utilities
} }
} }
public static void Dispose() public void Dispose()
{ {
try try
{ {

View File

@ -0,0 +1,12 @@
using System;
namespace Artemis.Utilities.ActiveWindowDetection
{
public interface IActiveWindowDetector : IDisposable
{
string ActiveWindowProcessName { get; }
string ActiveWindowWindowTitle { get; }
void Initialize();
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Timers;
namespace Artemis.Utilities.ActiveWindowDetection
{
public class TimerActiveWindowDetector : IActiveWindowDetector
{
#region DLL-Imports
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll")]
private static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
#endregion
#region Constants
private const int TIMER_INTERVAL = 1000;
private const int MAX_TITLE_LENGTH = 256;
#endregion
#region Properties & Fields
private Timer _timer;
public string ActiveWindowProcessName { get; private set; }
public string ActiveWindowWindowTitle { get; private set; }
#endregion
#region Methods
private void TimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
{
IntPtr activeWindow = GetForegroundWindow();
ActiveWindowProcessName = GetActiveWindowProcessName(activeWindow);
ActiveWindowWindowTitle = GetActiveWindowTitle(activeWindow);
}
private string GetActiveWindowProcessName(IntPtr hwnd)
{
try
{
uint pid;
GetWindowThreadProcessId(hwnd, out pid);
return System.Diagnostics.Process.GetProcessById((int)pid).ProcessName;
}
catch
{
return null;
}
}
private string GetActiveWindowTitle(IntPtr hwnd)
{
try
{
StringBuilder buffer = new StringBuilder(MAX_TITLE_LENGTH);
return GetWindowText(hwnd, buffer, MAX_TITLE_LENGTH) > 0 ? buffer.ToString() : null;
}
catch
{
return null;
}
}
public void Initialize()
{
_timer = new Timer(TIMER_INTERVAL) { AutoReset = true };
_timer.Elapsed += TimerOnElapsed;
_timer.Start();
}
public void Dispose()
{
_timer?.Stop();
_timer?.Dispose();
_timer = null;
}
#endregion
}
}

View File

@ -8,6 +8,7 @@ using Artemis.Events;
using Artemis.Managers; using Artemis.Managers;
using Artemis.Services; using Artemis.Services;
using Artemis.Settings; using Artemis.Settings;
using Artemis.Utilities.ActiveWindowDetection;
using Caliburn.Micro; using Caliburn.Micro;
using MahApps.Metro.Controls; using MahApps.Metro.Controls;
using NLog; using NLog;
@ -37,6 +38,8 @@ namespace Artemis.ViewModels.Flyouts
LogLevels = new BindableCollection<string>(); LogLevels = new BindableCollection<string>();
LogLevels.AddRange(LogLevel.AllLoggingLevels.Select(l => l.Name)); LogLevels.AddRange(LogLevel.AllLoggingLevels.Select(l => l.Name));
ActiveWindowDetections = new BindableCollection<ActiveWindowDetectionType>(Enum.GetValues(typeof(ActiveWindowDetectionType)).Cast<ActiveWindowDetectionType>());
PropertyChanged += KeyboardUpdater; PropertyChanged += KeyboardUpdater;
mainManager.EnabledChanged += MainManagerEnabledChanged; mainManager.EnabledChanged += MainManagerEnabledChanged;
mainManager.ModuleManager.EffectChanged += EffectManagerEffectChanged; mainManager.ModuleManager.EffectChanged += EffectManagerEffectChanged;
@ -96,6 +99,7 @@ namespace Artemis.ViewModels.Flyouts
public string VersionText => "Artemis " + Assembly.GetExecutingAssembly().GetName().Version; public string VersionText => "Artemis " + Assembly.GetExecutingAssembly().GetName().Version;
public BindableCollection<string> LogLevels { get; set; } public BindableCollection<string> LogLevels { get; set; }
public BindableCollection<ActiveWindowDetectionType> ActiveWindowDetections { get; set; }
public string SelectedTheme public string SelectedTheme
{ {
@ -130,6 +134,17 @@ namespace Artemis.ViewModels.Flyouts
} }
} }
public ActiveWindowDetectionType SelectedActiveWindowDetection
{
get { return GeneralSettings.ActiveWindowDetection; }
set
{
if (value == GeneralSettings.ActiveWindowDetection) return;
GeneralSettings.ActiveWindowDetection = value;
NotifyOfPropertyChange(() => SelectedActiveWindowDetection);
}
}
public string SelectedKeyboardProvider public string SelectedKeyboardProvider
{ {
get { return _selectedKeyboardProvider; } get { return _selectedKeyboardProvider; }

View File

@ -23,6 +23,7 @@ using Artemis.Properties;
using Artemis.Services; using Artemis.Services;
using Artemis.Styles.DropTargetAdorners; using Artemis.Styles.DropTargetAdorners;
using Artemis.Utilities; using Artemis.Utilities;
using Artemis.Utilities.ActiveWindowDetection;
using Caliburn.Micro; using Caliburn.Micro;
using Castle.Components.DictionaryAdapter; using Castle.Components.DictionaryAdapter;
using GongSolutions.Wpf.DragDrop; using GongSolutions.Wpf.DragDrop;

View File

@ -6,7 +6,7 @@
xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls" xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:cal="http://www.caliburnproject.org" xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="300"> d:DesignHeight="600" d:DesignWidth="310">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@ -25,6 +25,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@ -95,29 +96,35 @@
HorizontalAlignment="Right" Width="140" Minimum="1" Maximum="60" HorizontalAlignment="Right" Width="140" Minimum="1" Maximum="60"
Value="{Binding Path=GeneralSettings.ScreenCaptureFPS, Mode=TwoWay}" /> Value="{Binding Path=GeneralSettings.ScreenCaptureFPS, Mode=TwoWay}" />
<!-- Logging -->
<Label Grid.Row="9" Grid.Column="0" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Left" <Label Grid.Row="9" Grid.Column="0" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="Log level:" /> Content="Active window detection" />
<ComboBox Grid.Row="9" Grid.Column="1" x:Name="LogLevels" Margin="10" VerticalAlignment="Center" <ComboBox Grid.Row="9" Grid.Column="1" x:Name="ActiveWindowDetections" Margin="10" VerticalAlignment="Center"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Width="140" /> Width="140" />
<Button Grid.Row="10" Grid.Column="0" Margin="10" x:Name="ShowLogs" Content="Show logs"
<!-- Logging -->
<Label Grid.Row="10" Grid.Column="0" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Left"
Content="Log level:" />
<ComboBox Grid.Row="10" Grid.Column="1" x:Name="LogLevels" Margin="10" VerticalAlignment="Center"
HorizontalAlignment="Right"
Width="140" />
<Button Grid.Row="11" Grid.Column="0" Margin="10" x:Name="ShowLogs" Content="Show logs"
VerticalAlignment="Center" Width="100" HorizontalAlignment="Left" VerticalAlignment="Center" Width="100" HorizontalAlignment="Left"
Style="{DynamicResource SquareButtonStyle}" /> Style="{DynamicResource SquareButtonStyle}" />
<Button Grid.Row="10" Grid.Column="1" Margin="10" x:Name="ShowDebug" Content="Show debugger" <Button Grid.Row="11" Grid.Column="1" Margin="10" x:Name="ShowDebug" Content="Show debugger"
VerticalAlignment="Center" Width="100" HorizontalAlignment="Right" VerticalAlignment="Center" Width="100" HorizontalAlignment="Right"
Style="{DynamicResource SquareButtonStyle}" /> Style="{DynamicResource SquareButtonStyle}" />
<!-- Buttons --> <!-- Buttons -->
<Button Grid.Row="11" Grid.Column="0" Margin="10" x:Name="ResetSettings" Content="Reset settings" <Button Grid.Row="12" Grid.Column="0" Margin="10" x:Name="ResetSettings" Content="Reset settings"
VerticalAlignment="Center" HorizontalAlignment="Left" Width="100" VerticalAlignment="Center" HorizontalAlignment="Left" Width="100"
Style="{DynamicResource SquareButtonStyle}" /> Style="{DynamicResource SquareButtonStyle}" />
<Button Grid.Row="11" Grid.Column="1" Margin="11" x:Name="SaveSettings" Content="Save changes" <Button Grid.Row="12" Grid.Column="1" Margin="11" x:Name="SaveSettings" Content="Save changes"
VerticalAlignment="Center" HorizontalAlignment="Right" Width="100" VerticalAlignment="Center" HorizontalAlignment="Right" Width="100"
Style="{DynamicResource SquareButtonStyle}" /> Style="{DynamicResource SquareButtonStyle}" />
<!-- Version --> <!-- Version -->
<Grid Grid.Row="12" Grid.Column="0" Grid.ColumnSpan="2" Margin="10" VerticalAlignment="Bottom"> <Grid Grid.Row="13" Grid.Column="0" Grid.ColumnSpan="2" Margin="10" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />