WIP - microphone in use detection

This commit is contained in:
2021-04-16 21:38:38 -04:00
parent 6fec068715
commit e1ca9f67ac
13 changed files with 116 additions and 538 deletions

92
AudioWatcher.cs Normal file
View File

@@ -0,0 +1,92 @@
using CSCore.CoreAudioAPI;
using System;
using System.Diagnostics;
using System.Threading;
namespace WorkIndicator
{
public static class AudioWatcher
{
public delegate void MicrophoneInUseChangedDelegate(bool microphoneInUse);
private static ManualResetEvent _manualResetEvent;
private static int _activeSessionCount;
public static event MicrophoneInUseChangedDelegate MicrophoneInUseChanged;
public static void Start()
{
_manualResetEvent = new ManualResetEvent(false);
var thread = new Thread(delegate ()
{
var deviceEnumerator = new MMDeviceEnumerator();
foreach (var device in deviceEnumerator.EnumAudioEndpoints(DataFlow.Capture, DeviceState.Active))
{
var sessionManager = AudioSessionManager2.FromMMDevice(device);
var sessionEnumerator = sessionManager.GetSessionEnumerator();
sessionManager.SessionCreated += HandleAudioSessionCreated;
foreach (var audioSessionControl in sessionEnumerator)
{
HandleAudioStateChanged(audioSessionControl, new AudioSessionStateChangedEventArgs(audioSessionControl.SessionState));
audioSessionControl.StateChanged += HandleAudioStateChanged;
}
}
_manualResetEvent.WaitOne();
});
thread.SetApartmentState(ApartmentState.MTA);
thread.Start();
}
public static void Stop()
{
_manualResetEvent?.Set();
}
private static void HandleAudioSessionCreated(object sender, SessionCreatedEventArgs e)
{
HandleAudioStateChanged(null, new AudioSessionStateChangedEventArgs(e.NewSession.SessionState));
e.NewSession.StateChanged += HandleAudioStateChanged;
}
private static void HandleAudioStateChanged(object sender, AudioSessionStateChangedEventArgs e)
{
Debug.WriteLine($"{e.NewState}");
switch (e.NewState)
{
case AudioSessionState.AudioSessionStateActive:
_activeSessionCount++;
break;
case AudioSessionState.AudioSessionStateInactive:
_activeSessionCount--;
break;
case AudioSessionState.AudioSessionStateExpired:
break;
default:
throw new ArgumentOutOfRangeException();
}
if (_activeSessionCount < 0)
_activeSessionCount = 0;
Debug.WriteLine($"{_activeSessionCount}");
MicrophoneInUseChanged?.Invoke(MicrophoneInUse());
}
public static bool MicrophoneInUse()
{
return _activeSessionCount > 0;
}
}
}

View File

@@ -154,7 +154,7 @@ namespace WorkIndicator.Delcom
{
try
{
if (!_deviceHandle.IsClosed)
if (_deviceHandle != null && !_deviceHandle.IsClosed)
_deviceHandle.Close();
return 0;

View File

@@ -1,6 +1,6 @@
using System;
using Common.Extensions;
using Common.Extensions;
using System;
using System.Diagnostics;
namespace WorkIndicator.Delcom
{
@@ -80,6 +80,8 @@ namespace WorkIndicator.Delcom
_yellow = yellow;
_green = green;
Debug.WriteLine($"Red: {_red}, Yellow: {_yellow}, Green: {_green}");
port1 = port1.SetBitValue((int) Light.Green, green == LightState.Off);
port1 = port1.SetBitValue((int) Light.Yellow, yellow == LightState.Off);
port1 = port1.SetBitValue((int) Light.Red, red == LightState.Off);

View File

@@ -1,8 +1,4 @@
using Common.Native;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System;
using WorkIndicator.Delcom;
namespace WorkIndicator
@@ -18,46 +14,32 @@ namespace WorkIndicator
public static class LightController
{
private static WindowPatterns _windowPatterns;
private static StoplightIndicator _stoplightIndicator;
private static bool _initialized;
private static Status _status = Status.Auto;
public static void Initialize()
{
WindowPatterns.Changed += HandleWindowPatternsChanged;
_stoplightIndicator = new StoplightIndicator();
_stoplightIndicator.SetLight(StoplightIndicator.Light.Green, StoplightIndicator.LightState.On);
_stoplightIndicator.SetLight(StoplightIndicator.Light.Yellow, StoplightIndicator.LightState.On);
LoadPatterns();
AudioWatcher.MicrophoneInUseChanged += AudioWatcher_MicrophoneInUseChanged;
AudioWatcher.Start();
_initialized = true;
}
private static void LoadPatterns()
private static void AudioWatcher_MicrophoneInUseChanged(bool microphoneInUse)
{
_windowPatterns = WindowPatterns.Load();
if (_initialized)
TerminateWindowDetection();
InitializeWindowDetection();
UpdateLights();
}
private static void HandleWindowPatternsChanged(object sender, EventArgs e)
{
LoadPatterns();
}
public static void Dispose()
{
if (!_initialized)
return;
TerminateWindowDetection();
AudioWatcher.Stop();
_stoplightIndicator.SetLights(StoplightIndicator.LightState.Off, StoplightIndicator.LightState.Off, StoplightIndicator.LightState.Off);
_stoplightIndicator.Dispose();
@@ -83,13 +65,13 @@ namespace WorkIndicator
if (_status == Status.Auto)
{
if (WindowHandles.Count > 0)
if (AudioWatcher.MicrophoneInUse())
{
yellow = StoplightIndicator.LightState.On;
red = StoplightIndicator.LightState.On;
}
else
{
green = StoplightIndicator.LightState.On;
yellow = StoplightIndicator.LightState.On;
}
}
else
@@ -115,87 +97,5 @@ namespace WorkIndicator
_stoplightIndicator.SetLights(red, yellow, green);
}
#region Window detection
private static readonly WinEvent.WinEventDelegate ProcDelegate = WinEventProc;
private static readonly List<IntPtr> WindowEventHooks = new List<IntPtr>();
private static readonly List<IntPtr> WindowHandles = new List<IntPtr>();
private static readonly List<Regex> WindowMatchRegexList = new List<Regex>();
private static string WildcardToRegexPattern(string value)
{
return "^" + Regex.Escape(value).Replace("\\?", ".").Replace("\\*", ".*") + "$";
}
private static void InitializeWindowDetection()
{
foreach (var windowPattern in _windowPatterns.Where(w => w.Enabled))
WindowMatchRegexList.Add(new Regex(WildcardToRegexPattern(windowPattern.Pattern)));
Functions.User32.EnumWindows(EnumWindowProc, IntPtr.Zero);
IntPtr hook = WinEvent.SetWinEventHook(WinEvent.Event.ObjectCreate, WinEvent.Event.ObjectDestroy, IntPtr.Zero, ProcDelegate, 0, 0, WinEvent.SetWinEventHookFlags.OutOfContext | WinEvent.SetWinEventHookFlags.SkipOwnProcess);
if (hook != IntPtr.Zero)
WindowEventHooks.Add(hook);
hook = WinEvent.SetWinEventHook(WinEvent.Event.ObjectNameChange, WinEvent.Event.ObjectNameChange, IntPtr.Zero, ProcDelegate, 0, 0, WinEvent.SetWinEventHookFlags.OutOfContext | WinEvent.SetWinEventHookFlags.SkipOwnProcess);
if (hook != IntPtr.Zero)
WindowEventHooks.Add(hook);
}
private static void TerminateWindowDetection()
{
WindowEventHooks.ForEach(h => WinEvent.UnhookWinEvent(h));
WindowMatchRegexList.Clear();
WindowHandles.Clear();
}
private static bool EnumWindowProc(IntPtr hWnd, IntPtr lParam)
{
WinEventProc(IntPtr.Zero, WinEvent.Event.ObjectCreate, hWnd, WinEvent.ObjectIdentifier.Window, 0, 0, 0);
return true;
}
private static void WinEventProc(IntPtr hWinEventHook, WinEvent.Event eventType, IntPtr hwnd, WinEvent.ObjectIdentifier idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
if (idObject == WinEvent.ObjectIdentifier.Window && idChild == (int) WinEvent.ChildIdentifier.Self)
{
switch (eventType)
{
case WinEvent.Event.ObjectCreate:
case WinEvent.Event.ObjectNameChange:
if (WindowHandles.Contains(hwnd))
WindowHandles.Remove(hwnd);
foreach (var regex in WindowMatchRegexList)
{
if (regex.IsMatch(Functions.Window.GetText(hwnd)))
{
WindowHandles.Add(hwnd);
UpdateLights();
}
}
break;
case WinEvent.Event.ObjectDestroy:
if (WindowHandles.Contains(hwnd))
{
WindowHandles.Remove(hwnd);
UpdateLights();
}
break;
}
}
}
#endregion
}
}

View File

@@ -1,87 +0,0 @@
<Window x:Class="WorkIndicator.Options.WindowPatternWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:properties="clr-namespace:WorkIndicator.Properties"
mc:Ignorable="d"
WindowStartupLocation="CenterOwner"
Title="WindowPatternWindow"
Height="160"
Width="350"
FocusManager.FocusedElement="{Binding ElementName=NameTextBox}">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{x:Static properties:Resources.WindowPatternNameLabel}"
VerticalContentAlignment="Center"
Target="{Binding ElementName=NameTextBox}"
Grid.Row="0"
Grid.Column="0"
Margin="6"
Padding="0" />
<TextBox Name="NameTextBox"
Grid.Column="1"
Text="{Binding Path=Name, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}"
Grid.Row="0"
VerticalAlignment="Center"
Margin="6,0,6,0" />
<Label Content="{x:Static properties:Resources.WindowPatternPatternLabel}"
VerticalContentAlignment="Center"
Target="{Binding ElementName=PatternTextBox}"
Grid.Row="1"
Grid.Column="0"
Margin="6"
Padding="0" />
<TextBox Name="PatternTextBox"
Grid.Column="1"
Text="{Binding Path=Pattern, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}"
Grid.Row="1"
VerticalAlignment="Center"
Margin="6,0,6,0" />
<Label Content="{x:Static properties:Resources.WindowPatternEnabledLabel}"
VerticalContentAlignment="Center"
Target="{Binding ElementName=NameTextBox}"
Grid.Row="2"
Grid.Column="0"
Margin="6"
Padding="0" />
<CheckBox Grid.Column="1"
IsChecked="{Binding Path=Enabled, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=true}"
Grid.Row="2"
Margin="5,0,5,0"
VerticalAlignment="Center" />
<StackPanel Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="3"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="6">
<Button Content="{x:Static properties:Resources.OkayButton}"
Height="23"
VerticalAlignment="Bottom"
Width="75"
IsDefault="True"
Click="HandleOkayButtonClick"
Margin="0,0,8,0" />
<Button Content="{x:Static properties:Resources.CancelButton}"
Height="23"
VerticalAlignment="Bottom"
Width="75"
IsCancel="True" />
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@@ -1,66 +0,0 @@
using Common.Wpf.Extensions;
using System.Drawing;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace WorkIndicator.Options
{
public partial class WindowPatternWindow
{
public WindowPatternWindow()
{
InitializeComponent();
Icon = ((Icon)Properties.Resources.ResourceManager.GetObject("ApplicationIcon")).ToImageSource();
}
public bool? Display(WindowPattern windowPattern, Window owner)
{
// Set the data context
DataContext = windowPattern;
// Set the title based on the state of the item
Title = string.IsNullOrWhiteSpace(windowPattern.Name) ? Properties.Resources.WindowPatternWindowAdd : Properties.Resources.WindowPatternWindowEdit;
// Set the window owner
Owner = owner;
// Show the dialog and result the result
return ShowDialog();
}
private void HandleOkayButtonClick(object sender, RoutedEventArgs e)
{
// Get a list of all framework elements and explicit binding expressions
var bindingExpressions = this.GetBindingExpressions(new[] { UpdateSourceTrigger.Explicit });
// Loop over each binding expression and clear any existing error
this.ClearAllValidationErrors(bindingExpressions);
// Force all explicit bindings to update the source
this.UpdateAllSources(bindingExpressions);
// See if there are any errors
var hasError = bindingExpressions.Any(b => b.BindingExpression.HasError);
// If there was an error then set focus to the bad controls
if (hasError)
{
// Get the first framework element with an error
var firstErrorElement = bindingExpressions.First(b => b.BindingExpression.HasError).FrameworkElement;
// Set focus
firstErrorElement.Focus();
return;
}
// Dialog is good
DialogResult = true;
// Close the dialog
Close();
}
}
}

View File

@@ -1,79 +0,0 @@
<windows:CategoryPanel x:Class="WorkIndicator.Options.WindowPatternsOptionsPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:windows="clr-namespace:Common.Wpf.Windows;assembly=Common.Wpf"
xmlns:linkControl="clr-namespace:Common.Wpf.LinkControl;assembly=Common.Wpf"
xmlns:properties="clr-namespace:WorkIndicator.Properties"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<DataGrid x:Name="WindowPatternsGrid"
SelectionMode="Extended"
Grid.Column="0"
Grid.Row="0"
AutoGenerateColumns="False"
GridLinesVisibility="None"
CanUserResizeRows="False"
IsReadOnly="True"
HeadersVisibility="Column"
SelectionUnit="FullRow"
SelectionChanged="HandleWindowPatternsSelectionChanged"
Background="{x:Null}"
MouseDoubleClick="HandleWindowPatternsDoubleClick">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding Enabled}"
Header="{x:Static properties:Resources.ColumnHeader_Enabled}" />
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static properties:Resources.ColumnHeader_Name}"
SortDirection="Ascending"
Width="*" />
<DataGridTextColumn Binding="{Binding Pattern}"
Header="{x:Static properties:Resources.ColumnHeader_Pattern}"
Width="2*" />
</DataGrid.Columns>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness"
Value="0"></Setter>
<Setter Property="FocusVisualStyle"
Value="{x:Null}" />
</Style>
</DataGrid.CellStyle>
</DataGrid>
<Border Grid.Column="0"
Grid.Row="1"
BorderThickness="1,0,1,1"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<StackPanel Orientation="Horizontal"
Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<linkControl:LinkControl Margin="2"
Click="HandleAddWindowPatternButtonClick"
Text="{x:Static properties:Resources.AddWindowPattern}"
ToolTip="{x:Static properties:Resources.AddWindowPattern_ToolTip}">
</linkControl:LinkControl>
<linkControl:LinkControl Name="EditWindowPatternButton"
Margin="2"
Click="HandleEditWindowPatternButtonClick"
Text="{x:Static properties:Resources.EditWindowPattern}"
ToolTip="{x:Static properties:Resources.EditWindowPattern_ToolTip}">
</linkControl:LinkControl>
<linkControl:LinkControl Name="DeleteWindowPatternButton"
Margin="2"
Click="HandleDeleteWindowPatternButtonClick"
Text="{x:Static properties:Resources.DeleteWindowPattern}"
ToolTip="{x:Static properties:Resources.DeleteWindowPattern_ToolTip}">
</linkControl:LinkControl>
</StackPanel>
</Border>
</Grid>
</windows:CategoryPanel>

View File

@@ -1,114 +0,0 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WorkIndicator.Options
{
public partial class WindowPatternsOptionsPanel
{
public WindowPatternsOptionsPanel()
{
InitializeComponent();
}
private WindowPatterns WindowPatterns => Data as WindowPatterns;
public override void LoadPanel(object data)
{
base.LoadPanel(data);
WindowPatternsGrid.ItemsSource = WindowPatterns;
WindowPatternsGrid.SelectedItem = WindowPatterns.FirstOrDefault();
SetButtonStates();
}
public override bool ValidatePanel()
{
return true;
}
public override void SavePanel()
{
}
public override string CategoryName => Properties.Resources.OptionCategory_WindowPatterns;
private void SetButtonStates()
{
EditWindowPatternButton.IsEnabled = (WindowPatternsGrid.SelectedItems.Count == 1);
DeleteWindowPatternButton.IsEnabled = (WindowPatternsGrid.SelectedItems.Count > 0);
}
private void AddWindowPattern()
{
var windowPattern = new WindowPattern();
var windowPatternWindow = new WindowPatternWindow();
var result = windowPatternWindow.Display(windowPattern, Window.GetWindow(this));
if (result.HasValue && result.Value)
{
WindowPatterns.Add(windowPattern);
WindowPatternsGrid.SelectedItem = windowPattern;
SetButtonStates();
}
}
private void EditSelectedWindowPattern()
{
if (WindowPatternsGrid.SelectedItem == null)
return;
var windowPattern = WindowPatternsGrid.SelectedItem as WindowPattern;
var windowPatternWindow = new WindowPatternWindow();
windowPatternWindow.Display(windowPattern, Window.GetWindow(this));
}
private void DeleteSelectedWindowPattern()
{
var windowPattern = WindowPatternsGrid.SelectedItem as WindowPattern;
var index = WindowPatternsGrid.SelectedIndex;
WindowPatterns.Remove(windowPattern);
if (WindowPatternsGrid.Items.Count == index)
WindowPatternsGrid.SelectedIndex = WindowPatternsGrid.Items.Count - 1;
else if (WindowPatternsGrid.Items.Count >= index)
WindowPatternsGrid.SelectedIndex = index;
SetButtonStates();
}
private void HandleAddWindowPatternButtonClick(object sender, RoutedEventArgs e)
{
AddWindowPattern();
}
private void HandleEditWindowPatternButtonClick(object sender, RoutedEventArgs e)
{
EditSelectedWindowPattern();
}
private void HandleDeleteWindowPatternButtonClick(object sender, RoutedEventArgs e)
{
DeleteSelectedWindowPattern();
}
private void HandleWindowPatternsSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetButtonStates();
}
private void HandleWindowPatternsDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
EditSelectedWindowPattern();
}
}
}

View File

@@ -113,20 +113,17 @@ namespace WorkIndicator
_initialized = false;
}
public static void ShowSettings()
private static void ShowSettings()
{
var panels = new List<CategoryPanel>
{
new GeneralOptionsPanel(),
new WindowPatternsOptionsPanel(),
new AboutOptionsPanel()
};
var windowPatterns = WindowPatterns.Load();
if (_optionsWindow == null)
{
_optionsWindow = new CategoryWindow(windowPatterns, panels, Resources.ResourceManager, "OptionsWindow");
_optionsWindow = new CategoryWindow(null, panels, Resources.ResourceManager, "OptionsWindow");
_optionsWindow.Closed += (o, args) => { _optionsWindow = null; };
}
@@ -134,7 +131,6 @@ namespace WorkIndicator
if (dialogResult.HasValue && dialogResult.Value)
{
windowPatterns.Save();
}
}
}

View File

@@ -1,9 +0,0 @@
namespace WorkIndicator
{
public class WindowPattern
{
public string Name { get; set; } = string.Empty;
public string Pattern { get; set; } = string.Empty;
public bool Enabled { get; set; }
}
}

View File

@@ -1,47 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
using WorkIndicator.Properties;
namespace WorkIndicator
{
public class WindowPatterns : ObservableCollection<WindowPattern>
{
public static event EventHandler Changed;
public static WindowPatterns Load()
{
var windowPatterns = Load(Settings.Default.WindowPatterns);
return windowPatterns;
}
private static WindowPatterns Load(string serializedData)
{
var windowPatterns = JsonConvert.DeserializeObject<WindowPatterns>(serializedData) ?? new WindowPatterns();
return windowPatterns;
}
public void Save()
{
Settings.Default.WindowPatterns = Serialize();
Settings.Default.Save();
Changed?.Invoke(this, null);
}
private string Serialize()
{
return JsonConvert.SerializeObject(this);
}
public WindowPatterns Clone()
{
var data = Serialize();
return Load(data);
}
}
}

View File

@@ -147,6 +147,7 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="AudioWatcher.cs" />
<Compile Include="LightController.cs" />
<Compile Include="Delcom\StoplightIndicator.cs" />
<Compile Include="App.xaml.cs">
@@ -165,15 +166,7 @@
<Compile Include="Options\GeneralOptionsPanel.xaml.cs">
<DependentUpon>GeneralOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\WindowPatternsOptionsPanel.xaml.cs">
<DependentUpon>WindowPatternsOptionsPanel.xaml</DependentUpon>
</Compile>
<Compile Include="Options\WindowPatternWindow.xaml.cs">
<DependentUpon>WindowPatternWindow.xaml</DependentUpon>
</Compile>
<Compile Include="TrayIcon.cs" />
<Compile Include="WindowPattern.cs" />
<Compile Include="WindowPatterns.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
@@ -232,18 +225,13 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Options\WindowPatternsOptionsPanel.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Options\WindowPatternWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSCore">
<Version>1.2.1.2</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>10.0.3</Version>
<Version>13.0.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Delcom/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>