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:
@@ -12,22 +12,15 @@ import com.ts3client.sound.SoundNotifier;
|
||||
import com.ts3client.sound.SoundPlayer;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.JToolBar;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
@@ -65,25 +58,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
/** The tab that owns the capture device; null when nobody is capturing. */
|
||||
private ServerTab micTab;
|
||||
|
||||
private JMenu bookmarksMenu;
|
||||
private JCheckBoxMenuItem awayItem;
|
||||
private JMenuItem awayStatusItem;
|
||||
private JCheckBoxMenuItem commanderItem;
|
||||
private MainMenuBar menuBar;
|
||||
private javax.swing.Timer statusTimer;
|
||||
|
||||
private final JLabel statusLabel = new JLabel("Not connected");
|
||||
private final JLabel codecLabel = new JLabel();
|
||||
|
||||
/** Mirrors the active server's own client state next to the clock. */
|
||||
private TrayController tray;
|
||||
|
||||
private JToolBar toolbar;
|
||||
private JButton connectButton;
|
||||
private JButton disconnectButton;
|
||||
private JToggleButton activeButton;
|
||||
private JToggleButton micButton;
|
||||
private JToggleButton speakerButton;
|
||||
private DropDownToggleButton awayButton;
|
||||
private MainToolbar toolbar;
|
||||
private StatusBar statusBar;
|
||||
|
||||
private boolean pttPressed;
|
||||
|
||||
@@ -117,16 +99,19 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
setMinimumSize(new Dimension(720, 480));
|
||||
|
||||
tray = new TrayController(this, this::quit);
|
||||
setJMenuBar(buildMenuBar());
|
||||
menuBar = buildMenuBar();
|
||||
setJMenuBar(menuBar);
|
||||
|
||||
toolbar = buildToolbar();
|
||||
add(toolbar, BorderLayout.NORTH);
|
||||
// Switching the icon pack in the options dialog re-decorates the whole window.
|
||||
IconTheme.get().addListener(() -> SwingUtilities.invokeLater(this::rebuildIcons));
|
||||
add(tabPane, BorderLayout.CENTER);
|
||||
add(buildStatusBar(), BorderLayout.SOUTH);
|
||||
statusBar = new StatusBar();
|
||||
statusBar.setVisible(settings.showStatusBar);
|
||||
add(statusBar, BorderLayout.SOUTH);
|
||||
|
||||
codecLabel.setText(audio.description());
|
||||
statusBar.setCodec(audio.description());
|
||||
|
||||
ServerTab first = newTab();
|
||||
selectTab(first);
|
||||
@@ -149,160 +134,156 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
// ---- UI construction ----
|
||||
|
||||
private JMenuBar buildMenuBar() {
|
||||
JMenuBar bar = new JMenuBar();
|
||||
private MainMenuBar buildMenuBar() {
|
||||
return new MainMenuBar(bookmarks, new MainMenuBar.Listener() {
|
||||
@Override
|
||||
public void onConnect() {
|
||||
showConnectDialog();
|
||||
}
|
||||
|
||||
JMenu connections = new JMenu("Connections");
|
||||
JMenuItem connect = new JMenuItem("Connect…", Icons.of("CONNECT"));
|
||||
connect.setAccelerator(KeyStroke.getKeyStroke("control S"));
|
||||
connect.addActionListener(e -> showConnectDialog());
|
||||
JMenuItem disconnect = new JMenuItem("Disconnect", Icons.of("DISCONNECT"));
|
||||
disconnect.addActionListener(e -> doDisconnect());
|
||||
JMenuItem closeTab = new JMenuItem("Close tab", Icons.of("CLOSE_BUTTON"));
|
||||
closeTab.setAccelerator(KeyStroke.getKeyStroke("control W"));
|
||||
closeTab.addActionListener(e -> closeTab(selected));
|
||||
JMenuItem quit = new JMenuItem("Quit", Icons.of("QUIT"));
|
||||
quit.addActionListener(e -> quit());
|
||||
connections.add(connect);
|
||||
connections.add(disconnect);
|
||||
connections.add(closeTab);
|
||||
connections.addSeparator();
|
||||
connections.add(quit);
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
doDisconnect();
|
||||
}
|
||||
|
||||
bookmarksMenu = new JMenu("Bookmarks");
|
||||
rebuildBookmarksMenu();
|
||||
@Override
|
||||
public void onCloseTab() {
|
||||
closeTab(selected);
|
||||
}
|
||||
|
||||
JMenu self = new JMenu("Self");
|
||||
JMenuItem mute = new JMenuItem("Toggle microphone", Icons.of("CAPTURE"));
|
||||
mute.addActionListener(e -> micButton.doClick());
|
||||
JMenuItem deaf = new JMenuItem("Toggle speakers", Icons.of("PLAYBACK"));
|
||||
deaf.addActionListener(e -> speakerButton.doClick());
|
||||
awayItem = new JCheckBoxMenuItem("Away", Icons.of("AWAY"), false);
|
||||
awayItem.addActionListener(e -> toggleAway());
|
||||
awayStatusItem = new JMenuItem("Set away status…", Icons.of("EDIT"));
|
||||
awayStatusItem.addActionListener(e -> setAwayStatus());
|
||||
commanderItem = new JCheckBoxMenuItem("Channel commander", Icons.of("CHANNEL_COMMANDER"), false);
|
||||
commanderItem.addActionListener(e -> {
|
||||
if (selected != null) selected.setCommander(commanderItem.isSelected());
|
||||
@Override
|
||||
public void onQuit() {
|
||||
quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToggleMic() {
|
||||
toolbar.clickMic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToggleSpeaker() {
|
||||
toolbar.clickSpeaker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAwayToggle(boolean away) {
|
||||
toggleAway(away);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAwayStatus() {
|
||||
setAwayStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommanderToggle(boolean commander) {
|
||||
if (selected != null) selected.setCommander(commander);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChangeNickname() {
|
||||
changeNickname();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowIdentities() {
|
||||
showIdentities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowSettings() {
|
||||
showSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowAbout() {
|
||||
showAbout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectBookmark(Bookmark bookmark) {
|
||||
connectToBookmark(bookmark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCurrentServerBookmark() {
|
||||
addCurrentServerBookmark();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onManageBookmarks() {
|
||||
new BookmarksDialog(MainFrame.this, bookmarks, identities,
|
||||
MainFrame.this::connectToBookmark, menuBar::rebuildBookmarks).setVisible(true);
|
||||
}
|
||||
});
|
||||
JMenuItem nick = new JMenuItem("Change nickname…", Icons.of("CHANGE_NICKNAME"));
|
||||
nick.addActionListener(e -> changeNickname());
|
||||
self.add(mute);
|
||||
self.add(deaf);
|
||||
self.addSeparator();
|
||||
self.add(awayItem);
|
||||
self.add(awayStatusItem);
|
||||
self.add(commanderItem);
|
||||
self.addSeparator();
|
||||
self.add(nick);
|
||||
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
|
||||
identitiesItem.addActionListener(e -> showIdentities());
|
||||
JMenuItem options = new JMenuItem("Options…", Icons.of("SETTINGS"));
|
||||
options.addActionListener(e -> showSettings());
|
||||
tools.add(identitiesItem);
|
||||
tools.addSeparator();
|
||||
tools.add(options);
|
||||
|
||||
JMenu help = new JMenu("Help");
|
||||
JMenuItem about = new JMenuItem("About", Icons.of("ABOUT"));
|
||||
about.addActionListener(e -> showAbout());
|
||||
help.add(about);
|
||||
|
||||
bar.add(connections);
|
||||
bar.add(bookmarksMenu);
|
||||
bar.add(self);
|
||||
bar.add(tools);
|
||||
bar.add(help);
|
||||
return bar;
|
||||
}
|
||||
|
||||
private void rebuildBookmarksMenu() {
|
||||
bookmarksMenu.removeAll();
|
||||
for (Bookmark b : bookmarks.all()) {
|
||||
JMenuItem item = new JMenuItem(b.displayName(), Icons.of("SERVER_GREEN"));
|
||||
item.addActionListener(e -> connectToBookmark(b));
|
||||
bookmarksMenu.add(item);
|
||||
}
|
||||
if (!bookmarks.all().isEmpty()) bookmarksMenu.addSeparator();
|
||||
JMenuItem addCurrent = new JMenuItem("Add current server…", Icons.of("BOOKMARK_ADD"));
|
||||
addCurrent.addActionListener(e -> addCurrentServerBookmark());
|
||||
JMenuItem manage = new JMenuItem("Manage bookmarks…", Icons.of("BOOKMARK_MANAGER"));
|
||||
manage.addActionListener(e -> new BookmarksDialog(this, bookmarks, identities,
|
||||
this::connectToBookmark, this::rebuildBookmarksMenu).setVisible(true));
|
||||
bookmarksMenu.add(addCurrent);
|
||||
bookmarksMenu.add(manage);
|
||||
}
|
||||
private MainToolbar buildToolbar() {
|
||||
return new MainToolbar(settings, new MainToolbar.Listener() {
|
||||
@Override
|
||||
public void onConnect() {
|
||||
showConnectDialog();
|
||||
}
|
||||
|
||||
private JToolBar buildToolbar() {
|
||||
JToolBar tb = new JToolBar();
|
||||
tb.setFloatable(false);
|
||||
tb.setBackground(Theme.TOOLBAR_BG);
|
||||
tb.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
doDisconnect();
|
||||
}
|
||||
|
||||
connectButton = new JButton(Icons.connect());
|
||||
connectButton.setToolTipText("Connect to a server");
|
||||
connectButton.addActionListener(e -> showConnectDialog());
|
||||
@Override
|
||||
public void onActivate() {
|
||||
moveMicrophoneToSelectedTab();
|
||||
}
|
||||
|
||||
disconnectButton = new JButton(Icons.disconnect());
|
||||
disconnectButton.setToolTipText("Disconnect");
|
||||
disconnectButton.addActionListener(e -> doDisconnect());
|
||||
@Override
|
||||
public void onMicMuteToggle(boolean muted) {
|
||||
if (selected == null) return;
|
||||
selected.setMicMuted(muted);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
activeButton = new JToggleButton(Icons.micActive());
|
||||
activeButton.setToolTipText("Speak on this server (moves the microphone to this tab)");
|
||||
activeButton.addActionListener(e -> {
|
||||
if (selected != null && selected.isConnected()) setMicTab(selected);
|
||||
updateToolbar();
|
||||
@Override
|
||||
public void onDeafenToggle(boolean deafened) {
|
||||
if (selected == null) return;
|
||||
selected.setDeafened(deafened);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAwayToggle(boolean away) {
|
||||
toggleAway(away);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JPopupMenu buildAwayMenu() {
|
||||
return MainFrame.this.buildAwayMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettings() {
|
||||
showSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterVolumeChanged() {
|
||||
applyOutputSettingsToAllTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusBarVisibilityChanged(boolean visible) {
|
||||
statusBar.setVisible(visible);
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
|
||||
micButton = new JToggleButton(Icons.mic());
|
||||
micButton.setToolTipText("Mute / unmute microphone on this server");
|
||||
micButton.addActionListener(e -> {
|
||||
if (selected == null) return;
|
||||
selected.setMicMuted(micButton.isSelected());
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
speakerButton = new JToggleButton(Icons.speaker());
|
||||
speakerButton.setToolTipText("Deafen / undeafen (mute speakers) on this server");
|
||||
speakerButton.addActionListener(e -> {
|
||||
if (selected == null) return;
|
||||
selected.setDeafened(speakerButton.isSelected());
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
awayButton = new DropDownToggleButton(Icons.away(),
|
||||
"Away on this server (the arrow offers the global actions and presets)",
|
||||
this::buildAwayMenu);
|
||||
awayButton.addActionListener(e -> {
|
||||
if (selected == null) return;
|
||||
// The plain toggle carries no message; the menu is where messages are chosen.
|
||||
selected.setAway(awayButton.isSelected(), "");
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
JButton settingsButton = new JButton(Icons.settings());
|
||||
settingsButton.setToolTipText("Options");
|
||||
settingsButton.addActionListener(e -> showSettings());
|
||||
|
||||
tb.add(connectButton);
|
||||
tb.add(disconnectButton);
|
||||
tb.addSeparator();
|
||||
tb.add(activeButton);
|
||||
tb.add(micButton);
|
||||
tb.add(speakerButton);
|
||||
tb.add(awayButton);
|
||||
tb.addSeparator();
|
||||
tb.add(settingsButton);
|
||||
tb.add(Box.createHorizontalGlue());
|
||||
return tb;
|
||||
}
|
||||
|
||||
/** Rebuilds the icon-bearing chrome after the active icon pack changed. */
|
||||
private void rebuildIcons() {
|
||||
setIconImage(Icons.app().getImage());
|
||||
setJMenuBar(buildMenuBar());
|
||||
menuBar = buildMenuBar();
|
||||
setJMenuBar(menuBar);
|
||||
remove(toolbar);
|
||||
toolbar = buildToolbar();
|
||||
add(toolbar, BorderLayout.NORTH);
|
||||
@@ -313,18 +294,6 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
repaint();
|
||||
}
|
||||
|
||||
private JPanel buildStatusBar() {
|
||||
JPanel bar = new JPanel(new BorderLayout());
|
||||
bar.setBackground(Theme.STATUS_BG);
|
||||
bar.setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8));
|
||||
statusLabel.setFont(Theme.UI_FONT);
|
||||
codecLabel.setFont(Theme.UI_FONT);
|
||||
codecLabel.setForeground(Theme.CHAT_SYSTEM);
|
||||
bar.add(statusLabel, BorderLayout.WEST);
|
||||
bar.add(codecLabel, BorderLayout.EAST);
|
||||
return bar;
|
||||
}
|
||||
|
||||
// ---- tab management ----
|
||||
|
||||
private ServerTab newTab() {
|
||||
@@ -645,7 +614,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
if (joinChannel.isSelected()) bookmark.channel = channelPath;
|
||||
bookmarks.add(bookmark);
|
||||
bookmarks.save();
|
||||
rebuildBookmarksMenu();
|
||||
menuBar.rebuildBookmarks();
|
||||
}
|
||||
|
||||
/** The away button's drop-down: the global actions, the presets and their editor. */
|
||||
@@ -682,9 +651,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void toggleAway() {
|
||||
private void toggleAway(boolean away) {
|
||||
if (selected == null) return;
|
||||
selected.setAway(awayItem.isSelected(), "");
|
||||
selected.setAway(away, "");
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@@ -782,7 +751,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
soundPlayer.setOutputDevice(settings.outputDevice);
|
||||
for (ServerTab tab : tabs) {
|
||||
if (tab.connection().getPlayback() == null) continue;
|
||||
tab.connection().getPlayback().setMasterVolume(settings.outputVolume);
|
||||
tab.connection().getPlayback().setMasterVolume(settings.effectiveOutputVolume());
|
||||
tab.connection().getPlayback().setOutputDevice(settings.outputDevice);
|
||||
}
|
||||
}
|
||||
@@ -802,7 +771,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
"An open-source TeamSpeak 3 desktop client built on the ts3j\n" +
|
||||
"reverse-engineered protocol library, with native Opus voice,\n" +
|
||||
"voice-activation detection and push-to-talk.\n\n" +
|
||||
codecLabel.getText(),
|
||||
statusBar.codecText(),
|
||||
"About TS3J", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
@@ -810,28 +779,15 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
private void updateToolbar() {
|
||||
boolean connected = selected != null && selected.isConnected();
|
||||
disconnectButton.setEnabled(connected);
|
||||
micButton.setEnabled(connected);
|
||||
speakerButton.setEnabled(connected);
|
||||
activeButton.setEnabled(connected);
|
||||
awayItem.setEnabled(connected);
|
||||
awayStatusItem.setEnabled(connected);
|
||||
// The menu's actions are global, so the arrow stays live while any server is up.
|
||||
awayButton.setEnabled(tabs.stream().anyMatch(ServerTab::isConnected));
|
||||
awayButton.setToggleEnabled(connected);
|
||||
commanderItem.setEnabled(connected);
|
||||
boolean anyConnected = tabs.stream().anyMatch(ServerTab::isConnected);
|
||||
|
||||
boolean micMuted = connected && selected.isMicMuted();
|
||||
boolean deaf = connected && selected.isDeafened();
|
||||
micButton.setSelected(micMuted);
|
||||
micButton.setIcon(micMuted ? Icons.micMutedLarge() : Icons.mic());
|
||||
speakerButton.setSelected(deaf);
|
||||
speakerButton.setIcon(deaf ? Icons.speakerMutedLarge() : Icons.speaker());
|
||||
activeButton.setSelected(selected != null && selected == micTab);
|
||||
boolean active = selected != null && selected == micTab;
|
||||
boolean away = connected && selected.isAway();
|
||||
awayItem.setSelected(away);
|
||||
awayButton.setSelected(away);
|
||||
commanderItem.setSelected(connected && selected.isCommander());
|
||||
boolean commander = connected && selected.isCommander();
|
||||
toolbar.refresh(connected, anyConnected, micMuted, deaf, active, away);
|
||||
menuBar.refresh(connected, away, commander);
|
||||
updateTray();
|
||||
}
|
||||
|
||||
@@ -855,7 +811,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
}
|
||||
|
||||
private void updateStatusLabel() {
|
||||
statusLabel.setText(selected == null ? "Not connected" : selected.status());
|
||||
statusBar.setStatus(selected == null ? "Not connected" : selected.status());
|
||||
}
|
||||
|
||||
private void updateConnectionStatus() {
|
||||
@@ -868,6 +824,6 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
double ping = tab.connection().getPingMillis();
|
||||
if (ping >= 0) s.append(" | ping ").append(Math.round(ping)).append(" ms");
|
||||
if (tab != micTab) s.append(" | microphone on another tab");
|
||||
statusLabel.setText(s.toString());
|
||||
statusBar.setStatus(s.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user