diff --git a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java index 1e0d0c6..b356ea6 100644 --- a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java +++ b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java @@ -95,6 +95,12 @@ public final class Settings { public boolean music = false; /** Master playback gain, 0..1 (may exceed 1 for boost up to 2). */ 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. */ public double inputVolume = 1.0; /** 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. */ 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. */ public final NotificationSettings notifications = new NotificationSettings(); @@ -155,6 +167,16 @@ public final class Settings { 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). */ public static File configDir() { return DIR; @@ -185,6 +207,7 @@ public final class Settings { packetLoss = parseI(props.getProperty("packetLoss"), packetLoss); music = parseB(props.getProperty("music"), music); outputVolume = parseD(props.getProperty("outputVolume"), outputVolume); + masterVolume = parseD(props.getProperty("masterVolume"), masterVolume); inputVolume = parseD(props.getProperty("inputVolume"), inputVolume); denoise = parseB(props.getProperty("denoise"), denoise); denoiserLevel = parseD(props.getProperty("denoiserLevel"), denoiserLevel); @@ -195,6 +218,8 @@ public final class Settings { soundPackDir = props.getProperty("soundPackDir", soundPackDir); iconPack = props.getProperty("iconPack", iconPack); iconPackDir = props.getProperty("iconPackDir", iconPackDir); + showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar); + showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider); notifications.load(props); } @@ -223,6 +248,7 @@ public final class Settings { props.setProperty("packetLoss", Integer.toString(packetLoss)); props.setProperty("music", Boolean.toString(music)); props.setProperty("outputVolume", Double.toString(outputVolume)); + props.setProperty("masterVolume", Double.toString(masterVolume)); props.setProperty("inputVolume", Double.toString(inputVolume)); props.setProperty("denoise", Boolean.toString(denoise)); props.setProperty("denoiserLevel", Double.toString(denoiserLevel)); @@ -233,6 +259,8 @@ public final class Settings { props.setProperty("soundPackDir", soundPackDir); props.setProperty("iconPack", iconPack); props.setProperty("iconPackDir", iconPackDir); + props.setProperty("showStatusBar", Boolean.toString(showStatusBar)); + props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider)); notifications.store(props); } diff --git a/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java b/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java index bb1e6ef..ca8c4ed 100644 --- a/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java +++ b/ts3-client/core/src/main/java/com/ts3client/sound/SoundNotifier.java @@ -113,7 +113,7 @@ public final class SoundNotifier { if (script == null) return; 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 (script.kind() == SoundScript.Kind.SAY) { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java index 817ba09..73d957c 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainFrame.java @@ -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()); } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainMenuBar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainMenuBar.java new file mode 100644 index 0000000..7818c8c --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainMenuBar.java @@ -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); + } +} diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java new file mode 100644 index 0000000..093d327 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/MainToolbar.java @@ -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). + * + *

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; + } +} diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java b/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java new file mode 100644 index 0000000..5286743 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/StatusBar.java @@ -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(); + } +}