Skip to content

Commit

Permalink
Add audio device icons to managed device editor interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sypwn committed Dec 1, 2023
1 parent c1a5e3b commit 8ada456
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 5 deletions.
2 changes: 1 addition & 1 deletion WinAudioAssistant/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace WinAudioAssistant
public partial class App : Application
{
public static CoreAudioController CoreAudioController { get; private set; } = new();
public static IconManager IconManager { get; private set; } = new();
public static AudioEndpointManager AudioEndpointManager { get; private set; } = new();
public static SystemEventsHandler SystemEventsHandler { get; private set; } = new();
#if DEBUG
Expand All @@ -23,7 +24,6 @@ public partial class App : Application
public static UserSettings UserSettings { get; private set; } = new();
#endif


protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Expand Down
6 changes: 6 additions & 0 deletions WinAudioAssistant/Models/AudioEndpointInfo.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using AudioSwitcher.AudioApi;
using AudioSwitcher.AudioApi.CoreAudio;
using Newtonsoft.Json;
Expand Down Expand Up @@ -43,6 +45,10 @@ public struct AudioEndpointInfo
public readonly Guid Guid => AudioEndpoint_GUID;
[JsonIgnore]
public readonly string RealId => (DataFlow == DeviceType.Playback ? "{0.0.0.00000000}.{" : "{0.0.1.00000000}.{") + AudioEndpoint_GUID.ToString() + "}";
[JsonIgnore]
public readonly Icon Icon => App.IconManager.GetIconFromIconPath(DeviceClass_IconPath);
[JsonIgnore]
public readonly BitmapSource IconBitmap => App.IconManager.GetBitmapFromIconPath(DeviceClass_IconPath);

/// <summary>
/// Deserialization constructor.
Expand Down
161 changes: 161 additions & 0 deletions WinAudioAssistant/Models/IconManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using System.Windows.Resources;
using WinAudioAssistant.ViewModels;

namespace WinAudioAssistant.Models
{
/// <summary>
/// Loads and maintains a cache of audio device icons and images.
/// </summary>
public partial class IconManager
{
private readonly Dictionary<string, Icon> _iconCache = new(); // This is probably not necessary to keep
private readonly Dictionary<string, BitmapSource> _bitmapCache = new();
private readonly Icon _notFoundIcon;
private readonly BitmapSource _notFoundBitmap;
[GeneratedRegex(@"^(.+),(-?\d+)$")] // Matches a string that has an icon index/resourceid at the end.
private static partial Regex IconPathPattern();

public IconManager()
{
_notFoundIcon = GetIconFromResourcePath("Resources/Icons/MissingFile.ico");
_notFoundBitmap = GetBitmapFromResourcePath("Resources/Icons/MissingFile.ico");
}

/// <summary>
/// Converts an Icon into a BitmapSource for use in WPF.
/// </summary>
private static BitmapSource ConvertIconToBitmapSource(Icon icon)
{
return Imaging.CreateBitmapSourceFromHIcon(icon.Handle,Int32Rect.Empty,BitmapSizeOptions.FromWidthAndHeight(EditDeviceViewModel.IconSize, EditDeviceViewModel.IconSize));
}

/// <summary>
/// Attempts to fetch an icon from the file system and cache it as both an Icon and a BitmapSource.
/// </summary>
/// <param name="iconPath">Path to the icon file. Can include an icon index or resource identifier
/// at the end of the string separated by a comma, or simply a path to an .ico file.</param>
/// <returns>True if successful</returns>
private bool CacheFromIconPath(string iconPath)
{
Icon? icon;
Match match = IconPathPattern().Match(iconPath);
try
{
if (match.Success)
icon = Icon.ExtractIcon(match.Groups[1].Value, int.Parse(match.Groups[2].Value));
else
icon = Icon.ExtractIcon(iconPath, 0);
}
catch (Exception)
{
return false;
}

if (icon is null) return false;
_iconCache[iconPath] = icon; // NOTE: If we decide not to cache the icons in the future, we must add a call
// to icon.Dispose() after, or else the BitmapSource will keep it active with a reference.
_bitmapCache[iconPath] = ConvertIconToBitmapSource(icon);
return true;
}

/// <summary>
/// Fetches an icon from the application resource and caches it as both an Icon and a BitmapSource.
/// Will throw error if icon is not found.
/// </summary>
/// <param name="resourcePath">Path to the icon file.</param>
private void CacheFromIconResource(string resourcePath)
{
Uri resourceUri = new Uri(resourcePath, UriKind.Relative);
StreamResourceInfo streamInfo = App.GetResourceStream(resourceUri);

if (streamInfo is null)
throw new FileNotFoundException("Resource not found", resourcePath);

using Stream stream = streamInfo.Stream;
Icon icon = new(stream);
_iconCache[resourcePath] = icon;
_bitmapCache[resourcePath] = ConvertIconToBitmapSource(icon);
}

/// <summary>
/// Fetches an icon from the cache or file system and returns it as an Icon.
/// </summary>
/// <param name="iconPath">Path to the icon file. Can include an icon index or resource identifier
/// at the end of the string separated by a comma, or simply a path to an .ico file.</param>
/// <returns>The Icon at the path if found, or a generic icon if not found.</returns>
public Icon GetIconFromIconPath(string? iconPath)
{
if (iconPath is null || iconPath == string.Empty)
return _notFoundIcon;

if (_iconCache.TryGetValue(iconPath, out Icon? icon))
return icon;

if (CacheFromIconPath(iconPath))
return _iconCache[iconPath];
else
return _notFoundIcon;
}

/// <summary>
/// Fetches an icon from the cache or file system and returns it as a BitmapSource.
/// </summary>
/// <param name="iconPath">Path to the icon file. Can include an icon index or resource identifier
/// at the end of the string separated by a comma, or simply a path to an .ico file.</param>
/// <returns>A BitmapSource from the icon at the path if found, or a generic bitmap if not found.</returns>
public BitmapSource GetBitmapFromIconPath(string? iconPath)
{
if (iconPath is null || iconPath == string.Empty)
return _notFoundBitmap;

if (_bitmapCache.TryGetValue(iconPath, out BitmapSource? image))
return image;

if (CacheFromIconPath(iconPath))
return _bitmapCache[iconPath];
else
return _notFoundBitmap;
}

/// <summary>
/// Fetches an icon from the cache or application resources and returns it as an Icon.
/// </summary>
/// <param name="resourcePath">Path to the resource, which must be an .ico file.</param>
/// <returns>The Icon at the path.</returns>
public Icon GetIconFromResourcePath(string resourcePath)
{
if (_iconCache.TryGetValue(resourcePath, out Icon? icon))
return icon;

CacheFromIconResource(resourcePath);
return _iconCache[resourcePath];
}

/// <summary>
/// Fetches an icon from the cache or application resources and returns it as a BitmapSource.
/// </summary>
/// <param name="resourcePath">Path to the resource, which must be an .ico file.</param>
/// <returns>A BitmapSource from the icon at the path.</returns>
public BitmapSource GetBitmapFromResourcePath(string resourcePath)
{
if (_bitmapCache.TryGetValue(resourcePath, out BitmapSource? image))
return image;

CacheFromIconResource(resourcePath);
return _bitmapCache[resourcePath];
}


}
}
Binary file not shown.
2 changes: 2 additions & 0 deletions WinAudioAssistant/ViewModels/EditDeviceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace WinAudioAssistant.ViewModels
{
public class EditDeviceViewModel : INotifyPropertyChanged
{
public const int IconSize = 32;

public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
Expand Down
2 changes: 1 addition & 1 deletion WinAudioAssistant/Views/DevicePriorityView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@

<!-- Buttons -->
<StackPanel Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Center" Margin="5,10,5,0">
<Button Content="Apply" Width="75" Margin="10,0,0,0" Click="ApplyButton_Click" ToolTip="Saves changes without closing the dialog."/>
<Button Content="Save" Width="75" Margin="10,0,0,0" Click="SaveButton_Click" ToolTip="Saves changes and closes the dialog."/>
<Button Content="Apply" Width="75" Margin="10,0,0,0" Click="ApplyButton_Click" ToolTip="Saves changes without closing the dialog."/>
<Button Content="Cancel" Width="75" Margin="10,0,0,0" Click="CancelButton_Click" ToolTip="Closes the dialog without saving changes."/>
</StackPanel>
</Grid>
Expand Down
7 changes: 4 additions & 3 deletions WinAudioAssistant/Views/EditDeviceView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- <Icon Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" ... -->
<Image Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Margin="0,0,5,0"
Source="{Binding IconBitmap}"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding Device_DeviceDesc}"/>
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding DeviceInterface_FriendlyName}"/>
</Grid>
Expand Down Expand Up @@ -128,9 +129,9 @@
<!-- Row 3: Activation conditions -->

<!-- Buttons -->
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button Content="Apply" Width="75" Margin="10,0,0,0" Click="ApplyButton_Click" ToolTip="Saves changes without closing the dialog."/>
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Center" Margin="5">
<Button Content="Save" Width="75" Margin="10,0,0,0" Click="SaveButton_Click" ToolTip="Saves changes and closes the dialog."/>
<Button Content="Apply" Width="75" Margin="10,0,0,0" Click="ApplyButton_Click" ToolTip="Saves changes without closing the dialog."/>
<Button Content="Cancel" Width="75" Margin="10,0,0,0" Click="CancelButton_Click" ToolTip="Closes the dialog without saving changes."/>
</StackPanel>
</Grid>
Expand Down
12 changes: 12 additions & 0 deletions WinAudioAssistant/WinAudioAssistant.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,41 @@
<UseWPF>true</UseWPF>
</PropertyGroup>

<ItemGroup>
<None Remove="Resources\Icons\MissingFile.ico" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="gong-wpf-dragdrop" Version="3.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<Reference Include="AudioSwitcher.AudioApi">
<HintPath>.\Libs\Debug\AudioSwitcher.AudioApi.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
<Reference Include="AudioSwitcher.AudioApi">
<HintPath>.\Libs\Release\AudioSwitcher.AudioApi.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<Reference Include="AudioSwitcher.AudioApi.CoreAudio">
<HintPath>.\Libs\Debug\AudioSwitcher.AudioApi.CoreAudio.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
<Reference Include="AudioSwitcher.AudioApi.CoreAudio">
<HintPath>.\Libs\Release\AudioSwitcher.AudioApi.CoreAudio.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<Resource Include="Resources\Icons\MissingFile.ico" />
</ItemGroup>
</Project>

0 comments on commit 8ada456

Please sign in to comment.