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; } } }