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:
@@ -95,6 +95,12 @@ public final class Settings {
|
|||||||
public boolean music = false;
|
public boolean music = false;
|
||||||
/** Master playback gain, 0..1 (may exceed 1 for boost up to 2). */
|
/** Master playback gain, 0..1 (may exceed 1 for boost up to 2). */
|
||||||
public double outputVolume = 1.0;
|
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. */
|
/** Microphone input gain multiplier applied before VAD/encode. */
|
||||||
public double inputVolume = 1.0;
|
public double inputVolume = 1.0;
|
||||||
/** Remove steady background noise from the microphone (spectral denoise). */
|
/** 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. */
|
/** Extra folder to look for icon packs in, on top of the well-known locations. */
|
||||||
public String iconPackDir = "";
|
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. */
|
/** Which actions make a sound, and which ones are important enough to survive muting. */
|
||||||
public final NotificationSettings notifications = new NotificationSettings();
|
public final NotificationSettings notifications = new NotificationSettings();
|
||||||
|
|
||||||
@@ -155,6 +167,16 @@ public final class Settings {
|
|||||||
return new File(identityFile);
|
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). */
|
/** The directory holding all persistent client state (settings, identities, caches). */
|
||||||
public static File configDir() {
|
public static File configDir() {
|
||||||
return DIR;
|
return DIR;
|
||||||
@@ -185,6 +207,7 @@ public final class Settings {
|
|||||||
packetLoss = parseI(props.getProperty("packetLoss"), packetLoss);
|
packetLoss = parseI(props.getProperty("packetLoss"), packetLoss);
|
||||||
music = parseB(props.getProperty("music"), music);
|
music = parseB(props.getProperty("music"), music);
|
||||||
outputVolume = parseD(props.getProperty("outputVolume"), outputVolume);
|
outputVolume = parseD(props.getProperty("outputVolume"), outputVolume);
|
||||||
|
masterVolume = parseD(props.getProperty("masterVolume"), masterVolume);
|
||||||
inputVolume = parseD(props.getProperty("inputVolume"), inputVolume);
|
inputVolume = parseD(props.getProperty("inputVolume"), inputVolume);
|
||||||
denoise = parseB(props.getProperty("denoise"), denoise);
|
denoise = parseB(props.getProperty("denoise"), denoise);
|
||||||
denoiserLevel = parseD(props.getProperty("denoiserLevel"), denoiserLevel);
|
denoiserLevel = parseD(props.getProperty("denoiserLevel"), denoiserLevel);
|
||||||
@@ -195,6 +218,8 @@ public final class Settings {
|
|||||||
soundPackDir = props.getProperty("soundPackDir", soundPackDir);
|
soundPackDir = props.getProperty("soundPackDir", soundPackDir);
|
||||||
iconPack = props.getProperty("iconPack", iconPack);
|
iconPack = props.getProperty("iconPack", iconPack);
|
||||||
iconPackDir = props.getProperty("iconPackDir", iconPackDir);
|
iconPackDir = props.getProperty("iconPackDir", iconPackDir);
|
||||||
|
showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar);
|
||||||
|
showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider);
|
||||||
notifications.load(props);
|
notifications.load(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +248,7 @@ public final class Settings {
|
|||||||
props.setProperty("packetLoss", Integer.toString(packetLoss));
|
props.setProperty("packetLoss", Integer.toString(packetLoss));
|
||||||
props.setProperty("music", Boolean.toString(music));
|
props.setProperty("music", Boolean.toString(music));
|
||||||
props.setProperty("outputVolume", Double.toString(outputVolume));
|
props.setProperty("outputVolume", Double.toString(outputVolume));
|
||||||
|
props.setProperty("masterVolume", Double.toString(masterVolume));
|
||||||
props.setProperty("inputVolume", Double.toString(inputVolume));
|
props.setProperty("inputVolume", Double.toString(inputVolume));
|
||||||
props.setProperty("denoise", Boolean.toString(denoise));
|
props.setProperty("denoise", Boolean.toString(denoise));
|
||||||
props.setProperty("denoiserLevel", Double.toString(denoiserLevel));
|
props.setProperty("denoiserLevel", Double.toString(denoiserLevel));
|
||||||
@@ -233,6 +259,8 @@ public final class Settings {
|
|||||||
props.setProperty("soundPackDir", soundPackDir);
|
props.setProperty("soundPackDir", soundPackDir);
|
||||||
props.setProperty("iconPack", iconPack);
|
props.setProperty("iconPack", iconPack);
|
||||||
props.setProperty("iconPackDir", iconPackDir);
|
props.setProperty("iconPackDir", iconPackDir);
|
||||||
|
props.setProperty("showStatusBar", Boolean.toString(showStatusBar));
|
||||||
|
props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider));
|
||||||
notifications.store(props);
|
notifications.store(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public final class SoundNotifier {
|
|||||||
if (script == null) return;
|
if (script == null) return;
|
||||||
|
|
||||||
String resolved = script.resolve(withDefaults(variables));
|
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 (volume <= 0) return;
|
||||||
|
|
||||||
if (script.kind() == SoundScript.Kind.SAY) {
|
if (script.kind() == SoundScript.Kind.SAY) {
|
||||||
|
|||||||
@@ -12,22 +12,15 @@ import com.ts3client.sound.SoundNotifier;
|
|||||||
import com.ts3client.sound.SoundPlayer;
|
import com.ts3client.sound.SoundPlayer;
|
||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.Box;
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JCheckBox;
|
import javax.swing.JCheckBox;
|
||||||
import javax.swing.JCheckBoxMenuItem;
|
import javax.swing.JCheckBoxMenuItem;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JMenu;
|
|
||||||
import javax.swing.JMenuBar;
|
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JPopupMenu;
|
import javax.swing.JPopupMenu;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
import javax.swing.JToggleButton;
|
|
||||||
import javax.swing.JToolBar;
|
|
||||||
import javax.swing.KeyStroke;
|
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Dimension;
|
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. */
|
/** The tab that owns the capture device; null when nobody is capturing. */
|
||||||
private ServerTab micTab;
|
private ServerTab micTab;
|
||||||
|
|
||||||
private JMenu bookmarksMenu;
|
private MainMenuBar menuBar;
|
||||||
private JCheckBoxMenuItem awayItem;
|
|
||||||
private JMenuItem awayStatusItem;
|
|
||||||
private JCheckBoxMenuItem commanderItem;
|
|
||||||
private javax.swing.Timer statusTimer;
|
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. */
|
/** Mirrors the active server's own client state next to the clock. */
|
||||||
private TrayController tray;
|
private TrayController tray;
|
||||||
|
|
||||||
private JToolBar toolbar;
|
private MainToolbar toolbar;
|
||||||
private JButton connectButton;
|
private StatusBar statusBar;
|
||||||
private JButton disconnectButton;
|
|
||||||
private JToggleButton activeButton;
|
|
||||||
private JToggleButton micButton;
|
|
||||||
private JToggleButton speakerButton;
|
|
||||||
private DropDownToggleButton awayButton;
|
|
||||||
|
|
||||||
private boolean pttPressed;
|
private boolean pttPressed;
|
||||||
|
|
||||||
@@ -117,16 +99,19 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
setMinimumSize(new Dimension(720, 480));
|
setMinimumSize(new Dimension(720, 480));
|
||||||
|
|
||||||
tray = new TrayController(this, this::quit);
|
tray = new TrayController(this, this::quit);
|
||||||
setJMenuBar(buildMenuBar());
|
menuBar = buildMenuBar();
|
||||||
|
setJMenuBar(menuBar);
|
||||||
|
|
||||||
toolbar = buildToolbar();
|
toolbar = buildToolbar();
|
||||||
add(toolbar, BorderLayout.NORTH);
|
add(toolbar, BorderLayout.NORTH);
|
||||||
// Switching the icon pack in the options dialog re-decorates the whole window.
|
// Switching the icon pack in the options dialog re-decorates the whole window.
|
||||||
IconTheme.get().addListener(() -> SwingUtilities.invokeLater(this::rebuildIcons));
|
IconTheme.get().addListener(() -> SwingUtilities.invokeLater(this::rebuildIcons));
|
||||||
add(tabPane, BorderLayout.CENTER);
|
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();
|
ServerTab first = newTab();
|
||||||
selectTab(first);
|
selectTab(first);
|
||||||
@@ -149,160 +134,156 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
|
|
||||||
// ---- UI construction ----
|
// ---- UI construction ----
|
||||||
|
|
||||||
private JMenuBar buildMenuBar() {
|
private MainMenuBar buildMenuBar() {
|
||||||
JMenuBar bar = new JMenuBar();
|
return new MainMenuBar(bookmarks, new MainMenuBar.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onConnect() {
|
||||||
|
showConnectDialog();
|
||||||
|
}
|
||||||
|
|
||||||
JMenu connections = new JMenu("Connections");
|
@Override
|
||||||
JMenuItem connect = new JMenuItem("Connect…", Icons.of("CONNECT"));
|
public void onDisconnect() {
|
||||||
connect.setAccelerator(KeyStroke.getKeyStroke("control S"));
|
doDisconnect();
|
||||||
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);
|
|
||||||
|
|
||||||
bookmarksMenu = new JMenu("Bookmarks");
|
@Override
|
||||||
rebuildBookmarksMenu();
|
public void onCloseTab() {
|
||||||
|
closeTab(selected);
|
||||||
|
}
|
||||||
|
|
||||||
JMenu self = new JMenu("Self");
|
@Override
|
||||||
JMenuItem mute = new JMenuItem("Toggle microphone", Icons.of("CAPTURE"));
|
public void onQuit() {
|
||||||
mute.addActionListener(e -> micButton.doClick());
|
quit();
|
||||||
JMenuItem deaf = new JMenuItem("Toggle speakers", Icons.of("PLAYBACK"));
|
}
|
||||||
deaf.addActionListener(e -> speakerButton.doClick());
|
|
||||||
awayItem = new JCheckBoxMenuItem("Away", Icons.of("AWAY"), false);
|
@Override
|
||||||
awayItem.addActionListener(e -> toggleAway());
|
public void onToggleMic() {
|
||||||
awayStatusItem = new JMenuItem("Set away status…", Icons.of("EDIT"));
|
toolbar.clickMic();
|
||||||
awayStatusItem.addActionListener(e -> setAwayStatus());
|
}
|
||||||
commanderItem = new JCheckBoxMenuItem("Channel commander", Icons.of("CHANNEL_COMMANDER"), false);
|
|
||||||
commanderItem.addActionListener(e -> {
|
@Override
|
||||||
if (selected != null) selected.setCommander(commanderItem.isSelected());
|
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() {
|
private MainToolbar buildToolbar() {
|
||||||
bookmarksMenu.removeAll();
|
return new MainToolbar(settings, new MainToolbar.Listener() {
|
||||||
for (Bookmark b : bookmarks.all()) {
|
@Override
|
||||||
JMenuItem item = new JMenuItem(b.displayName(), Icons.of("SERVER_GREEN"));
|
public void onConnect() {
|
||||||
item.addActionListener(e -> connectToBookmark(b));
|
showConnectDialog();
|
||||||
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 JToolBar buildToolbar() {
|
@Override
|
||||||
JToolBar tb = new JToolBar();
|
public void onDisconnect() {
|
||||||
tb.setFloatable(false);
|
doDisconnect();
|
||||||
tb.setBackground(Theme.TOOLBAR_BG);
|
}
|
||||||
tb.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
|
|
||||||
|
|
||||||
connectButton = new JButton(Icons.connect());
|
@Override
|
||||||
connectButton.setToolTipText("Connect to a server");
|
public void onActivate() {
|
||||||
connectButton.addActionListener(e -> showConnectDialog());
|
moveMicrophoneToSelectedTab();
|
||||||
|
}
|
||||||
|
|
||||||
disconnectButton = new JButton(Icons.disconnect());
|
@Override
|
||||||
disconnectButton.setToolTipText("Disconnect");
|
public void onMicMuteToggle(boolean muted) {
|
||||||
disconnectButton.addActionListener(e -> doDisconnect());
|
if (selected == null) return;
|
||||||
|
selected.setMicMuted(muted);
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
activeButton = new JToggleButton(Icons.micActive());
|
@Override
|
||||||
activeButton.setToolTipText("Speak on this server (moves the microphone to this tab)");
|
public void onDeafenToggle(boolean deafened) {
|
||||||
activeButton.addActionListener(e -> {
|
if (selected == null) return;
|
||||||
if (selected != null && selected.isConnected()) setMicTab(selected);
|
selected.setDeafened(deafened);
|
||||||
updateToolbar();
|
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. */
|
/** Rebuilds the icon-bearing chrome after the active icon pack changed. */
|
||||||
private void rebuildIcons() {
|
private void rebuildIcons() {
|
||||||
setIconImage(Icons.app().getImage());
|
setIconImage(Icons.app().getImage());
|
||||||
setJMenuBar(buildMenuBar());
|
menuBar = buildMenuBar();
|
||||||
|
setJMenuBar(menuBar);
|
||||||
remove(toolbar);
|
remove(toolbar);
|
||||||
toolbar = buildToolbar();
|
toolbar = buildToolbar();
|
||||||
add(toolbar, BorderLayout.NORTH);
|
add(toolbar, BorderLayout.NORTH);
|
||||||
@@ -313,18 +294,6 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
repaint();
|
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 ----
|
// ---- tab management ----
|
||||||
|
|
||||||
private ServerTab newTab() {
|
private ServerTab newTab() {
|
||||||
@@ -645,7 +614,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
if (joinChannel.isSelected()) bookmark.channel = channelPath;
|
if (joinChannel.isSelected()) bookmark.channel = channelPath;
|
||||||
bookmarks.add(bookmark);
|
bookmarks.add(bookmark);
|
||||||
bookmarks.save();
|
bookmarks.save();
|
||||||
rebuildBookmarksMenu();
|
menuBar.rebuildBookmarks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The away button's drop-down: the global actions, the presets and their editor. */
|
/** 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;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleAway() {
|
private void toggleAway(boolean away) {
|
||||||
if (selected == null) return;
|
if (selected == null) return;
|
||||||
selected.setAway(awayItem.isSelected(), "");
|
selected.setAway(away, "");
|
||||||
updateToolbar();
|
updateToolbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,7 +751,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
soundPlayer.setOutputDevice(settings.outputDevice);
|
soundPlayer.setOutputDevice(settings.outputDevice);
|
||||||
for (ServerTab tab : tabs) {
|
for (ServerTab tab : tabs) {
|
||||||
if (tab.connection().getPlayback() == null) continue;
|
if (tab.connection().getPlayback() == null) continue;
|
||||||
tab.connection().getPlayback().setMasterVolume(settings.outputVolume);
|
tab.connection().getPlayback().setMasterVolume(settings.effectiveOutputVolume());
|
||||||
tab.connection().getPlayback().setOutputDevice(settings.outputDevice);
|
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" +
|
"An open-source TeamSpeak 3 desktop client built on the ts3j\n" +
|
||||||
"reverse-engineered protocol library, with native Opus voice,\n" +
|
"reverse-engineered protocol library, with native Opus voice,\n" +
|
||||||
"voice-activation detection and push-to-talk.\n\n" +
|
"voice-activation detection and push-to-talk.\n\n" +
|
||||||
codecLabel.getText(),
|
statusBar.codecText(),
|
||||||
"About TS3J", JOptionPane.INFORMATION_MESSAGE);
|
"About TS3J", JOptionPane.INFORMATION_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,28 +779,15 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
|
|
||||||
private void updateToolbar() {
|
private void updateToolbar() {
|
||||||
boolean connected = selected != null && selected.isConnected();
|
boolean connected = selected != null && selected.isConnected();
|
||||||
disconnectButton.setEnabled(connected);
|
boolean anyConnected = tabs.stream().anyMatch(ServerTab::isConnected);
|
||||||
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 micMuted = connected && selected.isMicMuted();
|
boolean micMuted = connected && selected.isMicMuted();
|
||||||
boolean deaf = connected && selected.isDeafened();
|
boolean deaf = connected && selected.isDeafened();
|
||||||
micButton.setSelected(micMuted);
|
boolean active = selected != null && selected == micTab;
|
||||||
micButton.setIcon(micMuted ? Icons.micMutedLarge() : Icons.mic());
|
|
||||||
speakerButton.setSelected(deaf);
|
|
||||||
speakerButton.setIcon(deaf ? Icons.speakerMutedLarge() : Icons.speaker());
|
|
||||||
activeButton.setSelected(selected != null && selected == micTab);
|
|
||||||
boolean away = connected && selected.isAway();
|
boolean away = connected && selected.isAway();
|
||||||
awayItem.setSelected(away);
|
boolean commander = connected && selected.isCommander();
|
||||||
awayButton.setSelected(away);
|
toolbar.refresh(connected, anyConnected, micMuted, deaf, active, away);
|
||||||
commanderItem.setSelected(connected && selected.isCommander());
|
menuBar.refresh(connected, away, commander);
|
||||||
updateTray();
|
updateTray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,7 +811,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatusLabel() {
|
private void updateStatusLabel() {
|
||||||
statusLabel.setText(selected == null ? "Not connected" : selected.status());
|
statusBar.setStatus(selected == null ? "Not connected" : selected.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateConnectionStatus() {
|
private void updateConnectionStatus() {
|
||||||
@@ -868,6 +824,6 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
|||||||
double ping = tab.connection().getPingMillis();
|
double ping = tab.connection().getPingMillis();
|
||||||
if (ping >= 0) s.append(" | ping ").append(Math.round(ping)).append(" ms");
|
if (ping >= 0) s.append(" | ping ").append(Math.round(ping)).append(" ms");
|
||||||
if (tab != micTab) s.append(" | microphone on another tab");
|
if (tab != micTab) s.append(" | microphone on another tab");
|
||||||
statusLabel.setText(s.toString());
|
statusBar.setStatus(s.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
ts3-client/swing/src/main/java/com/ts3client/ui/MainMenuBar.java
Normal file
155
ts3-client/swing/src/main/java/com/ts3client/ui/MainMenuBar.java
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package com.ts3client.ui;
|
||||||
|
|
||||||
|
import com.ts3client.config.Bookmark;
|
||||||
|
import com.ts3client.config.Bookmarks;
|
||||||
|
|
||||||
|
import javax.swing.JCheckBoxMenuItem;
|
||||||
|
import javax.swing.JMenu;
|
||||||
|
import javax.swing.JMenuBar;
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main window's menu bar: Connections, Bookmarks, Self, Tools and Help.
|
||||||
|
* Talks to {@link MainFrame} only through {@link Listener}; the away/commander
|
||||||
|
* item states are pushed in by {@link #refresh}, mirroring how {@link MainToolbar}
|
||||||
|
* is kept in sync.
|
||||||
|
*/
|
||||||
|
final class MainMenuBar extends JMenuBar {
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onConnect();
|
||||||
|
|
||||||
|
void onDisconnect();
|
||||||
|
|
||||||
|
void onCloseTab();
|
||||||
|
|
||||||
|
void onQuit();
|
||||||
|
|
||||||
|
void onToggleMic();
|
||||||
|
|
||||||
|
void onToggleSpeaker();
|
||||||
|
|
||||||
|
/** The plain toggle carries no message; "Set away status…" is where messages are chosen. */
|
||||||
|
void onAwayToggle(boolean away);
|
||||||
|
|
||||||
|
void onAwayStatus();
|
||||||
|
|
||||||
|
void onCommanderToggle(boolean commander);
|
||||||
|
|
||||||
|
void onChangeNickname();
|
||||||
|
|
||||||
|
void onShowIdentities();
|
||||||
|
|
||||||
|
void onShowSettings();
|
||||||
|
|
||||||
|
void onShowAbout();
|
||||||
|
|
||||||
|
void onConnectBookmark(Bookmark bookmark);
|
||||||
|
|
||||||
|
void onAddCurrentServerBookmark();
|
||||||
|
|
||||||
|
void onManageBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Bookmarks bookmarks;
|
||||||
|
private final Listener listener;
|
||||||
|
|
||||||
|
private JMenu bookmarksMenu;
|
||||||
|
private JCheckBoxMenuItem awayItem;
|
||||||
|
private JMenuItem awayStatusItem;
|
||||||
|
private JCheckBoxMenuItem commanderItem;
|
||||||
|
|
||||||
|
MainMenuBar(Bookmarks bookmarks, Listener listener) {
|
||||||
|
this.bookmarks = bookmarks;
|
||||||
|
this.listener = listener;
|
||||||
|
|
||||||
|
JMenu connections = new JMenu("Connections");
|
||||||
|
JMenuItem connect = new JMenuItem("Connect…", Icons.of("CONNECT"));
|
||||||
|
connect.setAccelerator(KeyStroke.getKeyStroke("control S"));
|
||||||
|
connect.addActionListener(e -> listener.onConnect());
|
||||||
|
JMenuItem disconnect = new JMenuItem("Disconnect", Icons.of("DISCONNECT"));
|
||||||
|
disconnect.addActionListener(e -> listener.onDisconnect());
|
||||||
|
JMenuItem closeTab = new JMenuItem("Close tab", Icons.of("CLOSE_BUTTON"));
|
||||||
|
closeTab.setAccelerator(KeyStroke.getKeyStroke("control W"));
|
||||||
|
closeTab.addActionListener(e -> listener.onCloseTab());
|
||||||
|
JMenuItem quit = new JMenuItem("Quit", Icons.of("QUIT"));
|
||||||
|
quit.addActionListener(e -> listener.onQuit());
|
||||||
|
connections.add(connect);
|
||||||
|
connections.add(disconnect);
|
||||||
|
connections.add(closeTab);
|
||||||
|
connections.addSeparator();
|
||||||
|
connections.add(quit);
|
||||||
|
|
||||||
|
bookmarksMenu = new JMenu("Bookmarks");
|
||||||
|
rebuildBookmarks();
|
||||||
|
|
||||||
|
JMenu self = new JMenu("Self");
|
||||||
|
JMenuItem mute = new JMenuItem("Toggle microphone", Icons.of("CAPTURE"));
|
||||||
|
mute.addActionListener(e -> listener.onToggleMic());
|
||||||
|
JMenuItem deaf = new JMenuItem("Toggle speakers", Icons.of("PLAYBACK"));
|
||||||
|
deaf.addActionListener(e -> listener.onToggleSpeaker());
|
||||||
|
awayItem = new JCheckBoxMenuItem("Away", Icons.of("AWAY"), false);
|
||||||
|
awayItem.addActionListener(e -> listener.onAwayToggle(awayItem.isSelected()));
|
||||||
|
awayStatusItem = new JMenuItem("Set away status…", Icons.of("EDIT"));
|
||||||
|
awayStatusItem.addActionListener(e -> listener.onAwayStatus());
|
||||||
|
commanderItem = new JCheckBoxMenuItem("Channel commander", Icons.of("CHANNEL_COMMANDER"), false);
|
||||||
|
commanderItem.addActionListener(e -> listener.onCommanderToggle(commanderItem.isSelected()));
|
||||||
|
JMenuItem nick = new JMenuItem("Change nickname…", Icons.of("CHANGE_NICKNAME"));
|
||||||
|
nick.addActionListener(e -> listener.onChangeNickname());
|
||||||
|
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 -> listener.onShowIdentities());
|
||||||
|
JMenuItem options = new JMenuItem("Options…", Icons.of("SETTINGS"));
|
||||||
|
options.addActionListener(e -> listener.onShowSettings());
|
||||||
|
tools.add(identitiesItem);
|
||||||
|
tools.addSeparator();
|
||||||
|
tools.add(options);
|
||||||
|
|
||||||
|
JMenu help = new JMenu("Help");
|
||||||
|
JMenuItem about = new JMenuItem("About", Icons.of("ABOUT"));
|
||||||
|
about.addActionListener(e -> listener.onShowAbout());
|
||||||
|
help.add(about);
|
||||||
|
|
||||||
|
add(connections);
|
||||||
|
add(bookmarksMenu);
|
||||||
|
add(self);
|
||||||
|
add(tools);
|
||||||
|
add(help);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reflects the current tab's away/commander state on the menu items. */
|
||||||
|
void refresh(boolean connected, boolean away, boolean commander) {
|
||||||
|
awayItem.setEnabled(connected);
|
||||||
|
awayStatusItem.setEnabled(connected);
|
||||||
|
commanderItem.setEnabled(connected);
|
||||||
|
awayItem.setSelected(away);
|
||||||
|
commanderItem.setSelected(commander);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-lists the saved bookmarks; called after they change (add, remove, manage…). */
|
||||||
|
void rebuildBookmarks() {
|
||||||
|
bookmarksMenu.removeAll();
|
||||||
|
for (Bookmark b : bookmarks.all()) {
|
||||||
|
JMenuItem item = new JMenuItem(b.displayName(), Icons.of("SERVER_GREEN"));
|
||||||
|
item.addActionListener(e -> listener.onConnectBookmark(b));
|
||||||
|
bookmarksMenu.add(item);
|
||||||
|
}
|
||||||
|
if (!bookmarks.all().isEmpty()) bookmarksMenu.addSeparator();
|
||||||
|
JMenuItem addCurrent = new JMenuItem("Add current server…", Icons.of("BOOKMARK_ADD"));
|
||||||
|
addCurrent.addActionListener(e -> listener.onAddCurrentServerBookmark());
|
||||||
|
JMenuItem manage = new JMenuItem("Manage bookmarks…", Icons.of("BOOKMARK_MANAGER"));
|
||||||
|
manage.addActionListener(e -> listener.onManageBookmarks());
|
||||||
|
bookmarksMenu.add(addCurrent);
|
||||||
|
bookmarksMenu.add(manage);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ts3client.ui;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
|
||||||
|
/** The strip at the bottom of the window: connection status on the left, codec info on the right. */
|
||||||
|
final class StatusBar extends JPanel {
|
||||||
|
|
||||||
|
private final JLabel statusLabel = new JLabel("Not connected");
|
||||||
|
private final JLabel codecLabel = new JLabel();
|
||||||
|
|
||||||
|
StatusBar() {
|
||||||
|
super(new BorderLayout());
|
||||||
|
setBackground(Theme.STATUS_BG);
|
||||||
|
setBorder(BorderFactory.createEmptyBorder(3, 8, 3, 8));
|
||||||
|
statusLabel.setFont(Theme.UI_FONT);
|
||||||
|
codecLabel.setFont(Theme.UI_FONT);
|
||||||
|
codecLabel.setForeground(Theme.CHAT_SYSTEM);
|
||||||
|
add(statusLabel, BorderLayout.WEST);
|
||||||
|
add(codecLabel, BorderLayout.EAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatus(String text) {
|
||||||
|
statusLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCodec(String text) {
|
||||||
|
codecLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
String codecText() {
|
||||||
|
return codecLabel.getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user