package com.ts3client.ui; import com.formdev.flatlaf.util.ScaledEmptyBorder; import com.formdev.flatlaf.util.UIScale; import com.ts3client.config.Settings; import com.ts3client.net.HostButton; import com.ts3client.net.PictureDownloads; 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 and the server's host button 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). * *
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; private final HostButtonView hostButton; MainToolbar(Settings settings, Listener listener, PictureDownloads pictures) { this.settings = settings; this.listener = listener; setFloatable(false); setBackground(Theme.toolbarBg()); setBorder(new ScaledEmptyBorder(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); hostButton = new HostButtonView(pictures); add(hostButton); } /** Shows the selected server's host button; {@code null} hides it. */ void showHostButton(HostButton button) { hostButton.show(button); } /** 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, UIScale.scale(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(UIScale.scale(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; } }