From cc6e187998dc1704b6636e9a4e2f57ae3be9c4c0 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Wed, 11 Mar 2026 01:11:17 +0000 Subject: [PATCH] bus mono now a ButtonMenu. this allows users to select between `mono off`, `mono on` and `stereo reverse`. This properly reflects the Voicemeter GUI. --- src/nvda_voicemeeter/builder.py | 15 +++++++++++---- src/nvda_voicemeeter/util.py | 4 ++++ src/nvda_voicemeeter/window.py | 27 ++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/nvda_voicemeeter/builder.py b/src/nvda_voicemeeter/builder.py index 65edbae..d1d865d 100644 --- a/src/nvda_voicemeeter/builder.py +++ b/src/nvda_voicemeeter/builder.py @@ -438,13 +438,20 @@ class Builder: def make_tab3_button_row(self, i) -> psg.Frame: """tab3 row represents bus composite toggle""" - def add_strip_outputs(layout): - params = ['MONO', 'EQ', 'MUTE'] + def add_bus_buttons(layout): + busmono = util.get_bus_mono() + params = ['EQ', 'MUTE'] if self.kind.name == 'basic': params.remove('EQ') busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)] layout.append( [ + psg.ButtonMenu( + 'Mono', + size=(6, 2), + menu_def=['', busmono], + key=f'BUS {i}||MONO', + ), *[ psg.Button( param.capitalize(), @@ -454,7 +461,7 @@ class Builder: for param in params ], psg.ButtonMenu( - 'BUSMODE', + 'Bus Mode', size=(12, 2), menu_def=['', busmodes], key=f'BUS {i}||MODE', @@ -463,7 +470,7 @@ class Builder: ) outputs = [] - [step(outputs) for step in (add_strip_outputs,)] + [step(outputs) for step in (add_bus_buttons,)] return psg.Frame( self.window.cache['labels'][f'BUS {i}||LABEL'], outputs, diff --git a/src/nvda_voicemeeter/util.py b/src/nvda_voicemeeter/util.py index a1c2dff..6bb46d4 100644 --- a/src/nvda_voicemeeter/util.py +++ b/src/nvda_voicemeeter/util.py @@ -155,6 +155,10 @@ def get_bus_modes(vm) -> list: ] +def get_bus_mono() -> list: + return ['off', 'on', 'stereo reverse'] + + def check_bounds(val, bounds: tuple) -> int | float: lower, upper = bounds if val > upper: diff --git a/src/nvda_voicemeeter/window.py b/src/nvda_voicemeeter/window.py index dcd5a6d..f61803b 100644 --- a/src/nvda_voicemeeter/window.py +++ b/src/nvda_voicemeeter/window.py @@ -58,6 +58,7 @@ class NVDAVMWindow(psg.Window): self[f'STRIP {i}||SLIDER LIMIT'].Widget.config(**slider_opts) for i in range(self.kind.num_bus): self[f'BUS {i}||SLIDER GAIN'].Widget.config(**slider_opts) + self[f'BUS {i}||MONO'].Widget.config(**buttonmenu_opts) self[f'BUS {i}||MODE'].Widget.config(**buttonmenu_opts) self.register_events() @@ -251,13 +252,16 @@ class NVDAVMWindow(psg.Window): self[f'STRIP {i}||SLIDER {param}'].bind('', '||KEY CTRL SHIFT R') # Bus Params - params = ['MONO', 'EQ', 'MUTE'] + params = ['EQ', 'MUTE'] if self.kind.name == 'basic': params.remove('EQ') for i in range(self.kind.num_bus): for param in params: self[f'BUS {i}||{param}'].bind('', '||FOCUS IN') self[f'BUS {i}||{param}'].bind('', '||KEY ENTER') + self[f'BUS {i}||MONO'].bind('', '||FOCUS IN') + self[f'BUS {i}||MONO'].bind('', '||KEY SPACE', propagate=False) + self[f'BUS {i}||MONO'].bind('', '||KEY ENTER') self[f'BUS {i}||MODE'].bind('', '||FOCUS IN') self[f'BUS {i}||MODE'].bind('', '||KEY SPACE', propagate=False) self[f'BUS {i}||MODE'].bind('', '||KEY ENTER', propagate=False) @@ -306,7 +310,7 @@ class NVDAVMWindow(psg.Window): mode = None continue - match parsed_cmd := self.parser.match.parseString(event): + match parsed_cmd := self.parser.match.parse_string(event): # Slider mode case [['ALT', 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' as direction], ['PRESS' | 'RELEASE' as e]]: if mode: @@ -972,7 +976,7 @@ class NVDAVMWindow(psg.Window): self.nvda.speak, 'on' if val else 'off', ) - case 'MONO' | 'MUTE': + case 'MUTE': val = not val setattr(self.vm.bus[int(index)], param.lower(), val) self.cache['bus'][event] = val @@ -981,6 +985,15 @@ class NVDAVMWindow(psg.Window): self.nvda.speak, 'on' if val else 'off', ) + case 'MONO': + chosen = values[event] + self.vm.bus[int(index)].mono = util.get_bus_mono().index(chosen) + self.cache['bus'][event] = chosen + self.TKroot.after( + 200, + self.nvda.speak, + f'mono {chosen}', + ) case 'MODE': chosen = util._bus_mode_map_reversed[values[event]] setattr(self.vm.bus[int(index)].mode, chosen, True) @@ -996,11 +1009,19 @@ class NVDAVMWindow(psg.Window): val = self.cache['bus'][f'BUS {index}||{param}'] if param == 'MODE': self.nvda.speak(f'{label} bus {param} {util._bus_mode_map[val]}') + elif param == 'MONO': + busmode = util.get_bus_mono()[val] + if busmode in ('on', 'off'): + self.nvda.speak(f'{label} {param} {busmode}') + else: + self.nvda.speak(f'{label} {busmode}') else: self.nvda.speak(f'{label} {param} {"on" if val else "off"}') case [['BUS', index], [param], ['KEY', 'SPACE' | 'ENTER']]: if param == 'MODE': util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MODE') + elif param == 'MONO': + util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MONO') else: self.find_element_with_focus().click()