package com.ts3client.ui;
import com.ts3client.config.Settings;
import com.ts3client.sound.NotificationSettings;
import com.ts3client.sound.SoundEvent;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPack;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Options page for sound packs: which pack is active, how loud it is, and what
* each action does.
*
*
An action can be switched off entirely, or marked important (shown in
* bold) — important actions are the only ones still played while the speakers are
* muted, which is how the official client behaves. Actions the active pack has no
* sound for are greyed out.
*/
final class NotificationsPanel extends JPanel {
private static final int TOGGLE_COLUMN_WIDTH = 34;
/** A table row: either a category heading or one action. */
private static final class Row {
final SoundEvent.Category heading;
final SoundEvent event;
Row(SoundEvent.Category heading, SoundEvent event) {
this.heading = heading;
this.event = event;
}
boolean isHeading() {
return event == null;
}
}
private final Settings settings;
private final SoundNotifier sounds;
/** Edited copy, applied to the settings only when the dialog is confirmed. */
private final NotificationSettings working;
private final List rows = new ArrayList<>();
private final String originalPackId;
private final String originalPackDir;
private final JComboBox packCombo = new JComboBox<>();
private final JLabel packInfo = new JLabel();
private final JSlider volume;
private final JTextField packDirField;
private final JTable table;
private final EventTableModel tableModel = new EventTableModel();
NotificationsPanel(Settings settings, SoundNotifier sounds) {
super(new BorderLayout(0, 8));
this.settings = settings;
this.sounds = sounds;
this.working = settings.notifications.copy();
this.originalPackId = settings.soundPack;
this.originalPackDir = settings.soundPackDir;
this.volume = new JSlider(0, 100, (int) Math.round(settings.soundVolume * 100));
this.packDirField = new JTextField(settings.soundPackDir, 18);
buildRows();
this.table = buildTable();
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
add(buildHeader(), BorderLayout.NORTH);
add(new JScrollPane(table), BorderLayout.CENTER);
add(buildFooter(), BorderLayout.SOUTH);
reloadPacks();
}
/** Copies the edited state into the settings; the caller saves them. */
void apply() {
settings.notifications.copyFrom(working);
settings.soundVolume = volume.getValue() / 100.0;
String directory = packDirField.getText().trim();
boolean rescan = !directory.equals(settings.soundPackDir);
settings.soundPackDir = directory;
sounds.setPack((SoundPack) packCombo.getSelectedItem());
if (rescan) sounds.reload();
}
/** Puts back the pack (and folder) the dialog started with, for a cancelled edit. */
void revert() {
settings.soundPackDir = originalPackDir;
settings.soundPack = originalPackId;
sounds.reload();
}
// ---- layout ----
private JComponent buildHeader() {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 2, 2, 2);
c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL;
packCombo.setToolTipText("Sound packs installed here or in a TeamSpeak 3 client");
packCombo.addActionListener(e -> onPackSelected());
JButton test = new JButton("Test");
test.setToolTipText("Play this pack's test sound");
test.addActionListener(e -> sounds.preview(SoundEvent.SPECIAL_SOUND_TEST));
JPanel packRow = new JPanel(new BorderLayout(6, 0));
packRow.add(packCombo, BorderLayout.CENTER);
packRow.add(test, BorderLayout.EAST);
int row = 0;
addRow(p, c, row++, new JLabel("Sound pack:"), packRow);
c.gridx = 1;
c.gridy = row++;
packInfo.setEnabled(false);
p.add(packInfo, c);
volume.setToolTipText("Volume of the notification sounds");
addRow(p, c, row++, new JLabel("Sound volume:"), volume);
JPanel dirRow = new JPanel(new BorderLayout(6, 0));
JButton browse = new JButton("Browse…");
browse.addActionListener(e -> browseForPackFolder());
dirRow.add(packDirField, BorderLayout.CENTER);
dirRow.add(browse, BorderLayout.EAST);
packDirField.setToolTipText("Extra folder to look for sound packs in");
addRow(p, c, row, new JLabel("Extra pack folder:"), dirRow);
return p;
}
private JComponent buildFooter() {
JPanel p = new JPanel(new BorderLayout(6, 0));
JLabel hint = new JLabel("Bold actions are important: they are still played "
+ "while your speakers are muted.");
hint.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0));
JPanel buttons = new JPanel();
JButton important = new JButton("Toggle important");
important.addActionListener(e -> toggleImportant());
JButton play = new JButton("Play");
play.addActionListener(e -> previewSelected());
JButton defaults = new JButton("Reset");
defaults.setToolTipText("Restore the default notification settings");
defaults.addActionListener(e -> {
working.resetToDefaults();
tableModel.fireTableDataChanged();
});
buttons.add(play);
buttons.add(important);
buttons.add(defaults);
p.add(hint, BorderLayout.CENTER);
p.add(buttons, BorderLayout.EAST);
return p;
}
private JTable buildTable() {
JTable t = new JTable(tableModel);
t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
t.setShowGrid(false);
t.setTableHeader(null);
t.setRowHeight(Math.max(t.getRowHeight(), t.getFontMetrics(t.getFont()).getHeight() + 6));
t.getColumnModel().getColumn(0).setMaxWidth(TOGGLE_COLUMN_WIDTH);
t.getColumnModel().getColumn(0).setMinWidth(TOGGLE_COLUMN_WIDTH);
t.getColumnModel().getColumn(0).setCellRenderer(new ToggleRenderer(t.getDefaultRenderer(Boolean.class)));
t.getColumnModel().getColumn(1).setCellRenderer(new ActionRenderer());
t.setPreferredScrollableViewportSize(new Dimension(360, 260));
t.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) previewSelected();
}
@Override
public void mousePressed(MouseEvent e) {
showMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
showMenu(e);
}
private void showMenu(MouseEvent e) {
if (!e.isPopupTrigger()) return;
int row = t.rowAtPoint(e.getPoint());
if (row < 0 || rows.get(row).isHeading()) return;
t.setRowSelectionInterval(row, row);
contextMenu(rows.get(row).event).show(t, e.getX(), e.getY());
}
});
return t;
}
private JPopupMenu contextMenu(SoundEvent event) {
JPopupMenu menu = new JPopupMenu();
JMenuItem important = new JMenuItem(working.isImportant(event) ? "Mark as Unimportant" : "Mark as Important");
important.addActionListener(e -> toggleImportant());
JMenuItem enabled = new JMenuItem(working.isEnabled(event) ? "Turn sound off" : "Turn sound on");
enabled.addActionListener(e -> {
working.setEnabled(event, !working.isEnabled(event));
tableModel.fireTableRowsUpdated(0, rows.size() - 1);
});
JMenuItem play = new JMenuItem("Play sound");
play.addActionListener(e -> sounds.preview(event));
menu.add(play);
menu.addSeparator();
menu.add(enabled);
menu.add(important);
return menu;
}
// ---- actions ----
private void onPackSelected() {
SoundPack pack = (SoundPack) packCombo.getSelectedItem();
sounds.setPack(pack);
packInfo.setText(pack == null ? "No sound packs found"
: "by " + (pack.author().isEmpty() ? "unknown" : pack.author())
+ (pack.version().isEmpty() ? "" : ", version " + pack.version()));
tableModel.fireTableDataChanged();
}
private void reloadPacks() {
sounds.reload();
packCombo.removeAllItems();
for (SoundPack pack : sounds.availablePacks()) {
packCombo.addItem(pack);
}
packCombo.setSelectedItem(sounds.pack());
onPackSelected();
}
private void browseForPackFolder() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setDialogTitle("Folder containing sound packs");
String current = packDirField.getText().trim();
if (!current.isEmpty()) chooser.setCurrentDirectory(new File(current));
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
packDirField.setText(chooser.getSelectedFile().getAbsolutePath());
settings.soundPackDir = packDirField.getText();
reloadPacks();
}
private SoundEvent selectedEvent() {
int row = table.getSelectedRow();
return row < 0 || rows.get(row).isHeading() ? null : rows.get(row).event;
}
private void previewSelected() {
SoundEvent event = selectedEvent();
if (event != null) sounds.preview(event);
}
private void toggleImportant() {
SoundEvent event = selectedEvent();
if (event == null) return;
working.setImportant(event, !working.isImportant(event));
tableModel.fireTableRowsUpdated(table.getSelectedRow(), table.getSelectedRow());
}
private void buildRows() {
for (SoundEvent.Category category : SoundEvent.Category.values()) {
rows.add(new Row(category, null));
for (SoundEvent event : SoundEvent.values()) {
if (event.category() == category) rows.add(new Row(category, event));
}
}
}
private static void addRow(JPanel p, GridBagConstraints c, int row, JLabel label, Component field) {
c.gridx = 0;
c.gridy = row;
c.weightx = 0;
p.add(label, c);
c.gridx = 1;
c.weightx = 1;
p.add(field, c);
}
// ---- table plumbing ----
private final class EventTableModel extends AbstractTableModel {
@Override
public int getRowCount() {
return rows.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public Class> getColumnClass(int column) {
return column == 0 ? Boolean.class : String.class;
}
@Override
public Object getValueAt(int rowIndex, int column) {
Row row = rows.get(rowIndex);
if (column == 0) return row.isHeading() ? Boolean.FALSE : working.isEnabled(row.event);
return row.isHeading() ? row.heading.label() : row.event.label();
}
@Override
public boolean isCellEditable(int rowIndex, int column) {
return column == 0 && !rows.get(rowIndex).isHeading();
}
@Override
public void setValueAt(Object value, int rowIndex, int column) {
Row row = rows.get(rowIndex);
if (column == 0 && !row.isHeading()) {
working.setEnabled(row.event, Boolean.TRUE.equals(value));
}
}
}
/** Only actions get a checkbox; a category heading spans an empty cell. */
private final class ToggleRenderer implements javax.swing.table.TableCellRenderer {
private final javax.swing.table.TableCellRenderer checkBox;
private final DefaultTableCellRenderer blank = new DefaultTableCellRenderer();
ToggleRenderer(javax.swing.table.TableCellRenderer checkBox) {
this.checkBox = checkBox;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean selected,
boolean focused, int rowIndex, int column) {
javax.swing.table.TableCellRenderer delegate = rows.get(rowIndex).isHeading() ? blank : checkBox;
return delegate.getTableCellRendererComponent(table,
delegate == blank ? "" : value, selected, focused, rowIndex, column);
}
}
/** Headings stand out, important actions are bold, unmapped ones are greyed. */
private final class ActionRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean selected,
boolean focused, int rowIndex, int column) {
super.getTableCellRendererComponent(table, value, selected, focused, rowIndex, column);
Row row = rows.get(rowIndex);
Font base = table.getFont();
// The foreground is set on every row: this renderer remembers the last
// unselected colour it was given, so a greyed row would tint the rest.
if (row.isHeading()) {
setFont(base.deriveFont(Font.BOLD));
setForeground(selected ? table.getSelectionForeground() : table.getForeground());
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
setToolTipText(null);
return this;
}
setBorder(BorderFactory.createEmptyBorder(2, 18, 2, 2));
setFont(base.deriveFont(working.isImportant(row.event) ? Font.BOLD : Font.PLAIN));
boolean silent = !sounds.hasSound(row.event);
setForeground(selected ? table.getSelectionForeground()
: silent ? java.awt.Color.GRAY : table.getForeground());
setToolTipText(silent ? "This sound pack has no sound for this action" : null);
return this;
}
}
}