Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions EarTrumpet/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public partial class App
private WindowHolder _mixerWindow;
private WindowHolder _settingsWindow;
private ErrorReporter _errorReporter;
private TaskbarMiddleClickMuteService _taskbarMiddleClickMuteService;

public static AppSettings Settings { get; private set; }

Expand Down Expand Up @@ -104,6 +105,9 @@ private void CompleteStartup()
_mixerWindow = new WindowHolder(CreateMixerExperience);
_settingsWindow = new WindowHolder(CreateSettingsExperience);

_taskbarMiddleClickMuteService = new TaskbarMiddleClickMuteService(CollectionViewModel, Settings);
Exit += (_, __) => _taskbarMiddleClickMuteService?.Dispose();

Settings.FlyoutHotkeyTyped += () => _flyoutViewModel.OpenFlyout(InputType.Keyboard);
Settings.MixerHotkeyTyped += () => _mixerWindow.OpenOrClose();
Settings.SettingsHotkeyTyped += () => _settingsWindow.OpenOrBringToFront();
Expand Down
6 changes: 6 additions & 0 deletions EarTrumpet/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ public bool UseGlobalMouseWheelHook
set => _settings.Set("UseGlobalMouseWheelHook", value);
}

public bool UseTaskbarMiddleClickMute
{
get => _settings.Get("UseTaskbarMiddleClickMute", false);
set => _settings.Set("UseTaskbarMiddleClickMute", value);
}

public bool HasShownFirstRun
{
get => _settings.HasKey("hasShownFirstRun");
Expand Down
6 changes: 6 additions & 0 deletions EarTrumpet/EarTrumpet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,17 @@
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="System.Runtime.WindowsRuntime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath Condition="Exists('$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll')">$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll</HintPath>
</Reference>
<Reference Include="Windows, Version=255.255.255.255, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\Windows.winmd')">$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\Windows.winmd</HintPath>
<HintPath Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\10.0.16299.0\Windows.winmd')">$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\10.0.16299.0\Windows.winmd</HintPath>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="UIAutomationClient" />
<Reference Include="UIAutomationTypes" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="XamlAnimatedGif, Version=2.0.0.0, Culture=neutral, PublicKeyToken=20a987d8023d9690, processorArchitecture=MSIL">
Expand Down Expand Up @@ -189,6 +194,7 @@
<Compile Include="Extensions\EventBinding\HandledEventBindingExtension.cs" />
<Compile Include="Extensions\FrameworkElementExtensions.cs" />
<Compile Include="Extensions\ListExtensions.cs" />
<Compile Include="Extensions\TaskbarMiddleClickMuteService.cs" />
<Compile Include="Features.cs" />
<Compile Include="Interop\Helpers\AudioPolicyConfigFactoryImplFor21H2.cs" />
<Compile Include="Interop\Helpers\AudioPolicyConfigFactoryImplForDownlevel.cs" />
Expand Down
213 changes: 213 additions & 0 deletions EarTrumpet/Extensions/TaskbarMiddleClickMuteService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using EarTrumpet.Interop.Helpers;
using EarTrumpet.UI.ViewModels;
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Automation;
using System.Windows.Forms;

namespace EarTrumpet.Extensions
{
public class TaskbarMiddleClickMuteService : IDisposable
{
private readonly MouseHook _mouseHook;
private readonly DeviceCollectionViewModel _collectionViewModel;
private readonly AppSettings _settings;
private bool _disposed = false;

public TaskbarMiddleClickMuteService(DeviceCollectionViewModel collectionViewModel, AppSettings settings)
{
_collectionViewModel = collectionViewModel;
_settings = settings;
_mouseHook = new MouseHook();
_mouseHook.MiddleClickEvent += OnMiddleClick;
_mouseHook.SetHook();
}

private int OnMiddleClick(object sender, MouseEventArgs e)
{
if (!_settings.UseTaskbarMiddleClickMute)
{
return 0;
}

try
{
if (!IsClickOnTaskbar(e.X, e.Y))
{
return 0;
}

System.Threading.Tasks.Task.Run(() =>
{
try
{
string appName = GetTaskbarButtonAppName(e.X, e.Y);
if (!string.IsNullOrEmpty(appName))
{
ToggleMuteForApp(appName);
}
}
catch (Exception ex)
{
Trace.WriteLine($"TaskbarMiddleClickMuteService error: {ex.Message}");
}
});

return 1;
}
catch (Exception ex)
{
Trace.WriteLine($"TaskbarMiddleClickMuteService OnMiddleClick error: {ex.Message}");
}

return 0;
}

private bool IsClickOnTaskbar(int x, int y)
{
try
{
var taskbarState = WindowsTaskbar.Current;
var point = new System.Drawing.Point(x, y);

var rect = taskbarState.Size;
var bounds = new System.Drawing.Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);

if (bounds.Contains(point))
{
return true;
}
}
catch { }
return false;
}

private string GetTaskbarButtonAppName(int x, int y)
{
try
{
var point = new System.Windows.Point(x, y);
AutomationElement element = AutomationElement.FromPoint(point);

if (element == null)
return null;

AutomationElement current = element;
int maxDepth = 10;
int depth = 0;

while (current != null && depth < maxDepth)
{
string name = current.Current.Name;
string className = current.Current.ClassName;
var controlType = current.Current.ControlType;

if (!string.IsNullOrEmpty(name) &&
(className == "Taskbar.TaskListButtonAutomationPeer" ||
className.Contains("TaskListButton") ||
controlType == ControlType.Button ||
controlType == ControlType.ListItem ||
controlType == ControlType.MenuItem))
{
string cleanName = CleanAppName(name);
if (!string.IsNullOrEmpty(cleanName))
{
return cleanName;
}
}

try
{
TreeWalker walker = TreeWalker.ControlViewWalker;
current = walker.GetParent(current);
depth++;
}
catch
{
break;
}
}
}
catch (Exception ex)
{
Trace.WriteLine($"TaskbarMiddleClickMuteService GetTaskbarButtonAppName error: {ex.Message}");
}

return null;
}

private string CleanAppName(string name)
{
if (string.IsNullOrEmpty(name))
return null;

string cleanName = name;

cleanName = System.Text.RegularExpressions.Regex.Replace(cleanName, @"\s*-\s*\d+\s*.*$", "");

int dashIndex = cleanName.IndexOf(" - ");
if (dashIndex > 0)
{
cleanName = cleanName.Substring(0, dashIndex);
}

cleanName = System.Text.RegularExpressions.Regex.Replace(cleanName, @"\s*\(\d+\)\s*$", "");

return cleanName.Trim();
}

private bool ToggleMuteForApp(string appName)
{
if (string.IsNullOrEmpty(appName))
return false;

string lowerAppName = appName.ToLowerInvariant();

foreach (var device in _collectionViewModel.AllDevices)
{
foreach (var app in device.Apps)
{
string displayName = app.DisplayName?.ToLowerInvariant() ?? "";
string exeName = app.ExeName?.ToLowerInvariant() ?? "";

if (displayName.Contains(lowerAppName) ||
lowerAppName.Contains(displayName) ||
exeName.Contains(lowerAppName) ||
lowerAppName.Contains(exeName.Replace(".exe", "")))
{
app.IsMuted = !app.IsMuted;
Trace.WriteLine($"TaskbarMiddleClickMuteService toggled mute for {app.DisplayName}");
return true;
}
}
}

return false;
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_mouseHook.MiddleClickEvent -= OnMiddleClick;
_mouseHook.UnHook();
}
_disposed = true;
}
}

~TaskbarMiddleClickMuteService()
{
Dispose(false);
}
}
}
34 changes: 28 additions & 6 deletions EarTrumpet/Interop/Helpers/MouseHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ internal struct MouseLLHookStruct
public delegate int MouseWheelHandler(object sender, MouseEventArgs e);
public event MouseWheelHandler MouseWheelEvent;

public delegate int MiddleClickHandler(object sender, MouseEventArgs e);
public event MiddleClickHandler MiddleClickEvent;

private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_MBUTTONUP = 0x0208;
private const int WH_MOUSE_LL = 14;
private User32.HookProc _hProc;
private int _hHook;
Expand All @@ -53,17 +58,34 @@ public void UnHook()

private int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode < 0 || MouseWheelEvent == null || (Int32)wParam != WM_MOUSEWHEEL)
if (nCode < 0)
{
return User32.CallNextHookEx(_hHook, nCode, wParam, lParam);
}
MouseLLHookStruct MyMouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
int result = MouseWheelEvent(this, new MouseEventArgs(MouseButtons.None, 0, MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y, MyMouseHookStruct.mouseData >> 16));
if (result == 0)

int msgType = (Int32)wParam;

if (msgType == WM_MOUSEWHEEL && MouseWheelEvent != null)
{
return User32.CallNextHookEx(_hHook, nCode, wParam, lParam);
MouseLLHookStruct MyMouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
int result = MouseWheelEvent(this, new MouseEventArgs(MouseButtons.None, 0, MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y, MyMouseHookStruct.mouseData >> 16));
if (result != 0)
{
return result;
}
}
return result;

if (msgType == WM_MBUTTONDOWN && MiddleClickEvent != null)
{
MouseLLHookStruct MyMouseHookStruct = (MouseLLHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLLHookStruct));
int result = MiddleClickEvent(this, new MouseEventArgs(MouseButtons.Middle, 1, MyMouseHookStruct.pt.x, MyMouseHookStruct.pt.y, 0));
if (result != 0)
{
return result;
}
}

return User32.CallNextHookEx(_hHook, nCode, wParam, lParam);
}
}
}
9 changes: 9 additions & 0 deletions EarTrumpet/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions EarTrumpet/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,7 @@ Open [https://eartrumpet.app/jmp/fixfonts] now?</value>
<data name="SettingsUseLogarithmicVolume" xml:space="preserve">
<value>Use logarithmic volume scale</value>
</data>
<data name="SettingsUseTaskbarMiddleClickMute" xml:space="preserve">
<value>Middle-click on a taskbar app icon to toggle mute</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public bool UseGlobalMouseWheelHook
set => _settings.UseGlobalMouseWheelHook = value;
}

public bool UseTaskbarMiddleClickMute
{
get => _settings.UseTaskbarMiddleClickMute;
set => _settings.UseTaskbarMiddleClickMute = value;
}

private readonly AppSettings _settings;

public EarTrumpetMouseSettingsPageViewModel(AppSettings settings) : base(null)
Expand Down
3 changes: 3 additions & 0 deletions EarTrumpet/UI/Views/SettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@
<CheckBox HorizontalAlignment="Left"
Content="{x:Static resx:Resources.SettingsUseGlobalMouseWheelHook}"
IsChecked="{Binding UseGlobalMouseWheelHook, Mode=TwoWay}" />
<CheckBox HorizontalAlignment="Left"
Content="{x:Static resx:Resources.SettingsUseTaskbarMiddleClickMute}"
IsChecked="{Binding UseTaskbarMiddleClickMute, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:EarTrumpetCommunitySettingsPageViewModel}">
Expand Down