Global hotkeys, in TeamSpeak's own shape
Bindings are captured system-wide rather than only while the window has focus: X11's RECORD extension where the X server sees every key, and a /dev/input reader as the Wayland fallback. Any key can act as a modifier, mouse buttons included, as TS3 allows. The action catalogue, its three categories and the "advanced actions" split are reverse-engineered from the original client; actions this client cannot perform are listed but greyed out. Push-to-talk becomes one of these hotkeys, so the old focus-bound pushToTalkKey setting is gone and the Voice Activation button edits that binding instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.hotkey.Hotkey;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Font;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The options dialog's hotkey tab: the list of bindings with the buttons to add, edit
|
||||
* and remove them. Edits happen on a working copy and only reach the running engine
|
||||
* when the dialog is applied.
|
||||
*/
|
||||
final class HotkeysPanel extends JPanel {
|
||||
|
||||
private static final String[] COLUMNS = {"Action", "Hotkey", "Trigger", "Active server", "On"};
|
||||
|
||||
private final HotkeyService service;
|
||||
private final List<Hotkey> working = new ArrayList<>();
|
||||
private final Model model = new Model();
|
||||
private final JTable table = new JTable(model);
|
||||
|
||||
HotkeysPanel(HotkeyService service) {
|
||||
super(new BorderLayout(6, 6));
|
||||
this.service = service;
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
synchronized (service.all()) {
|
||||
for (Hotkey h : service.all()) working.add(h.copy());
|
||||
}
|
||||
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(table.getRowHeight() + 4);
|
||||
table.getColumnModel().getColumn(0).setPreferredWidth(240);
|
||||
table.getColumnModel().getColumn(1).setPreferredWidth(150);
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) edit();
|
||||
}
|
||||
});
|
||||
|
||||
JButton add = new JButton("Add…");
|
||||
JButton edit = new JButton("Edit…");
|
||||
JButton remove = new JButton("Remove");
|
||||
add.addActionListener(e -> add());
|
||||
edit.addActionListener(e -> edit());
|
||||
remove.addActionListener(e -> remove());
|
||||
|
||||
JPanel buttons = new JPanel();
|
||||
buttons.add(add);
|
||||
buttons.add(edit);
|
||||
buttons.add(remove);
|
||||
|
||||
JLabel status = new JLabel(service.status());
|
||||
status.setFont(status.getFont().deriveFont(Font.ITALIC, status.getFont().getSize2D() - 1f));
|
||||
|
||||
add(new JScrollPane(table), BorderLayout.CENTER);
|
||||
JPanel south = new JPanel(new BorderLayout());
|
||||
south.add(buttons, BorderLayout.WEST);
|
||||
south.add(status, BorderLayout.SOUTH);
|
||||
add(south, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void add() {
|
||||
HotkeyDialog dlg = new HotkeyDialog(owner(), service, null);
|
||||
dlg.setVisible(true);
|
||||
if (!dlg.isConfirmed()) return;
|
||||
working.add(dlg.result());
|
||||
model.fireTableDataChanged();
|
||||
}
|
||||
|
||||
private void edit() {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
HotkeyDialog dlg = new HotkeyDialog(owner(), service, working.get(row));
|
||||
dlg.setVisible(true);
|
||||
if (!dlg.isConfirmed()) return;
|
||||
working.set(row, dlg.result());
|
||||
model.fireTableRowsUpdated(row, row);
|
||||
}
|
||||
|
||||
private void remove() {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
working.remove(row);
|
||||
model.fireTableDataChanged();
|
||||
}
|
||||
|
||||
private Window owner() {
|
||||
return SwingUtilities.getWindowAncestor(this);
|
||||
}
|
||||
|
||||
/** Commits the edited list to the engine and to disk. */
|
||||
void apply() {
|
||||
service.replaceAll(working);
|
||||
}
|
||||
|
||||
/** Picks the stored bindings up again after something else changed them. */
|
||||
void reload() {
|
||||
working.clear();
|
||||
synchronized (service.all()) {
|
||||
for (Hotkey h : service.all()) working.add(h.copy());
|
||||
}
|
||||
model.fireTableDataChanged();
|
||||
}
|
||||
|
||||
private final class Model extends AbstractTableModel {
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return working.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return COLUMNS.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return COLUMNS[column];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int column) {
|
||||
return column >= 3 ? Boolean.class : String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return column == 4 || (column == 3 && working.get(row).isServerScoped());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int row, int column) {
|
||||
Hotkey h = working.get(row);
|
||||
return switch (column) {
|
||||
case 0 -> h.path();
|
||||
case 1 -> service.display(h.combo);
|
||||
case 2 -> h.action.momentary() ? "While held" : h.trigger.label();
|
||||
case 3 -> h.isServerScoped() && h.activeServerOnly;
|
||||
default -> h.enabled;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueAt(Object value, int row, int column) {
|
||||
Hotkey h = working.get(row);
|
||||
if (column == 3) {
|
||||
h.activeServerOnly = Boolean.TRUE.equals(value);
|
||||
} else if (column == 4) {
|
||||
h.enabled = Boolean.TRUE.equals(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user