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

@@ -0,0 +1,226 @@
package com.ts3client.ui;
import com.ts3client.config.Settings;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
/**
* The main window's toolbar: connect/disconnect, the per-tab mic/speaker/away
* controls, and the Master Volume slider on the right. A right-click anywhere on
* the bar opens a customization menu for the window's optional chrome (this
* toolbar has no say over the status bar's own visibility beyond reporting the
* toggle).
*
* <p>Talks to {@link MainFrame} only through {@link Listener}, so it owns no
* connection/tab state itself &mdash; {@link #refresh} is handed everything it
* needs to redraw each time the selection changes.
*/
final class MainToolbar extends JToolBar {
interface Listener {
void onConnect();
void onDisconnect();
/** Moves the microphone to the tab on screen. */
void onActivate();
void onMicMuteToggle(boolean muted);
void onDeafenToggle(boolean deafened);
/** The plain toggle carries no message; the drop-down arrow is where messages are chosen. */
void onAwayToggle(boolean away);
JPopupMenu buildAwayMenu();
void onSettings();
/** The slider changed; push the new master volume to every open connection. */
void onMasterVolumeChanged();
void onStatusBarVisibilityChanged(boolean visible);
}
private final Settings settings;
private final Listener listener;
private final JButton connectButton;
private final JButton disconnectButton;
private final JToggleButton activeButton;
private final JToggleButton micButton;
private final JToggleButton speakerButton;
private final DropDownToggleButton awayButton;
private final JComponent masterVolumePanel;
MainToolbar(Settings settings, Listener listener) {
this.settings = settings;
this.listener = listener;
setFloatable(false);
setBackground(Theme.TOOLBAR_BG);
setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
setComponentPopupMenu(buildContextMenu());
connectButton = new JButton(Icons.connect());
connectButton.setToolTipText("Connect to a server");
connectButton.addActionListener(e -> listener.onConnect());
disconnectButton = new JButton(Icons.disconnect());
disconnectButton.setToolTipText("Disconnect");
disconnectButton.addActionListener(e -> listener.onDisconnect());
activeButton = new JToggleButton(Icons.micActive());
activeButton.setToolTipText("Speak on this server (moves the microphone to this tab)");
activeButton.addActionListener(e -> listener.onActivate());
micButton = new JToggleButton(Icons.mic());
micButton.setToolTipText("Mute / unmute microphone on this server");
micButton.addActionListener(e -> listener.onMicMuteToggle(micButton.isSelected()));
speakerButton = new JToggleButton(Icons.speaker());
speakerButton.setToolTipText("Deafen / undeafen (mute speakers) on this server");
speakerButton.addActionListener(e -> listener.onDeafenToggle(speakerButton.isSelected()));
awayButton = new DropDownToggleButton(Icons.away(),
"Away on this server (the arrow offers the global actions and presets)",
listener::buildAwayMenu);
awayButton.addActionListener(e -> listener.onAwayToggle(awayButton.isSelected()));
JButton settingsButton = new JButton(Icons.settings());
settingsButton.setToolTipText("Options");
settingsButton.addActionListener(e -> listener.onSettings());
add(connectButton);
add(disconnectButton);
addSeparator();
add(activeButton);
add(micButton);
add(speakerButton);
add(awayButton);
addSeparator();
add(settingsButton);
add(Box.createHorizontalGlue());
masterVolumePanel = buildMasterVolumeControl();
masterVolumePanel.setVisible(settings.showMasterVolumeSlider);
add(masterVolumePanel);
}
/** Reflects the current tab's state on the buttons. */
void refresh(boolean connected, boolean anyConnected, boolean micMuted, boolean deafened,
boolean active, boolean away) {
disconnectButton.setEnabled(connected);
micButton.setEnabled(connected);
speakerButton.setEnabled(connected);
activeButton.setEnabled(connected);
// The away menu's actions are global, so the arrow stays live while any server is up.
awayButton.setEnabled(anyConnected);
awayButton.setToggleEnabled(connected);
micButton.setSelected(micMuted);
micButton.setIcon(micMuted ? Icons.micMutedLarge() : Icons.mic());
speakerButton.setSelected(deafened);
speakerButton.setIcon(deafened ? Icons.speakerMutedLarge() : Icons.speaker());
activeButton.setSelected(active);
awayButton.setSelected(away);
}
/** Simulates a click, for the "Toggle microphone" menu item. */
void clickMic() {
micButton.doClick();
}
/** Simulates a click, for the "Toggle speakers" menu item. */
void clickSpeaker() {
speakerButton.doClick();
}
/** The Master Volume slider: multiplies both voice and notification volume. */
private JComponent buildMasterVolumeControl() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
panel.setOpaque(false);
panel.setAlignmentY(Component.CENTER_ALIGNMENT);
JLabel icon = new JLabel(Icons.speaker());
icon.setToolTipText("Master volume (voice + notifications)");
icon.setAlignmentY(Component.CENTER_ALIGNMENT);
JSlider slider = new JSlider(0, 200, (int) Math.round(settings.masterVolume * 100));
slider.setOpaque(false);
slider.setToolTipText("Master volume (voice + notifications) — scroll to adjust");
slider.setAlignmentY(Component.CENTER_ALIGNMENT);
slider.setPreferredSize(new Dimension(135, slider.getPreferredSize().height));
slider.addChangeListener(e -> {
settings.masterVolume = slider.getValue() / 100.0;
listener.onMasterVolumeChanged();
// Only touch the disk once the drag (or a wheel step) has settled.
if (!slider.getValueIsAdjusting()) settings.save();
});
slider.addMouseWheelListener(e -> {
int step = e.getWheelRotation() < 0 ? 5 : -5;
slider.setValue(slider.getValue() + step);
e.consume();
});
panel.add(icon);
panel.add(slider);
return panel;
}
/** Right-click on the toolbar: toggles for the window's optional chrome. */
private JPopupMenu buildContextMenu() {
JPopupMenu menu = new JPopupMenu();
JCheckBoxMenuItem statusBarItem = new JCheckBoxMenuItem("Show Status Bar", settings.showStatusBar);
statusBarItem.addActionListener(e -> {
settings.showStatusBar = statusBarItem.isSelected();
settings.save();
listener.onStatusBarVisibilityChanged(settings.showStatusBar);
});
JCheckBoxMenuItem volumeItem = new JCheckBoxMenuItem("Show Master Volume Slider", settings.showMasterVolumeSlider);
volumeItem.addActionListener(e -> {
settings.showMasterVolumeSlider = volumeItem.isSelected();
settings.save();
masterVolumePanel.setVisible(settings.showMasterVolumeSlider);
revalidate();
repaint();
});
menu.add(statusBarItem);
menu.add(volumeItem);
// Either toggle can also be flipped elsewhere (or via settings.properties), so
// re-sync the ticks each time the menu is about to be shown.
menu.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
statusBarItem.setSelected(settings.showStatusBar);
volumeItem.setSelected(settings.showMasterVolumeSlider);
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
return menu;
}
}