Compare commits

..

4 Commits

Author SHA1 Message Date
f5ab700817 add some error handling for audio sessions 2024-10-02 18:06:27 -06:00
d1a5e37067 updating readme for overrides 2024-04-20 21:47:42 -06:00
fdfa32909f Add in the ability to make custom overrides.
Fixed issues with the CSS in the property inspector
Make it so i can actually write unit tests if i want to
2024-04-20 21:19:44 -06:00
7abbc92080 Improve sound browser to make it have more stuff in it
improve sound browser debuggability by making it capture the window earlier so you can set a breakpoint and not have it just get VS info

put in code to try to handle helldivers 2
2024-02-24 23:46:00 -07:00
38 changed files with 1230 additions and 340 deletions

67
Overrides.md Normal file
View File

@ -0,0 +1,67 @@
## Overrides
Some games use particularly agressive forms of anti-cheat that interfere with the ability to determine information about the focused application, and then pair it to the appropriate audio process.
Unfortunately, there is nothing I can do about that.
In order to work around this, the overrides mechanism is there for you to set up manual mappings.
I chose to base it off of a process's Main Window Title.
These can change throughout the usage of an application. Once again, there's nothing I can do about that.
The reason I chose to use the Main Window Title despite this problem, is because in the case of some games, it was one of the only pieces of information that I could get about the running process due to its anti-cheat.
There is no way of knowing if this will work in all cases, but it seems to be reliable for the time being. And if I'm ever unable to get the Main Window's Title, I don't know if there will be a different data point to use for matching. Its kind of just the only thing available.
In order to make it so that I don't have to update the plugin for each game using agressive anti-cheat, and to make it so that you don't have to wait for me to fix something, I have created a way to put overrides into the plugin directly.
## Syntax
```
<Match type>: Window title string
audio process string
//lines starting in // are comments, and are ignored
```
```
Match Type
eq: equals - case insensitive
start: starts with - case insensitive
end: ends with - case insensitive
regex: regular expression - case sensitive
```
## Examples
```
//helldivers 2 has a trademark symbol in it, and those are hard to type.
//so we just find a window that starts with helldivers
start: Helldivers
helldivers
```
```
//you can actually map anything you want. it doesn't have to be only things with anti-cheat problems
eq: task manager
Google Chrome
```
## Help
Getting window titles can be a little hard sometimes. You can mouse over the icon on the start bar, and get it from there.
Another great way to get it is to run this powershell
```
Get-Process | Where-Object ($_.mainWindowTitle} | Format-Table mainWindowTitle
```
For audio processes right click on the volume in the tray of the start bar, and open the Volume Mixer. When you look through the list of apps, you can just type the name from that.
Alternatively you can put in the name of the actual executable. The easiest way to get to those is to run the SoundBrowser published in the releases in github.

View File

@ -12,6 +12,11 @@ The screen updates to show the name/icon of the app so that you can always know
![Focus volume control plugin preview](previews/1-preview.png?raw=true) ![Focus volume control plugin preview](previews/1-preview.png?raw=true)
## Help
If you're having trouble with a specific applicaiton, there is an override that you can put in.
[More Details Here](Overrides.md)
## Developing ## Developing
build the solution with visual studio build the solution with visual studio

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,168 @@
using FocusVolumeControl.Overrides;
namespace FocusVolumeControl.UnitTests
{
public class OverrideParserTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\n")]
public void BlankReturnsEmpty(string str)
{
//arrange
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Empty(overrides);
}
[Fact]
public void HelldiversParses()
{
//arrange
var str =
"""
eq: HELLDIVERS 2
helldivers2
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Single(overrides);
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
}
[Fact]
public void MultipleOverridesParse()
{
//arrange
var str =
"""
eq: HELLDIVERS 2
helldivers2
start: Task
Steam
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
Assert.Equal(MatchType.StartsWith, overrides[1].MatchType);
Assert.Equal("Task", overrides[1].WindowQuery);
Assert.Equal("Steam", overrides[1].AudioProcessName);
}
[Fact]
public void IncompleteMatchesAreSkipped()
{
//arrange
var str =
"""
eq: HELLDIVERS 2
start: Task
Steam
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Single(overrides);
Assert.Equal(MatchType.StartsWith, overrides[0].MatchType);
Assert.Equal("Task", overrides[0].WindowQuery);
Assert.Equal("Steam", overrides[0].AudioProcessName);
}
[Fact]
public void InvalidMatchesAreSkipped()
{
//arrange
var str =
"""
equal: Chrome
chrome
end: Task
Steam
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Single(overrides);
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
Assert.Equal("Task", overrides[0].WindowQuery);
Assert.Equal("Steam", overrides[0].AudioProcessName);
}
[Fact]
public void MatchesAreCaseInsensitive()
{
//arrange
var str =
"""
Eq: 0
0
eNd: 1
1
StArT: 2
2
Regex: 3
3
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
Assert.Equal(MatchType.EndsWith, overrides[1].MatchType);
Assert.Equal(MatchType.StartsWith, overrides[2].MatchType);
Assert.Equal(MatchType.Regex, overrides[3].MatchType);
}
[Fact]
public void CommentsAreSkipped()
{
//arrange
var str =
"""
//Eq: 0
//0
end: 1
1
""";
//act
var overrides = OverrideParser.Parse(str);
//assert
Assert.Single(overrides);
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
Assert.Equal("1", overrides[0].WindowQuery);
Assert.Equal("1", overrides[0].AudioProcessName);
}
}
}

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl", "Focus
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl.UnitTests", "FocusVolumeControl.UnitTests\FocusVolumeControl.UnitTests.csproj", "{322E16C9-C96E-45DF-912F-DB6366170645}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,10 @@ Global
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.Build.0 = Release|Any CPU {0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.Build.0 = Release|Any CPU
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.Build.0 = Debug|Any CPU
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.ActiveCfg = Release|Any CPU
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,9 +1,10 @@
using FocusVolumeControl.AudioSessions; using FocusVolumeControl.AudioSessions;
using FocusVolumeControl.Overrides;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Text.RegularExpressions;
namespace FocusVolumeControl.AudioHelpers; namespace FocusVolumeControl.AudioHelpers;
@ -15,6 +16,8 @@ public class AudioHelper
int[] _currentProcesses; int[] _currentProcesses;
int _retryFallbackCount = 0; int _retryFallbackCount = 0;
public List<Override> Overrides { get; set; }
public IAudioSession Current { get; private set; } public IAudioSession Current { get; private set; }
public void ResetCache() public void ResetCache()
@ -39,7 +42,6 @@ public class AudioHelper
public IAudioSession FindSession(List<Process> processes) public IAudioSession FindSession(List<Process> processes)
{ {
//var blah = new AudioPolicyConfigFactoryImplFor21H2();
var results = new ActiveAudioSessionWrapper(); var results = new ActiveAudioSessionWrapper();
Process bestProcessMatch = null; Process bestProcessMatch = null;
@ -51,6 +53,11 @@ public class AudioHelper
{ {
deviceCollection.Item(d, out var device); deviceCollection.Item(d, out var device);
if(device == null)
{
continue;
}
Guid iid = typeof(IAudioSessionManager2).GUID; Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m); device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
var manager = (IAudioSessionManager2)m; var manager = (IAudioSessionManager2)m;
@ -59,6 +66,11 @@ public class AudioHelper
manager.GetSessionEnumerator(out var sessionEnumerator); manager.GetSessionEnumerator(out var sessionEnumerator);
if(sessionEnumerator == null)
{
continue;
}
var currentIndex = int.MaxValue; var currentIndex = int.MaxValue;
sessionEnumerator.GetCount(out var count); sessionEnumerator.GetCount(out var count);
@ -66,6 +78,11 @@ public class AudioHelper
{ {
sessionEnumerator.GetSession(i, out var session); sessionEnumerator.GetSession(i, out var session);
if(session == null)
{
continue;
}
session.GetProcessId(out var sessionProcessId); session.GetProcessId(out var sessionProcessId);
var audioProcess = GetProcessById(sessionProcessId); var audioProcess = GetProcessById(sessionProcessId);
@ -74,16 +91,6 @@ public class AudioHelper
continue; continue;
} }
/*
blah.GetPersistedDefaultAudioEndpoint(sessionProcessId, DataFlow.Render, Role.Multimedia, out var persistedDeviceId);
persistedDeviceId = UnpackDeviceId(persistedDeviceId);
if(!string.IsNullOrEmpty(persistedDeviceId) && persistedDeviceId != currentDeviceId)
{
continue;
}
*/
var index = processes.FindIndex(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName); var index = processes.FindIndex(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName);
if (index > -1) if (index > -1)
@ -125,7 +132,12 @@ public class AudioHelper
{ {
lock (_lock) lock (_lock)
{ {
var processes = GetPossibleProcesses(); var processes = TryGetProcessFromOverrides();
if(processes == null)
{
processes = GetPossibleProcesses();
}
var processIds = processes?.Select(x => x.Id).ToArray(); var processIds = processes?.Select(x => x.Id).ToArray();
//_currentProcesses null - first time getting sessions //_currentProcesses null - first time getting sessions
@ -174,9 +186,9 @@ public class AudioHelper
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now /// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public List<Process> GetPossibleProcesses() public List<Process> GetPossibleProcesses(IntPtr? handleOverride = null)
{ {
var handle = Native.GetForegroundWindow(); var handle = handleOverride ?? Native.GetForegroundWindow();
if (handle == IntPtr.Zero) if (handle == IntPtr.Zero)
{ {
@ -186,10 +198,19 @@ public class AudioHelper
var ids = Native.GetProcessesOfChildWindows(handle); var ids = Native.GetProcessesOfChildWindows(handle);
Native.GetWindowThreadProcessId(handle, out var pid); Native.GetWindowThreadProcessId(handle, out var pid);
if(pid != 0)
{
ids.Insert(0, pid); ids.Insert(0, pid);
}
if(ids.Count == 0)
{
return new List<Process>();
}
var processes = ids.Distinct() var processes = ids.Distinct()
.Select(x => Process.GetProcessById(x)) .Select(Process.GetProcessById)
.ToList(); .ToList();
if(processes.FirstOrDefault()?.ProcessName == "explorer") if(processes.FirstOrDefault()?.ProcessName == "explorer")
@ -235,6 +256,11 @@ public class AudioHelper
{ {
deviceCollection.Item(d, out var device); deviceCollection.Item(d, out var device);
if(device == null)
{
continue;
}
Guid iid = typeof(IAudioSessionManager2).GUID; Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m); device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
var manager = (IAudioSessionManager2)m; var manager = (IAudioSessionManager2)m;
@ -242,11 +268,21 @@ public class AudioHelper
manager.GetSessionEnumerator(out var sessionEnumerator); manager.GetSessionEnumerator(out var sessionEnumerator);
if(sessionEnumerator == null)
{
continue;
}
sessionEnumerator.GetCount(out var count); sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
sessionEnumerator.GetSession(i, out var session); sessionEnumerator.GetSession(i, out var session);
if(session == null)
{
continue;
}
var volume = (ISimpleAudioVolume)session; var volume = (ISimpleAudioVolume)session;
var guid = Guid.Empty; var guid = Guid.Empty;
volume.SetMasterVolume(1, ref guid); volume.SetMasterVolume(1, ref guid);
@ -265,6 +301,10 @@ public class AudioHelper
{ {
deviceCollection.Item(d, out var device); deviceCollection.Item(d, out var device);
if(device == null)
{
continue;
}
Guid iid = typeof(IAudioSessionManager2).GUID; Guid iid = typeof(IAudioSessionManager2).GUID;
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m); device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
@ -273,11 +313,21 @@ public class AudioHelper
manager.GetSessionEnumerator(out var sessionEnumerator); manager.GetSessionEnumerator(out var sessionEnumerator);
if(sessionEnumerator == null)
{
continue;
}
sessionEnumerator.GetCount(out var count); sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
sessionEnumerator.GetSession(i, out var session); sessionEnumerator.GetSession(i, out var session);
if(session == null)
{
continue;
}
if (session.IsSystemSoundsSession() == 0) if (session.IsSystemSoundsSession() == 0)
{ {
return new SystemSoundsAudioSession(session); return new SystemSoundsAudioSession(session);
@ -300,123 +350,138 @@ public class AudioHelper
} }
static Dictionary<string, List<int>> _audioSessionNameCache = new Dictionary<string, List<int>>();
private List<int> GetFromNameCacheIfPossible(string processName)
class AudioPolicyConfigFactoryImplFor21H2
{ {
private readonly IAudioPolicyConfigFactoryVariantFor21H2 _factory; if(_audioSessionNameCache.TryGetValue(processName, out var result))
internal AudioPolicyConfigFactoryImplFor21H2()
{ {
var iid = typeof(IAudioPolicyConfigFactoryVariantFor21H2).GUID; if(result.Count!= 0)
Combase.RoGetActivationFactory("Windows.Media.Internal.AudioPolicyConfig", ref iid, out object factory); {
_factory = (IAudioPolicyConfigFactoryVariantFor21H2)factory; foreach(var pid in result)
{
var p = GetProcessById(pid);
if(p == null || p.ProcessName != processName)
{
_audioSessionNameCache.Remove(processName);
return null;
}
}
return result;
}
}
return null;
} }
public uint ClearAllPersistedApplicationDefaultEndpoints() private List<Process> TryGetProcessFromOverrides(IntPtr? handleOverride = null)
{ {
return _factory.ClearAllPersistedApplicationDefaultEndpoints(); var handle = handleOverride ?? Native.GetForegroundWindow();
}
public uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, out string deviceId) if (Overrides?.Any() == true)
{ {
return _factory.GetPersistedDefaultAudioEndpoint(processId, flow, role, out deviceId); Process tmp = null;
} foreach (var p in Process.GetProcesses())
public uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId)
{ {
return _factory.SetPersistedDefaultAudioEndpoint(processId, flow, role, deviceId); if (p.MainWindowHandle == handle)
{
tmp = p;
break;
} }
} }
if (tmp != null)
[Guid("ab3d4648-e242-459f-b02f-541c70306324")]
[InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
public interface IAudioPolicyConfigFactoryVariantFor21H2
{ {
int __incomplete__add_CtxVolumeChange(); foreach (var o in Overrides)
int __incomplete__remove_CtxVolumeChanged(); {
int __incomplete__add_RingerVibrateStateChanged(); if (
int __incomplete__remove_RingerVibrateStateChange(); (o.MatchType == MatchType.Equal && tmp.MainWindowTitle.Equals(o.WindowQuery, StringComparison.InvariantCultureIgnoreCase))
int __incomplete__SetVolumeGroupGainForId(); || (o.MatchType == MatchType.StartsWith && tmp.MainWindowTitle.StartsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
int __incomplete__GetVolumeGroupGainForId(); || (o.MatchType == MatchType.EndsWith && tmp.MainWindowTitle.EndsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
int __incomplete__GetActiveVolumeGroupForEndpointId(); || (o.MatchType == MatchType.Regex && Regex.IsMatch(tmp.MainWindowTitle, o.WindowQuery))
int __incomplete__GetVolumeGroupsForEndpoint(); )
int __incomplete__GetCurrentVolumeContext(); {
int __incomplete__SetVolumeGroupMuteForId(); var ids = FindAudioSessionByProcessName(o.AudioProcessName);
int __incomplete__GetVolumeGroupMuteForId(); if (ids?.Count > 0)
int __incomplete__SetRingerVibrateState(); {
int __incomplete__GetRingerVibrateState(); return ids.Distinct()
int __incomplete__SetPreferredChatApplication(); .Select(Process.GetProcessById)
int __incomplete__ResetPreferredChatApplication(); .ToList();
int __incomplete__GetPreferredChatApplication();
int __incomplete__GetCurrentChatApplications(); }
int __incomplete__add_ChatContextChanged(); }
int __incomplete__remove_ChatContextChanged(); }
[PreserveSig] }
uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId); }
[PreserveSig] return null;
uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, [Out, MarshalAs(UnmanagedType.HString)] out string deviceId);
[PreserveSig]
uint ClearAllPersistedApplicationDefaultEndpoints();
} }
[Guid("2a59116d-6c4f-45e0-a74f-707e3fef9258")] private List<int> FindAudioSessionByProcessName(string processName)
[InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
public interface IAudioPolicyConfigFactoryVariantForDownlevel
{ {
int __incomplete__add_CtxVolumeChange(); var cached = GetFromNameCacheIfPossible(processName);
int __incomplete__remove_CtxVolumeChanged(); if(cached != null)
int __incomplete__add_RingerVibrateStateChanged(); {
int __incomplete__remove_RingerVibrateStateChange(); return cached;
int __incomplete__SetVolumeGroupGainForId();
int __incomplete__GetVolumeGroupGainForId();
int __incomplete__GetActiveVolumeGroupForEndpointId();
int __incomplete__GetVolumeGroupsForEndpoint();
int __incomplete__GetCurrentVolumeContext();
int __incomplete__SetVolumeGroupMuteForId();
int __incomplete__GetVolumeGroupMuteForId();
int __incomplete__SetRingerVibrateState();
int __incomplete__GetRingerVibrateState();
int __incomplete__SetPreferredChatApplication();
int __incomplete__ResetPreferredChatApplication();
int __incomplete__GetPreferredChatApplication();
int __incomplete__GetCurrentChatApplications();
int __incomplete__add_ChatContextChanged();
int __incomplete__remove_ChatContextChanged();
[PreserveSig]
uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId);
[PreserveSig]
uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, [Out, MarshalAs(UnmanagedType.HString)] out string deviceId);
[PreserveSig]
uint ClearAllPersistedApplicationDefaultEndpoints();
} }
static class Combase var results = new List<int>();
{
[DllImport("combase.dll", PreserveSig = false)]
public static extern void RoGetActivationFactory(
[MarshalAs(UnmanagedType.HString)] string activatableClassId,
[In] ref Guid iid,
[Out, MarshalAs(UnmanagedType.IInspectable)] out Object factory);
[DllImport("combase.dll", PreserveSig = false)] var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
public static extern void WindowsCreateString(
[MarshalAs(UnmanagedType.LPWStr)] string src, deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
[In] uint length, deviceCollection.GetCount(out var numDevices);
[Out] out IntPtr hstring); for (int d = 0; d < numDevices; d++)
{
deviceCollection.Item(d, out var device);
if(device == null)
{
continue;
} }
private const string DEVINTERFACE_AUDIO_RENDER = "#{e6327cad-dcec-4949-ae8a-991e976a79d2}"; Guid iid = typeof(IAudioSessionManager2).GUID;
private const string DEVINTERFACE_AUDIO_CAPTURE = "#{2eef81be-33fa-4800-9670-1cd474972c3f}"; device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
private const string MMDEVAPI_TOKEN = @"\\?\SWD#MMDEVAPI#"; var manager = (IAudioSessionManager2)m;
private string UnpackDeviceId(string deviceId)
device.GetId(out var currentDeviceId);
manager.GetSessionEnumerator(out var sessionEnumerator);
if(sessionEnumerator == null)
{ {
if (deviceId.StartsWith(MMDEVAPI_TOKEN)) deviceId = deviceId.Remove(0, MMDEVAPI_TOKEN.Length); continue;
if (deviceId.EndsWith(DEVINTERFACE_AUDIO_RENDER)) deviceId = deviceId.Remove(deviceId.Length - DEVINTERFACE_AUDIO_RENDER.Length);
if (deviceId.EndsWith(DEVINTERFACE_AUDIO_CAPTURE)) deviceId = deviceId.Remove(deviceId.Length - DEVINTERFACE_AUDIO_CAPTURE.Length);
return deviceId;
} }
sessionEnumerator.GetCount(out var count);
for (int i = 0; i < count; i++)
{
sessionEnumerator.GetSession(i, out var session);
if(session == null)
{
continue;
}
session.GetDisplayName(out var displayName);
session.GetProcessId(out var sessionProcessId);
var audioProcess = GetProcessById(sessionProcessId);
if(audioProcess == null)
{
continue;
}
var audioProcessName = _nameAndIconHelper.TryGetProcessNameWithoutIcon(audioProcess);
if(audioProcess.ProcessName == processName || displayName == processName || processName == audioProcessName)
{
results.Add(sessionProcessId);
}
}
}
results = results.Distinct().ToList();
_audioSessionNameCache[processName] = results;
return results;
}
} }

View File

@ -83,6 +83,41 @@ public class NameAndIconHelper
} }
} }
public string TryGetProcessNameWithoutIcon(Process process)
{
try
{
//appx packages are installed from the windows store. eg, itunes
var appx = AppxPackage.FromProcess(process);
if (appx == null)
{
//if the display name is already set, then it came from the display name of the audio session
if(!string.IsNullOrEmpty(process.MainWindowTitle))
{
return process.MainWindowTitle;
}
//using process.MainModule.FileVersionInfo sometimes throws permission exceptions
//we get the file version info with a limited query flag to avoid that
var fileVersionInfo = GetFileVersionInfo(process);
if(!string.IsNullOrEmpty(fileVersionInfo?.FileDescription))
{
return fileVersionInfo.FileDescription;
}
return process.ProcessName;
}
else
{
return appx.DisplayName ?? process.ProcessName;
}
}
catch { }
return process.ProcessName;
}
FileVersionInfo GetFileVersionInfo(Process process) FileVersionInfo GetFileVersionInfo(Process process)
{ {
var path = GetExecutablePathWithPInvoke(process); var path = GetExecutablePathWithPInvoke(process);

View File

@ -10,13 +10,19 @@ using FocusVolumeControl.AudioSession;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
#nullable enable
public sealed class ActiveAudioSessionWrapper : IAudioSession public sealed class ActiveAudioSessionWrapper : IAudioSession
{ {
public string DisplayName { get; set; } public string DisplayName { get; set; } = null!;
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>(); private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>(); private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
public IconWrapper? IconWrapper { get; set; } public IconWrapper? IconWrapper { get; set; }
public IEnumerable<int> Pids => Sessions.Select(x =>
{
x.GetProcessId(out var pid); return pid;
});
public string GetIcon() public string GetIcon()
{ {
@ -98,3 +104,4 @@ public sealed class ActiveAudioSessionWrapper : IAudioSession
return VolumeHelpers.GetVolumePercentage(level); return VolumeHelpers.GetVolumePercentage(level);
} }
} }
#nullable restore

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Data; using System.Data;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
@ -91,6 +92,91 @@ public interface IMMDeviceEnumerator
int GetDefaultAudioEndpoint(DataFlow dataFlow, Role role, out IMMDevice device); int GetDefaultAudioEndpoint(DataFlow dataFlow, Role role, out IMMDevice device);
} }
public enum EStgmAccess
{
STGM_READ = 0x0
}
[Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore
{
[PreserveSig]
int GetCount(out int count);
[PreserveSig]
int GetAt(int iProp, out PropertyKey pkey);
[PreserveSig]
int GetValue(ref PropertyKey key, out PropVariant pv);
[PreserveSig]
int SetValue(ref PropertyKey key, ref PropVariant propvar);
[PreserveSig]
int Commit();
}
public struct PropertyKey
{
public Guid fmtId;
public int PId;
public PropertyKey(Guid fmtId, int pId)
{
this.fmtId = fmtId;
this.PId = pId;
}
}
public struct Blob
{
public int Length;
public IntPtr Data;
}
[StructLayout(LayoutKind.Explicit)]
public struct PropVariant
{
[FieldOffset(0)] short vt;
[FieldOffset(2)] short wReserved1;
[FieldOffset(4)] short wReserved2;
[FieldOffset(6)] short wReserved3;
[FieldOffset(8)] sbyte cVal;
[FieldOffset(8)] byte bVal;
[FieldOffset(8)] short iVal;
[FieldOffset(8)] ushort uiVal;
[FieldOffset(8)] int lVal;
[FieldOffset(8)] uint ulVal;
[FieldOffset(8)] long hVal;
[FieldOffset(8)] ulong uhVal;
[FieldOffset(8)] float fltVal;
[FieldOffset(8)] double dblVal;
[FieldOffset(8)] Blob blobVal;
[FieldOffset(8)] DateTime date;
[FieldOffset(8)] bool boolVal;
[FieldOffset(8)] int scode;
[FieldOffset(8)] FILETIME filetime;
[FieldOffset(8)] IntPtr everything_else;
public object Value
{
get
{
var ve = (VarEnum)vt;
if((VarEnum)vt == VarEnum.VT_LPWSTR)
{
return Marshal.PtrToStringUni(everything_else);
}
throw new Exception();
}
}
}
public static class PKey
{
public static readonly PropertyKey DeviceFriendlyName = new(new(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14);
public static readonly PropertyKey DeviceFriendlyNameAttributes = new(new(0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b), 3);
public static readonly PropertyKey DeviceInterfaceFriendlyName = new(new(0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22), 2);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMMDevice public interface IMMDevice
{ {
@ -98,7 +184,7 @@ public interface IMMDevice
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
[PreserveSig] [PreserveSig]
int NotImpl1(); int OpenPropertyStore(EStgmAccess stgmAccess, out IPropertyStore propertyStore);
[PreserveSig] [PreserveSig]
int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId); int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
@ -15,4 +17,6 @@ public interface IAudioSession
public void IncrementVolumeLevel(int step, int ticks); public void IncrementVolumeLevel(int step, int ticks);
public int GetVolumeLevel(); public int GetVolumeLevel();
public IEnumerable<int> Pids { get; }
} }

View File

@ -4,6 +4,8 @@ using FocusVolumeControl.UI;
using System; using System;
using System.Drawing; using System.Drawing;
#nullable enable
namespace FocusVolumeControl.AudioSession namespace FocusVolumeControl.AudioSession
{ {
public abstract class IconWrapper public abstract class IconWrapper
@ -101,3 +103,4 @@ namespace FocusVolumeControl.AudioSession
} }
} }
#nullable restore

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
@ -15,6 +16,9 @@ internal sealed class SystemSoundsAudioSession : IAudioSession
ISimpleAudioVolume _volumeControl; ISimpleAudioVolume _volumeControl;
public string DisplayName => "System sounds"; public string DisplayName => "System sounds";
public IEnumerable<int> Pids => new int[0];
public string GetIcon() => "Images/systemSounds"; public string GetIcon() => "Images/systemSounds";
public void ToggleMute() public void ToggleMute()

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace FocusVolumeControl.AudioSessions; namespace FocusVolumeControl.AudioSessions;
@ -15,6 +16,8 @@ internal sealed class SystemVolumeAudioSession : IAudioSession
public string DisplayName => "System Volume"; public string DisplayName => "System Volume";
public string GetIcon() => "Images/encoderIcon"; public string GetIcon() => "Images/encoderIcon";
public IEnumerable<int> Pids => new int[0];
public void ToggleMute() public void ToggleMute()
{ {
_volumeControl.SetMute(!IsMuted(), Guid.Empty); _volumeControl.SetMute(!IsMuted(), Guid.Empty);

View File

@ -2,6 +2,7 @@
using BarRaider.SdTools.Payloads; using BarRaider.SdTools.Payloads;
using FocusVolumeControl.AudioHelpers; using FocusVolumeControl.AudioHelpers;
using FocusVolumeControl.AudioSessions; using FocusVolumeControl.AudioSessions;
using FocusVolumeControl.Overrides;
using FocusVolumeControl.UI; using FocusVolumeControl.UI;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -15,6 +16,12 @@ namespace FocusVolumeControl;
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")] [PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
public class DialAction : EncoderBase public class DialAction : EncoderBase
{ {
private const string DefaultOverrides =
"""
//eq: HELLDIVERS™ 2
//helldivers2
""";
private class PluginSettings private class PluginSettings
{ {
[JsonProperty("fallbackBehavior")] [JsonProperty("fallbackBehavior")]
@ -23,11 +30,15 @@ public class DialAction : EncoderBase
[JsonProperty("stepSize")] [JsonProperty("stepSize")]
public int StepSize { get; set; } public int StepSize { get; set; }
[JsonProperty("overrides")]
public string Overrides { get; set; }
public static PluginSettings CreateDefaultSettings() public static PluginSettings CreateDefaultSettings()
{ {
PluginSettings instance = new PluginSettings(); PluginSettings instance = new PluginSettings();
instance.FallbackBehavior = FallbackBehavior.SystemSounds; instance.FallbackBehavior = FallbackBehavior.SystemSounds;
instance.StepSize = 1; instance.StepSize = 1;
instance.Overrides = DefaultOverrides;
return instance; return instance;
} }
} }
@ -46,12 +57,18 @@ public class DialAction : EncoderBase
else else
{ {
settings = payload.Settings.ToObject<PluginSettings>(); settings = payload.Settings.ToObject<PluginSettings>();
if(string.IsNullOrEmpty(settings.Overrides))
{
settings.Overrides = DefaultOverrides;
_ = SaveSettings();
}
} }
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged; WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
try try
{ {
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
//just in case we fail to get the active session, don't prevent the plugin from launching //just in case we fail to get the active session, don't prevent the plugin from launching
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior); var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
_ = UpdateStateIfNeeded(session); _ = UpdateStateIfNeeded(session);
@ -220,7 +237,8 @@ public class DialAction : EncoderBase
try try
{ {
Tools.AutoPopulateSettings(settings, payload.Settings); Tools.AutoPopulateSettings(settings, payload.Settings);
_ = SaveSettings(); _audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
//_ = SaveSettings();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -66,6 +66,10 @@
<Compile Include="AudioSessions\IAudioSession.cs" /> <Compile Include="AudioSessions\IAudioSession.cs" />
<Compile Include="FallbackBehavior.cs" /> <Compile Include="FallbackBehavior.cs" />
<Compile Include="AudioHelpers\NameAndIconHelper.cs" /> <Compile Include="AudioHelpers\NameAndIconHelper.cs" />
<Compile Include="InternalsVisibleTo.cs" />
<Compile Include="Overrides\Override.cs" />
<Compile Include="Overrides\MatchType.cs" />
<Compile Include="Overrides\OverrideParser.cs" />
<Compile Include="UI\IconExtraction.cs" /> <Compile Include="UI\IconExtraction.cs" />
<Compile Include="UI\ISDConnectionExtensions.cs" /> <Compile Include="UI\ISDConnectionExtensions.cs" />
<Compile Include="Native.cs" /> <Compile Include="Native.cs" />
@ -91,7 +95,7 @@
<Content Include="Images\**\*.png"> <Content Include="Images\**\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css"> <Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css;PropertyInspector\assets\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="PropertyInspector\PluginActionPI.html"> <Content Include="PropertyInspector\PluginActionPI.html">

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("FocusVolumeControl.UnitTests")]
[assembly: InternalsVisibleTo("SoundBrowser")]

View File

@ -0,0 +1,10 @@
namespace FocusVolumeControl.Overrides
{
public enum MatchType
{
Equal,
StartsWith,
EndsWith,
Regex,
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl.Overrides
{
public class Override
{
public MatchType MatchType { get; set; }
public string WindowQuery { get; set; }
public string AudioProcessName { get; set; }
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl.Overrides
{
internal class OverrideParser
{
public static List<Override> Parse(string raw)
{
var overrides = new List<Override>();
if (raw == null)
{
return overrides;
}
var lines = raw.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
Override tmp = null;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line) || line.StartsWith("//"))
{
continue;
}
var split = line.Split(':');
if (split.Length > 1)
{
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
{
overrides.Add(tmp);
}
tmp = new Override();
if (string.Equals(split[0], "eq", StringComparison.OrdinalIgnoreCase))
{
tmp.MatchType = MatchType.Equal;
}
else if (string.Equals(split[0], "start", StringComparison.OrdinalIgnoreCase))
{
tmp.MatchType = MatchType.StartsWith;
}
else if (string.Equals(split[0], "end", StringComparison.OrdinalIgnoreCase))
{
tmp.MatchType = MatchType.EndsWith;
}
else if (string.Equals(split[0], "regex", StringComparison.OrdinalIgnoreCase))
{
tmp.MatchType = MatchType.Regex;
}
else
{
continue;
}
tmp.WindowQuery = split[1].Trim();
}
else if (tmp != null)
{
tmp.AudioProcessName = split[0].Trim();
}
}
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
{
overrides.Add(tmp);
}
return overrides;
}
}
}

View File

@ -6,8 +6,8 @@
<meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black> <meta name=apple-mobile-web-app-status-bar-style content=black>
<title>FocusVolumeControl Settings</title> <title>FocusVolumeControl Settings</title>
<link rel="stylesheet" href="./lib/sdpi.css"> <link rel="stylesheet" href="./css/sdpi.css">
<link rel="sytlesheet" href="./lib/rangeTooltip.css"> <link rel="sytlesheet" href="./css/rangeTooltip.css">
<script src="lib/sdtools.common.js"></script> <script src="lib/sdtools.common.js"></script>
<script src="lib/rangeTooltip.js"></script> <script src="lib/rangeTooltip.js"></script>
</head> </head>
@ -16,13 +16,20 @@
<div class="sdpi-item"> <div class="sdpi-item">
<div class="sdpi-item-label">Fallback</div> <div class="sdpi-item-label">Fallback</div>
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()"> <select class="sdpi-item-value select sdProperty" id="fallbackBehavior" oninput="setSettings()">
<option value="0">System Sounds</option> <option value="0">System Sounds</option>
<option value="1">Previous App</option> <option value="1">Previous App</option>
<option value="2">Default Output Device Volume</option> <option value="2">Default Output Device Volume</option>
</select> </select>
</div> </div>
<details>
<summary>Fallback Details</summary>
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
<p>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p>
</details>
<div type="range" class="sdpi-item sdShowTooltip"> <div type="range" class="sdpi-item sdShowTooltip">
<div class="sdpi-item-label">Step Size</div> <div class="sdpi-item-label">Step Size</div>
@ -32,15 +39,29 @@
<span class="clickable" value="1">10</span> <span class="clickable" value="1">10</span>
</div> </div>
</div> </div>
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div> <div class="sdpi-info-label hidden" style="z-index: 999;" value="">Tooltip</div>
<div type="textarea" class="sdpi-item">
<div class="sdpi-item-label">Overrides</div>
<span class="sdpi-item-value" textarea>
<textarea type="textarea" class="sdProperty" id="overrides" oninput="setSettings()"></textarea>
</span>
</div>
<details> <details>
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p> <summary>Override Details</summary>
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p> <p>Some games use anti-cheat software that interferes with the ability to know what application is running, and pair it to the appropriate audio process.</p>
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p> <p>There is nothing I can do about this.</p>
<p>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p> <p>In order to work around this, the overrides mechanism is there for you to manually map an application window to an audio process.</p>
<p>They synax for this is</p>
<p>[matching]: Window Title <br />Audio Process</p>
<p>Blank lines can be used for spacing<br/>Lines starting with // are ignored</p>
<p>[matching] can be eq, start, end, or regex. Which will perform an exact match, starts with, ends with, or regular expressions respectively.</p>
<p>Example:<br />eq: Task Manager<br />Discord</p>
</details> </details>
</div> </div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6">
<polygon fill="#8E8E92" fill-rule="evenodd" points="5 4 9 0 10 1 5 6 0 1 1 0"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12">
<g fill="#8E8E92" transform="translate(4, 5) rotate(-90) translate(-4, -5) translate(-2, 2)">
<polygon id="Path" points="5 4 9 0 10 1 5 6 0 1 1 0"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="10" viewBox="0 0 12 10">
<polygon fill="#FFF" points="7.2 7.5 7.2 -1.3 8.7 -1.3 8.6 9.1 2.7 8.7 2.7 7.2" transform="rotate(37 5.718 3.896)"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<path fill="#9C9C9C" fill-rule="nonzero"
d="M1,5 L1,14 L14,14 L14,5 L1,5 Z M0,1 L15,1 L15,15 L0,15 L0,1 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
<rect width="1" height="1" x="2" fill="#9C9C9C" fill-rule="nonzero"/>
<rect width="1" height="1" x="12" fill="#9C9C9C" fill-rule="nonzero"/>
<g transform="translate(3 7)">
<rect width="1" height="1" x="2" fill="#9C9C9C"/>
<rect width="1" height="1" fill="#666"/>
<rect width="1" height="1" x="4" fill="#9C9C9C"/>
<rect width="1" height="1" x="6" fill="#9C9C9C"/>
<rect width="1" height="1" x="8" fill="#9C9C9C"/>
<rect width="1" height="1" y="2" fill="#9C9C9C"/>
<rect width="1" height="1" x="2" y="2" fill="#9C9C9C"/>
<rect width="1" height="1" x="4" y="2" fill="#9C9C9C"/>
<rect width="1" height="1" x="6" y="2" fill="#9C9C9C"/>
<rect width="1" height="1" x="8" y="2" fill="#9C9C9C"/>
<rect width="1" height="1" y="4" fill="#9C9C9C"/>
<rect width="1" height="1" x="2" y="4" fill="#9C9C9C"/>
<rect width="1" height="1" x="4" y="4" fill="#9C9C9C"/>
<rect width="1" height="1" x="6" y="4" fill="#9C9C9C"/>
<rect width="1" height="1" x="8" y="4" fill="#666"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="#9C9C9C">
<path d="M15,15 L1.77635684e-15,15 L1.77635684e-15,1 L15,1 L15,15 Z M5,7 L5,8 L6,8 L6,7 L5,7 Z M3,7 L3,8 L4,8 L4,7 L3,7 Z M7,7 L7,8 L8,8 L8,7 L7,7 Z M9,7 L9,8 L10,8 L10,7 L9,7 Z M11,7 L11,8 L12,8 L12,7 L11,7 Z M3,9 L3,10 L4,10 L4,9 L3,9 Z M5,9 L5,10 L6,10 L6,9 L5,9 Z M7,9 L7,10 L8,10 L8,9 L7,9 Z M9,9 L9,10 L10,10 L10,9 L9,9 Z M11,9 L11,10 L12,10 L12,9 L11,9 Z M3,11 L3,12 L4,12 L4,11 L3,11 Z M5,11 L5,12 L6,12 L6,11 L5,11 Z M7,11 L7,12 L8,12 L8,11 L7,11 Z M9,11 L9,12 L10,12 L10,11 L9,11 Z M11,11 L11,12 L12,12 L12,11 L11,11 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
<rect width="1" height="1" x="2"/>
<rect width="1" height="1" x="12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 780 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="6" viewBox="0 0 6 6">
<circle cx="3" cy="3" r="3" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 133 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="8" viewBox="0 0 1 8">
<rect id="Rectangle" width="1" height="6" x="0" y="1" fill="#555"/>
</svg>

After

Width:  |  Height:  |  Size: 159 B

View File

@ -1,65 +1,3 @@
.linkspan {
cursor: pointer;
color: #7397d2;
text-decoration: underline;
}
.titleAlignedSmall {
font-size: 9pt;
padding-left: 33px !important;
}
.small {
font-size: 9pt !important;
}
.leftMargin10 {
margin-left: 10px;
}
.leftMargin0 {
margin-left: 0px !important;
}
.leftPadding3 {
padding-left: 3px !important;
}
.leftPadding0 {
padding-left: 0px !important;
}
.bright {
color: #d8d8d8;
}
.iconLeft {
background-position: 0px 4px !important;
}
.summaryIconPadding {
padding-left: 15px;
}
.subMenu {
border-left: 1px dotted gray;
padding-left: 15px;
max-width: 96%;
background-color: #323232;
}
:root {
--sdpi-bgcolor: #2D2D2D;
--sdpi-background: #3D3D3D;
--sdpi-color: #d8d8d8;
--sdpi-bordercolor: #3a3a3a;
--sdpi-buttonbordercolor: #969696;
--sdpi-borderradius: 0px;
--sdpi-width: 224px;
--sdpi-fontweight: 600;
--sdpi-letterspacing: -0.25pt;
}
html { html {
--sdpi-bgcolor: #2D2D2D; --sdpi-bgcolor: #2D2D2D;
--sdpi-background: #3D3D3D; --sdpi-background: #3D3D3D;
@ -70,6 +8,16 @@ html {
--sdpi-width: 224px; --sdpi-width: 224px;
--sdpi-fontweight: 600; --sdpi-fontweight: 600;
--sdpi-letterspacing: -0.25pt; --sdpi-letterspacing: -0.25pt;
--sdpi-tab-color: #969696;
--sdpi-tab-left-margin: 1px;
--sdpi-tab-top-offset: 1px;
--sdpi-tab-selected-color: #333333;
--sdpi-tab-selected-top-offset: 0px;
--sdpi-tab-font-size: 9pt;
--sdpi-tab-container-left-offset: 5px;
--sdpi-tab-padding-horizontal: 12px;
--sdpi-tab-padding-vertical: 5px;
--sdpi-linecolor: #454545;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
@ -119,8 +67,16 @@ hr2,
margin: 8px 0px; margin: 8px 0px;
} }
.sdpi-heading::before,
.sdpi-heading::after { h1 {
font-size: 1.3em;
font-weight: 500;
text-align: center;
margin-bottom: 12px;
}
.sdpi-heading::before,
.sdpi-heading::after {
content: ""; content: "";
flex-grow: 1; flex-grow: 1;
background: var(--sdpi-background); background: var(--sdpi-background);
@ -128,7 +84,7 @@ hr2,
font-size: 0px; font-size: 0px;
line-height: 0px; line-height: 0px;
margin: 0px 16px; margin: 0px 16px;
} }
hr2 { hr2 {
height: 2px; height: 2px;
@ -225,6 +181,9 @@ progress {
margin-bottom: 4px; margin-bottom: 4px;
} }
/* TABS */
.tabs { .tabs {
/** /**
* Setting display to flex makes this container lay * Setting display to flex makes this container lay
@ -232,21 +191,93 @@ progress {
* as in the above "Stepper input" example. * as in the above "Stepper input" example.
*/ */
display: flex; display: flex;
border-bottom: 1px solid #D7DBDD; border-bottom: 1px solid rgba(255, 255, 255, 0.0);
flex-wrap: nowrap;
white-space: nowrap;
overflow-x: auto;
text-transform: capitalize;
background-color: transparent;
margin-left: var(--sdpi-tab-container-left-offset);
}
.tabs::-webkit-scrollbar {
height: 4px;
display: none;
}
.tabs::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 8px;
}
.tabs::-webkit-scrollbar-thumb {
background-color: #444;
outline: 1px solid #444;
border-radius: 8px;
}
.tab-separator {
margin-left: 100px;
max-width: 234px;
margin-bottom: 20px;
margin-top: -4px;
} }
.tab { .tab {
cursor: pointer; cursor: pointer;
padding: 5px 30px; padding: var(--sdpi-tab-padding-vertical) var(--sdpi-tab-padding-horizontal);
color: #16a2d7; color: var(--sdpi-tab-color);
font-size: 9pt; font-size: var(--sdpi-tab-font-size);
border-bottom: 2px solid transparent; font-weight: var(--title-font-weight);
background-color: rgba(0, 0, 0, 0.1);
margin: 0px;
margin-top: var(--sdpi-tab-top-offset);
margin-left: var(--sdpi-tab-left-margin);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid var(--sdpi-linecolor);
-webkit-user-select: none;
user-select: none;
} }
.tab.is-tab-selected { .tab:first-child {
border-bottom-color: #4ebbe4; margin-left: 0px;
} }
.tab-container {
margin-top: -14px;
}
.tab-container > hr {
margin-left: 100px;
max-width: 234px;
}
.tabs + hr {
margin-left: 0px;
max-width: 234px;
margin-top: -6px;
}
.tab.selected {
color: white;
background-color: var(--sdpi-tab-selected-color);
border-bottom: 2px solid var(--sdpi-tab-selected-color);
margin-top: var(--sdpi-tab-selected-top-offset);
}
.sdpi-item.tabgroup {
margin-top: 0px;
}
.istab {
background-color: rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
padding: 4px;
}
select { select {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@ -306,18 +337,30 @@ option {
.sdpi-wrapper { .sdpi-wrapper {
overflow-x: hidden; overflow-x: hidden;
height: 100%; height: 100%;
margin-right: 1px; /* ensure scroller thumb is not clipped */
} }
.sdpi-item { .sdpi-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-height: 32px; min-height: 30px;
align-items: center; align-items: first baseline;
margin-top: 2px; margin-top: 2px;
max-width: 344px; max-width: 344px;
-webkit-user-drag: none; -webkit-user-drag: none;
} }
.sdpi-item[type="textarea"],
.sdpi-item[type="color"],
.sdpi-item[type="canvas"],
.sdpi-item .aligncenter {
align-items: center;
}
.sdpi-item[type="color"] > .sdpi-item-label {
line-height: 22px;
}
.sdpi-item:first-child { .sdpi-item:first-child {
margin-top: -1px; margin-top: -1px;
} }
@ -412,7 +455,7 @@ table > caption {
padding-right: 5px; padding-right: 5px;
font-weight: 600; font-weight: 600;
-webkit-user-select: none; -webkit-user-select: none;
line-height: 24px; line-height: normal;
margin-left: -1px; margin-left: -1px;
} }
@ -486,8 +529,8 @@ ol.sdpi-item-value,
margin-left: 5px; margin-left: 5px;
margin-right: 12px; margin-right: 12px;
padding: 4px !important; padding: 4px !important;
display: flex; /* display: flex;
flex-direction: column; flex-direction: column; */
} }
.two-items li { .two-items li {
@ -632,6 +675,16 @@ summary {
cursor: pointer; cursor: pointer;
} }
.sdpi-item.details {
align-items: first baseline;
}
/* needs Chromium update 2023
.sdpi-item:has(>details) {
align-items: first baseline;
}
*/
details * { details * {
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: normal;
@ -654,21 +707,21 @@ details.message {
} }
details.message > summary:first-of-type { details.message > summary:first-of-type {
/*line-height: 48px;*/ line-height: 20px;
} }
details.message h1 { details.message h1 {
text-align: left; text-align: left;
} }
details:not(.pointer) > summary { /* details:not(.pointer)>summary {
list-style: none; list-style: none;
} }
details > summary::-webkit-details-marker details > summary::-webkit-details-marker,
.message > summary::-webkit-details-marker { .message > summary::-webkit-details-marker {
display: none; display: none;
} } */
.info20, .info20,
.question, .question,
@ -702,7 +755,6 @@ details > summary::-webkit-details-marker
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
} }
.sdpi-more-info { .sdpi-more-info {
position: fixed; position: fixed;
left: 0px; left: 0px;
@ -731,7 +783,6 @@ details > summary::-webkit-details-marker
user-select: none; user-select: none;
} }
.sdpi-bottom-bar { .sdpi-bottom-bar {
display: flex; display: flex;
align-self: right; align-self: right;
@ -759,7 +810,6 @@ details a {
padding-right: 28px; padding-right: 28px;
} }
input:not([type="range"]), input:not([type="range"]),
textarea { textarea {
-webkit-appearance: none; -webkit-appearance: none;
@ -827,7 +877,6 @@ input[type="checkbox"] {
margin-top: -2px; margin-top: -2px;
min-width: 8px; min-width: 8px;
text-align: right; text-align: right;
user-select: none;
cursor: pointer; cursor: pointer;
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
@ -840,6 +889,10 @@ input[type="checkbox"] {
span + input[type="range"] { span + input[type="range"] {
display: flex; display: flex;
}
span + .range-container > input[type="range"],
span + input[type="range"] {
max-width: 168px; max-width: 168px;
} }
@ -1028,7 +1081,6 @@ textarea {
padding: 0; padding: 0;
} }
.card-carousel-wrapper { .card-carousel-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1091,7 +1143,8 @@ textarea {
margin: 0 5px; margin: 0 5px;
cursor: pointer; cursor: pointer;
/* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
background-color: #fff; /* background-color: #fff; */
text-align: center;
border-radius: 4px; border-radius: 4px;
z-index: 3; z-index: 3;
} }
@ -1114,6 +1167,7 @@ textarea {
.card-carousel-cards .card-carousel--card img:hover { .card-carousel-cards .card-carousel--card img:hover {
opacity: 0.5; opacity: 0.5;
background-color: rgba(255, 255, 255, .1);
} }
.card-carousel-cards .card-carousel--card--footer { .card-carousel-cards .card-carousel--card--footer {
@ -1141,44 +1195,8 @@ textarea {
color: #666a73; color: #666a73;
} }
h1 {
font-size: 1.3em;
font-weight: 500;
text-align: center;
margin-bottom: 12px;
}
::-webkit-datetime-edit {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background: url(../assets/elg_calendar_inv.svg) no-repeat left center;
padding-right: 1em;
padding-left: 25px;
background-position: 4px 0px;
}
::-webkit-datetime-edit-fields-wrapper {
}
::-webkit-datetime-edit-text {
padding: 0 0.3em;
}
::-webkit-datetime-edit-month-field {
}
::-webkit-datetime-edit-day-field {
}
::-webkit-datetime-edit-year-field {
}
::-webkit-inner-spin-button {
/* display: none; */
}
::-webkit-calendar-picker-indicator { ::-webkit-calendar-picker-indicator {
background: transparent; background: url(../assets/elg_calendar_inv.svg) no-repeat center;
font-size: 17px; font-size: 17px;
} }
@ -1186,53 +1204,30 @@ h1 {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
} }
input[type="text"]::-webkit-calendar-picker-indicator {
background: transparent;
font-size: 12px;
}
input[type="date"] { input[type="date"] {
-webkit-align-items: center; -webkit-align-items: center;
align-items: center;
display: -webkit-inline-flex; display: -webkit-inline-flex;
font-family: monospace;
overflow: hidden; overflow: hidden;
padding: 0;
-webkit-padding-start: 1px; -webkit-padding-start: 1px;
} }
input::-webkit-datetime-edit { input::-webkit-datetime-edit {
-webkit-flex: 1; flex: 1;
-webkit-user-modify: read-only !important; -webkit-user-modify: read-only !important;
user-modify: read-only !important;
display: inline-block; display: inline-block;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
padding: 4px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
/*
input::-webkit-datetime-edit-fields-wrapper {
-webkit-user-modify: read-only !important;
display: inline-block;
padding: 1px 0;
white-space: pre;
}
*/
/*
input[type="date"] {
background-color: red;
outline: none;
}
input[type="date"]::-webkit-clear-button {
font-size: 18px;
height: 30px;
position: relative;
}
input[type="date"]::-webkit-inner-spin-button {
height: 28px;
}
input[type="date"]::-webkit-calendar-picker-indicator {
font-size: 15px;
} */
input[type="file"] { input[type="file"] {
opacity: 0; opacity: 0;
display: none; display: none;
@ -1308,7 +1303,6 @@ input:required:valid {
background-color: var(--sdpi-background); background-color: var(--sdpi-background);
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@ -1330,23 +1324,72 @@ a {
color: #7397d2; color: #7397d2;
} }
.testcontainer { input[type="week"] {
display: flex; -webkit-appearance: auto !important;
background-color: #0000ff20; appearance: auto !important;
max-width: 400px;
height: 200px;
align-content: space-evenly;
} }
input[type=range] { input[type="month"] + datalist,
-webkit-appearance: none; input[type="day"] + datalist,
/* background-color: green; */ input[type="week"] + datalist,
input[type=text] + datalist {
display: none !important;
}
input[type="range"] {
-webkit-appearance: auto;
appearance: auto;
height: 6px; height: 6px;
margin-top: 12px; margin-top: 12px;
z-index: 0; z-index: 0;
overflow: visible; overflow: visible;
} }
input[type="range"]::-webkit-slider-runnable-track {
border: 0px solid transparent;
}
.sdpi-item[type="range"] .sdpi-item-value.datalist {
flex-direction: column;
}
datalist {
--sdpi-datalist-margin: 7px;
display: flex;
justify-content: space-between;
margin-top: 0px;
padding-top: 0px;
font-size: 12px;
margin-left: var(--sdpi-datalist-margin);
width: calc(100% - calc(var(--sdpi-datalist-margin) * 2.5));
}
datalist > option {
display: flex;
justify-content: center;
align-items: end;
/* background-image: url(../assets/tick.svg); */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='8' viewBox='0 0 1 8'%3E%3Crect width='1' height='6' x='0' y='1' fill='%23555'/%3E%3C/svg%3E%0A");
padding: 0;
font-weight: 400;
font-size: 12px;
color: #9A9A99;
width: 1px;
height: 30px;
z-index: 1;
margin-top: -6px;
background-position: top 6px right 5px;
background-repeat: repeat no-repeat; /* fallback */
background-repeat-y: no-repeat;
user-select: none;
-webkit-user-select: none;
}
[role="spinbutton"] {
-webkit-appearance: auto;
appearance: auto;
}
/* /*
input[type="range"]::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
@ -1595,7 +1638,8 @@ input[type="range"].colortemperature::-webkit-slider-runnable-track {
background-image: linear-gradient(to right, #94d0ec, #ffb165); background-image: linear-gradient(to right, #94d0ec, #ffb165);
} }
input[type="range"].colorbrightness::-webkit-slider-runnable-track { input[type="range"].colorbrightness.greyscale::-webkit-slider-runnable-track,
input[type="range"].colorbrightness.grayscale::-webkit-slider-runnable-track {
background-color: #efefef; background-color: #efefef;
background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0)); background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0));
} }
@ -1648,3 +1692,54 @@ select {
-webkit-appearance: media-slider; -webkit-appearance: media-slider;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/*--------- context menu ----------*/
.context-menu {
display: none;
position: absolute;
z-index: 10;
padding: 12px 0;
width: 120px;
background-color: #3D3D3D;
border: solid 1px #dfdfdf;
box-shadow: 1px 1px 2px #cfcfcf;
}
.context-menu--active {
display: block;
}
.context-menu__items {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
}
.context-menu__item {
display: block;
margin-bottom: 4px;
background-color: #3D3D3D !important;
}
.context-menu__item:last-child {
margin-bottom: 0;
}
.context-menu__link {
display: block;
padding: 4px 12px;
color: #ffff;
text-decoration: none;
white-space: nowrap;
}
.context-menu__link:hover {
color: #fff;
background-color: #0066aa;
}
.context-menu_message {
cursor: default;
}

View File

@ -29,6 +29,7 @@ function calcRangeLabel(elem) {
return tooltipValue + outputType; return tooltipValue + outputType;
} }
/*
function setElementLabel(elem, str) { function setElementLabel(elem, str) {
// Try to set this for the rangeLabel class, if it exists // Try to set this for the rangeLabel class, if it exists
let label = elem.querySelector('.rangeLabel'); let label = elem.querySelector('.rangeLabel');
@ -39,6 +40,7 @@ function setElementLabel(elem, str) {
console.log('setElementLabel ERROR! No .rangeLabel found', elem); console.log('setElementLabel ERROR! No .rangeLabel found', elem);
} }
} }
*/
function setRangeTooltips() { function setRangeTooltips() {
console.log("Loading setRangeTooltips"); console.log("Loading setRangeTooltips");
@ -68,7 +70,7 @@ function setRangeTooltips() {
tooltip.style.top = (rangeRect.top - 32) + 'px'; tooltip.style.top = (rangeRect.top - 32) + 'px';
} }
setElementLabel(elem, labelStr) //setElementLabel(elem, labelStr)
}; };
rangeSelector.addEventListener( rangeSelector.addEventListener(
@ -100,7 +102,7 @@ function setRangeTooltips() {
console.log('rangeTooltip settingsUpdated called'); console.log('rangeTooltip settingsUpdated called');
window.setTimeout(function () { window.setTimeout(function () {
let str = calcRangeLabel(rangeSelector); let str = calcRangeLabel(rangeSelector);
setElementLabel(elem, str); //setElementLabel(elem, str);
}, 500); }, 500);
}, },
false false
@ -112,7 +114,7 @@ function setRangeTooltips() {
console.log('rangeTooltip websocketCreate called'); console.log('rangeTooltip websocketCreate called');
window.setTimeout(function () { window.setTimeout(function () {
let str = calcRangeLabel(rangeSelector); let str = calcRangeLabel(rangeSelector);
setElementLabel(elem, str); //setElementLabel(elem, str);
}, 500); }, 500);
}, },
false false

View File

@ -7,6 +7,8 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace FocusVolumeControl.UI namespace FocusVolumeControl.UI
{ {
internal class JavaIconExtractor internal class JavaIconExtractor
@ -94,3 +96,5 @@ namespace FocusVolumeControl.UI
} }
} }
#nullable restore

View File

@ -8,6 +8,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Threading; using System.Windows.Threading;
#nullable enable
namespace FocusVolumeControl namespace FocusVolumeControl
{ {
internal class WindowChangedEventLoop internal class WindowChangedEventLoop
@ -16,10 +18,10 @@ namespace FocusVolumeControl
public static WindowChangedEventLoop Instance => _lazy.Value; public static WindowChangedEventLoop Instance => _lazy.Value;
readonly Thread _thread; readonly Thread _thread;
Dispatcher _dispatcher; Dispatcher? _dispatcher;
IntPtr _foregroundWindowChangedEvent; IntPtr _foregroundWindowChangedEvent;
Native.WinEventDelegate _delegate; Native.WinEventDelegate? _delegate;
private WindowChangedEventLoop() private WindowChangedEventLoop()
{ {
@ -37,7 +39,7 @@ namespace FocusVolumeControl
_thread.Start(); _thread.Start();
} }
public event Action WindowChanged; public event Action? WindowChanged;
CancellationTokenSource? _cancellationTokenSource = null; CancellationTokenSource? _cancellationTokenSource = null;
@ -64,3 +66,5 @@ namespace FocusVolumeControl
} }
} }
} }
#nullable restore

View File

@ -33,7 +33,7 @@
"Name": "Focused Application Volume", "Name": "Focused Application Volume",
"Description": "Control the volume of the focused application", "Description": "Control the volume of the focused application",
"URL": "https://github.com/dlprows/FocusVolumeControl", "URL": "https://github.com/dlprows/FocusVolumeControl",
"Version": "1.2.0", "Version": "1.3.1",
"CodePath": "FocusVolumeControl", "CodePath": "FocusVolumeControl",
"Category": "Volume Control [dlprows]", "Category": "Volume Control [dlprows]",
"Icon": "Images/pluginIcon", "Icon": "Images/pluginIcon",

View File

@ -12,9 +12,9 @@
<RowDefinition Height="auto"/> <RowDefinition Height="auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button x:Name="_pauseButton" Grid.Row="0" Click="PauseClicked"></Button>
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock> <TextBox x:Name="_tf" Grid.Row="1" IsReadOnly="True">current</TextBox>
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>

View File

@ -1,10 +1,13 @@
using FocusVolumeControl; using FocusVolumeControl;
using FocusVolumeControl.AudioHelpers; using FocusVolumeControl.AudioHelpers;
using FocusVolumeControl.AudioSessions; using FocusVolumeControl.AudioSessions;
using NHotkey;
using NHotkey.Wpf;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Input;
namespace SoundBrowser; namespace SoundBrowser;
@ -16,6 +19,8 @@ public partial class MainWindow : Window
AudioHelper _audioHelper; AudioHelper _audioHelper;
Native.WinEventDelegate _delegate; Native.WinEventDelegate _delegate;
bool _paused = false;
bool _doOnce = false;
public MainWindow() public MainWindow()
{ {
@ -25,20 +30,93 @@ public partial class MainWindow : Window
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected //normally you can just pass a lambda, but for some reason, that seems to get garbage collected
_delegate = new Native.WinEventDelegate(WinEventProc); _delegate = new Native.WinEventDelegate(WinEventProc);
Native.RegisterForForegroundWindowChangedEvent(_delegate); Native.RegisterForForegroundWindowChangedEvent(_delegate);
HotkeyManager.Current.AddOrReplace("Pause", Key.P, ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift, OnPauseShortcut);
}
private void OnPauseShortcut(object? sender, HotkeyEventArgs e)
{
if( _paused )
{
_paused = false;
_pauseButton.Content = "Running - click to pause on next app";
}
else
{
_paused = true;
_doOnce = true;
_pauseButton.Content = "Paused - click to resume";
_handle = Native.GetForegroundWindow();
DoThing();
}
}
private void PauseClicked(object sender, RoutedEventArgs e)
{
_paused = !_paused;
if(_paused)
{
_pauseButton.Content = "Pausing on next app";
_doOnce = true;
}
else
{
_pauseButton.Content = "Running - click to pause on next app";
}
} }
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
SetupCurrentAppFields(); _handle = Native.GetForegroundWindow();
SetupAllSessionFields(); DoThing();
} }
private void SetupCurrentAppFields() private void DoThing()
{ {
var handle = Native.GetForegroundWindow(); if(_paused)
var sb = new StringBuilder(); {
if(!_doOnce)
{
return;
}
_doOnce = false;
_pauseButton.Content = "Paused - click to resume";
}
else
{
_pauseButton.Content = "Running - click to pause on next app";
}
if (handle != IntPtr.Zero) var sb = new StringBuilder();
SetupCurrentAppFields(sb);
sb.AppendLine("");
sb.AppendLine("-------------------------------------------------------------------------------");
sb.AppendLine("");
DetermineDefaultDevice(sb);
sb.AppendLine("");
SetupAllSessionFields(sb);
_tf.Text = sb.ToString();
Trace.WriteLine(sb.ToString());
}
private void DetermineDefaultDevice(StringBuilder sb)
{
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia, out var device);
var name = GetDeviceName(device);
sb.AppendLine($"Default Audio Device: {name}");
}
IntPtr _handle;
private void SetupCurrentAppFields(StringBuilder sb)
{
if (_handle != IntPtr.Zero)
{ {
//use this in debug to help there be less events //use this in debug to help there be less events
@ -52,14 +130,15 @@ public partial class MainWindow : Window
} }
*/ */
var processes = _audioHelper.GetPossibleProcesses(); var processes = _audioHelper.GetPossibleProcesses(_handle);
var session = _audioHelper.FindSession(processes); var session = _audioHelper.FindSession(processes);
sb.AppendLine("Possible Current Processes");
foreach (var p in processes) foreach (var p in processes)
{ {
var displayName = (new NameAndIconHelper()).GetProcessInfo(p); var displayName = (new NameAndIconHelper()).GetProcessInfo(p);
sb.AppendLine($"pid: {p.Id}"); sb.AppendLine($"\tpid: {p.Id}");
sb.AppendLine($"\tprocessName: {p.ProcessName}"); sb.AppendLine($"\tprocessName: {p.ProcessName}");
sb.AppendLine($"\tDisplayName: {displayName}"); sb.AppendLine($"\tDisplayName: {displayName}");
@ -69,6 +148,7 @@ public partial class MainWindow : Window
if (session != null) if (session != null)
{ {
sb.AppendLine("picked the following best match"); sb.AppendLine("picked the following best match");
sb.AppendLine($"\tpid: {string.Join(", ", session.Pids)}");
sb.AppendLine($"\tsession: {session.DisplayName}"); sb.AppendLine($"\tsession: {session.DisplayName}");
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}"); sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
} }
@ -78,15 +158,10 @@ public partial class MainWindow : Window
} }
} }
_tf.Text = sb.ToString();
} }
private void SetupAllSessionFields() private void SetupAllSessionFields(StringBuilder sb)
{ {
_tf2.Text = "";
var sb = new StringBuilder();
sb.AppendLine("-------------------------------------------------------------------------------");
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator(); var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection); deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
@ -97,7 +172,8 @@ public partial class MainWindow : Window
{ {
deviceCollection.Item(i, out var device); deviceCollection.Item(i, out var device);
//todo: put the device name in the output //todo: put the device name in the output
sb.AppendLine($"----"); var name = GetDeviceName(device);
sb.AppendLine($"----{name}----");
Guid iid = typeof(IAudioSessionManager2).GUID; Guid iid = typeof(IAudioSessionManager2).GUID;
@ -116,13 +192,36 @@ public partial class MainWindow : Window
session.GetProcessId(out var processId); session.GetProcessId(out var processId);
session.GetIconPath(out var path); session.GetIconPath(out var path);
var audioProcess = Process.GetProcessById(processId); var audioProcess = Process.GetProcessById(processId);
if(audioProcess.Id == 0)
{
continue;
}
var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess); var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess);
sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}"); sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}");
} }
_tf2.Text = sb.ToString(); sb.Append("\n");
} }
} }
private string GetDeviceName(IMMDevice device)
{
var fnkey = PKey.DeviceFriendlyName;
device.OpenPropertyStore(EStgmAccess.STGM_READ, out var propertyStore);
propertyStore.GetCount(out var count);
for(int i = 0; i < count; i++)
{
propertyStore.GetAt(i, out var pkey);
if(pkey.fmtId == fnkey.fmtId && pkey.PId == fnkey.PId)
{
propertyStore.GetValue(ref pkey, out var pvalue);
return (string)pvalue.Value;
}
}
return "";
}
} }

View File

@ -7,6 +7,10 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" /> <ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
</ItemGroup> </ItemGroup>