diff --git a/EarTrumpet/App.xaml.cs b/EarTrumpet/App.xaml.cs
index 5df83cfa5..05cd2d5c5 100644
--- a/EarTrumpet/App.xaml.cs
+++ b/EarTrumpet/App.xaml.cs
@@ -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; }
@@ -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();
diff --git a/EarTrumpet/AppSettings.cs b/EarTrumpet/AppSettings.cs
index 8ba84d363..6464d171b 100644
--- a/EarTrumpet/AppSettings.cs
+++ b/EarTrumpet/AppSettings.cs
@@ -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");
diff --git a/EarTrumpet/EarTrumpet.csproj b/EarTrumpet/EarTrumpet.csproj
index b4666b20c..bcdb883b8 100644
--- a/EarTrumpet/EarTrumpet.csproj
+++ b/EarTrumpet/EarTrumpet.csproj
@@ -64,12 +64,17 @@
4.0
+
+ $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll
+
False
$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\Windows.winmd
$(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\10.0.16299.0\Windows.winmd
+
+
@@ -189,6 +194,7 @@
+
diff --git a/EarTrumpet/Extensions/TaskbarMiddleClickMuteService.cs b/EarTrumpet/Extensions/TaskbarMiddleClickMuteService.cs
new file mode 100644
index 000000000..309c60690
--- /dev/null
+++ b/EarTrumpet/Extensions/TaskbarMiddleClickMuteService.cs
@@ -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);
+ }
+ }
+}
diff --git a/EarTrumpet/Interop/Helpers/MouseHook.cs b/EarTrumpet/Interop/Helpers/MouseHook.cs
index 72bdfa51e..c78dd5412 100644
--- a/EarTrumpet/Interop/Helpers/MouseHook.cs
+++ b/EarTrumpet/Interop/Helpers/MouseHook.cs
@@ -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;
@@ -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);
}
}
}
diff --git a/EarTrumpet/Properties/Resources.Designer.cs b/EarTrumpet/Properties/Resources.Designer.cs
index c8b85eec0..876db1d8c 100644
--- a/EarTrumpet/Properties/Resources.Designer.cs
+++ b/EarTrumpet/Properties/Resources.Designer.cs
@@ -647,6 +647,15 @@ public static string EventTrigger_AddText {
}
}
+ ///
+ /// Looks up a localized string similar to Middle-click on a taskbar app icon to toggle mute.
+ ///
+ public static string SettingsUseTaskbarMiddleClickMute {
+ get {
+ return ResourceManager.GetString("SettingsUseTaskbarMiddleClickMute", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to EarTrumpet {Option}.
///
diff --git a/EarTrumpet/Properties/Resources.resx b/EarTrumpet/Properties/Resources.resx
index 109324f4e..5f7b4c44b 100644
--- a/EarTrumpet/Properties/Resources.resx
+++ b/EarTrumpet/Properties/Resources.resx
@@ -665,4 +665,7 @@ Open [https://eartrumpet.app/jmp/fixfonts] now?
Use logarithmic volume scale
+
+ Middle-click on a taskbar app icon to toggle mute
+
\ No newline at end of file
diff --git a/EarTrumpet/UI/ViewModels/EarTrumpetMouseSettingsPageViewModel.cs b/EarTrumpet/UI/ViewModels/EarTrumpetMouseSettingsPageViewModel.cs
index c76edcf27..86f07def6 100644
--- a/EarTrumpet/UI/ViewModels/EarTrumpetMouseSettingsPageViewModel.cs
+++ b/EarTrumpet/UI/ViewModels/EarTrumpetMouseSettingsPageViewModel.cs
@@ -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)
diff --git a/EarTrumpet/UI/Views/SettingsWindow.xaml b/EarTrumpet/UI/Views/SettingsWindow.xaml
index 3ac1c5c1a..0f0959b6c 100644
--- a/EarTrumpet/UI/Views/SettingsWindow.xaml
+++ b/EarTrumpet/UI/Views/SettingsWindow.xaml
@@ -169,6 +169,9 @@
+