The look-and-feel is FlatLaf now, chosen light or dark on the new Design tab and remembered in the settings. Theme keeps only the colours TeamSpeak gives meaning to (talking, away, muted, channel and server names) and takes its surfaces and fonts from the look-and-feel, so a switch recolours the whole window, chat logs included: the HTML panes share one themed stylesheet and re-parse their content when the theme changes. Also fixes the channel tree not scrolling: the tree listened for the mouse wheel to re-check its hover row, and AWT only forwards a wheel event to the enclosing scroll pane when the component under the pointer has no wheel listener of its own. The hover row now follows the viewport instead, which also covers scrollbar drags and keyboard scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.9 KiB
Java
149 lines
5.9 KiB
Java
package com.ts3client.ui;
|
|
|
|
import javax.swing.BorderFactory;
|
|
import javax.swing.Box;
|
|
import javax.swing.BoxLayout;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JSpinner;
|
|
import javax.swing.SpinnerNumberModel;
|
|
import java.awt.Color;
|
|
import java.awt.GridBagConstraints;
|
|
import java.awt.GridBagLayout;
|
|
import java.awt.Insets;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* The channel editor's "Permissions" tab: the needed-power permissions set on the channel
|
|
* itself, which is the subset of channel permissions TeamSpeak surfaces here.
|
|
*
|
|
* <p>These are not channel properties but permissions, so they are read with
|
|
* {@code channelpermlist} and written with {@code channeladdperm}. A power left at zero is
|
|
* not a permission the channel carries: clearing a spinner back to zero removes the
|
|
* permission again so the channel inherits it, which is how the official client behaves.
|
|
*/
|
|
final class ChannelPermissionsPanel extends JPanel {
|
|
|
|
/** The regular powers, in the order the official dialog lists them. */
|
|
private static final String[][] REGULAR = {
|
|
{"i_channel_needed_join_power", "Join:"},
|
|
{"i_channel_needed_subscribe_power", "Subscribe:"},
|
|
{"i_channel_needed_description_view_power", "Desc. View:"},
|
|
{"i_channel_needed_modify_power", "Modify:"},
|
|
{"i_channel_needed_delete_power", "Delete:"}};
|
|
|
|
private static final String[][] FILE_TRANSFER = {
|
|
{"i_ft_needed_file_browse_power", "Browse:"},
|
|
{"i_ft_needed_file_upload_power", "Upload:"},
|
|
{"i_ft_needed_file_download_power", "Download:"},
|
|
{"i_ft_needed_file_rename_power", "Rename:"},
|
|
{"i_ft_needed_directory_create_power", "Dir. Create:"}};
|
|
|
|
private final Map<String, JSpinner> spinners = new LinkedHashMap<>();
|
|
/** The values the server reported, to tell an edited power from an untouched one. */
|
|
private final Map<String, Integer> original = new LinkedHashMap<>();
|
|
private final JLabel status = new JLabel(" ");
|
|
|
|
ChannelPermissionsPanel() {
|
|
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
|
|
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
|
|
|
JPanel groups = new JPanel();
|
|
groups.setLayout(new BoxLayout(groups, BoxLayout.X_AXIS));
|
|
groups.add(group("Regular Needed Powers", REGULAR));
|
|
groups.add(Box.createHorizontalStrut(8));
|
|
groups.add(group("File Transfer Needed Powers", FILE_TRANSFER));
|
|
groups.setAlignmentX(0f);
|
|
add(groups);
|
|
|
|
status.setAlignmentX(0f);
|
|
status.setBorder(BorderFactory.createEmptyBorder(6, 2, 0, 2));
|
|
add(status);
|
|
add(Box.createVerticalGlue());
|
|
setEditable(false);
|
|
}
|
|
|
|
/** Fills the spinners from a {@code channelpermlist} result and enables editing. */
|
|
void read(Map<String, Integer> permissions) {
|
|
original.clear();
|
|
for (Map.Entry<String, JSpinner> entry : spinners.entrySet()) {
|
|
int value = permissions.getOrDefault(entry.getKey(), 0);
|
|
original.put(entry.getKey(), value);
|
|
entry.getValue().setValue(value);
|
|
}
|
|
setEditable(true);
|
|
setStatus(" ", false);
|
|
}
|
|
|
|
/** Greys the tab out with a reason, e.g. when the server refuses to show the permissions. */
|
|
void showUnavailable(String message) {
|
|
setEditable(false);
|
|
setStatus(message, true);
|
|
}
|
|
|
|
/** The permissions whose power was changed to a non-zero value. */
|
|
Map<String, Integer> changed() {
|
|
Map<String, Integer> changed = new LinkedHashMap<>();
|
|
for (Map.Entry<String, JSpinner> entry : spinners.entrySet()) {
|
|
int value = (Integer) entry.getValue().getValue();
|
|
if (value != 0 && value != original.getOrDefault(entry.getKey(), 0)) {
|
|
changed.put(entry.getKey(), value);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/** The permissions cleared back to zero, which are removed from the channel. */
|
|
List<String> cleared() {
|
|
List<String> cleared = new ArrayList<>();
|
|
for (Map.Entry<String, JSpinner> entry : spinners.entrySet()) {
|
|
int value = (Integer) entry.getValue().getValue();
|
|
if (value == 0 && original.getOrDefault(entry.getKey(), 0) != 0) cleared.add(entry.getKey());
|
|
}
|
|
return cleared;
|
|
}
|
|
|
|
void setStatus(String message, boolean error) {
|
|
status.setText(message == null || message.isEmpty() ? " " : message);
|
|
status.setForeground(error ? Color.RED.darker() : Theme.chatSystem());
|
|
}
|
|
|
|
private void setEditable(boolean editable) {
|
|
for (JSpinner spinner : spinners.values()) spinner.setEnabled(editable);
|
|
}
|
|
|
|
private JPanel group(String title, String[][] permissions) {
|
|
JPanel panel = new JPanel(new GridBagLayout());
|
|
panel.setBorder(BorderFactory.createTitledBorder(title));
|
|
GridBagConstraints c = new GridBagConstraints();
|
|
c.insets = new Insets(3, 4, 3, 4);
|
|
c.anchor = GridBagConstraints.WEST;
|
|
c.fill = GridBagConstraints.HORIZONTAL;
|
|
int row = 0;
|
|
for (String[] permission : permissions) {
|
|
JSpinner spinner = Spinners.compact(new JSpinner(new SpinnerNumberModel(0, 0, 9999, 1)));
|
|
spinners.put(permission[0], spinner);
|
|
c.gridx = 0;
|
|
c.gridy = row;
|
|
c.weightx = 0;
|
|
panel.add(new JLabel(permission[1]), c);
|
|
c.gridx = 1;
|
|
c.weightx = 1;
|
|
c.fill = GridBagConstraints.NONE;
|
|
panel.add(spinner, c);
|
|
c.fill = GridBagConstraints.HORIZONTAL;
|
|
row++;
|
|
}
|
|
// Keeps the rows at the top of the group instead of centred in whatever height
|
|
// the tab happens to give it.
|
|
c.gridx = 0;
|
|
c.gridy = row;
|
|
c.weighty = 1;
|
|
panel.add(Box.createGlue(), c);
|
|
return panel;
|
|
}
|
|
}
|