Add Master Volume slider, status bar toggle, and toolbar customization menu

The toolbar now has a right-click menu to hide/show the status bar and
a short Master Volume slider (drag or scroll) that multiplies both
voice and notification volume via new Settings.effectiveOutputVolume()/
effectiveSoundVolume() helpers.

Also splits MainFrame's toolbar, menu bar and status bar construction
into their own MainToolbar/MainMenuBar/StatusBar classes, each talking
back to MainFrame only through a Listener interface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:37:36 +00:00
parent e5fb87a796
commit cf5ba94092
6 changed files with 601 additions and 200 deletions

View File

@@ -95,6 +95,12 @@ public final class Settings {
public boolean music = false;
/** Master playback gain, 0..1 (may exceed 1 for boost up to 2). */
public double outputVolume = 1.0;
/**
* Global multiplier on top of {@link #outputVolume} and {@link #soundVolume},
* controlled from the toolbar's Master Volume slider so both can be ridden
* with one control. 0..2, same boost headroom as the other volumes.
*/
public double masterVolume = 1.0;
/** Microphone input gain multiplier applied before VAD/encode. */
public double inputVolume = 1.0;
/** Remove steady background noise from the microphone (spectral denoise). */
@@ -119,6 +125,12 @@ public final class Settings {
/** Extra folder to look for icon packs in, on top of the well-known locations. */
public String iconPackDir = "";
// ---- window chrome ----
/** Whether the status bar at the bottom of the window is shown. */
public boolean showStatusBar = true;
/** Whether the Master Volume slider is shown on the toolbar. */
public boolean showMasterVolumeSlider = true;
/** Which actions make a sound, and which ones are important enough to survive muting. */
public final NotificationSettings notifications = new NotificationSettings();
@@ -155,6 +167,16 @@ public final class Settings {
return new File(identityFile);
}
/** {@link #outputVolume} scaled by the Master Volume slider. */
public double effectiveOutputVolume() {
return outputVolume * masterVolume;
}
/** {@link #soundVolume} scaled by the Master Volume slider. */
public double effectiveSoundVolume() {
return soundVolume * masterVolume;
}
/** The directory holding all persistent client state (settings, identities, caches). */
public static File configDir() {
return DIR;
@@ -185,6 +207,7 @@ public final class Settings {
packetLoss = parseI(props.getProperty("packetLoss"), packetLoss);
music = parseB(props.getProperty("music"), music);
outputVolume = parseD(props.getProperty("outputVolume"), outputVolume);
masterVolume = parseD(props.getProperty("masterVolume"), masterVolume);
inputVolume = parseD(props.getProperty("inputVolume"), inputVolume);
denoise = parseB(props.getProperty("denoise"), denoise);
denoiserLevel = parseD(props.getProperty("denoiserLevel"), denoiserLevel);
@@ -195,6 +218,8 @@ public final class Settings {
soundPackDir = props.getProperty("soundPackDir", soundPackDir);
iconPack = props.getProperty("iconPack", iconPack);
iconPackDir = props.getProperty("iconPackDir", iconPackDir);
showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar);
showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider);
notifications.load(props);
}
@@ -223,6 +248,7 @@ public final class Settings {
props.setProperty("packetLoss", Integer.toString(packetLoss));
props.setProperty("music", Boolean.toString(music));
props.setProperty("outputVolume", Double.toString(outputVolume));
props.setProperty("masterVolume", Double.toString(masterVolume));
props.setProperty("inputVolume", Double.toString(inputVolume));
props.setProperty("denoise", Boolean.toString(denoise));
props.setProperty("denoiserLevel", Double.toString(denoiserLevel));
@@ -233,6 +259,8 @@ public final class Settings {
props.setProperty("soundPackDir", soundPackDir);
props.setProperty("iconPack", iconPack);
props.setProperty("iconPackDir", iconPackDir);
props.setProperty("showStatusBar", Boolean.toString(showStatusBar));
props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider));
notifications.store(props);
}

View File

@@ -113,7 +113,7 @@ public final class SoundNotifier {
if (script == null) return;
String resolved = script.resolve(withDefaults(variables));
double volume = Math.max(0, Math.min(1.0, settings.soundVolume));
double volume = Math.max(0, Math.min(1.0, settings.effectiveSoundVolume()));
if (volume <= 0) return;
if (script.kind() == SoundScript.Kind.SAY) {