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:
226
ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java
Normal file
226
ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java
Normal 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 — {@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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user