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 @@ +