package chatty.gui.components.settings;
import chatty.Chatty;
import chatty.util.api.usericons.Usericon;
import chatty.util.api.usericons.Usericon.Type;
import chatty.gui.GuiUtil;
import chatty.gui.RegexDocumentFilter;
import chatty.gui.components.LinkLabel;
import chatty.gui.components.LinkLabelListener;
import chatty.util.MiscUtil;
import chatty.util.api.usericons.UsericonFactory;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.text.PlainDocument;
/**
* Table to add/remove/edit usericons (badges).
*
* @author tduva
*/
class UsericonEditor extends TableEditor<Usericon> {
private static final Map<Usericon.Type, String> typeNames;
private final MyItemEditor editor;
public UsericonEditor(JDialog owner, LinkLabelListener linkLabelListener) {
super(SORTING_MODE_MANUAL, false);
editor = new MyItemEditor(owner, linkLabelListener);
setModel(new MyTableModel());
setItemEditor(editor);
setRendererForColumn(1, new IdRenderer(getForeground()));
setRendererForColumn(2, new ImageRenderer());
setRendererForColumn(3, new ChannelRenderer(getForeground()));
}
/**
* Names for the usericon types.
*/
static {
typeNames = new LinkedHashMap<>();
typeNames.put(Usericon.Type.ADDON, "Addon");
typeNames.put(Usericon.Type.MOD, "Moderator");
typeNames.put(Usericon.Type.SUB, "Subscriber");
typeNames.put(Usericon.Type.TURBO, "Turbo");
typeNames.put(Usericon.Type.PRIME, "Prime");
typeNames.put(Usericon.Type.BITS, "Bits");
typeNames.put(Usericon.Type.ADMIN, "Admin");
typeNames.put(Usericon.Type.STAFF, "Staff");
typeNames.put(Usericon.Type.BROADCASTER, "Broadcaster");
typeNames.put(Usericon.Type.GLOBAL_MOD, "Global Moderator");
typeNames.put(Usericon.Type.BOT, "Bot");
typeNames.put(Usericon.Type.TWITCH, "Other Twitch");
}
public void setTwitchBadgeTypes(Set<String> types) {
editor.setTwitchBadgeTypes(types);
}
public void addUsericonOfBadgeType(String idVersion) {
Usericon usericon = UsericonFactory.createCustomIcon(Type.TWITCH, idVersion, "", "", "");
addItem(usericon);
}
private static String getTypeName(Type type) {
return type.label;
}
/**
* The table model defining the columns and what data is returned on which
* column.
*/
private static class MyTableModel extends ListTableModel<Usericon> {
public MyTableModel() {
super(new String[]{"Type","Restriction","Image","Channel"});
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Usericon icon = get(rowIndex);
if (columnIndex == 0) {
if (icon.type == Usericon.Type.TWITCH) {
return "["+icon.getIdAndVersion()+"]";
} else if (!icon.badgeType.isEmpty()) {
return getTypeName(icon.type)+" ["+icon.badgeType+"]";
} else {
return getTypeName(icon.type);
}
} else if (columnIndex == 1) {
return icon;
} else if (columnIndex == 2) {
return icon;
} else if (columnIndex == 3) {
return icon;
}
return null;
}
@Override
public int getSearchColumn(int column) {
return 1;
}
@Override
public String getSearchValueAt(int row, int column) {
return get(row).restriction;
}
@Override
public Class getColumnClass(int columnIndex) {
if (columnIndex >= 1) {
return Usericon.class;
} else if (columnIndex == 2) {
return ImageIcon.class;
} else {
return String.class;
}
}
}
/**
* The renderer for the restriction column, which shows whether the value
* is valid.
*/
private static class IdRenderer extends DefaultTableCellRenderer {
private final Color defaultColor;
IdRenderer(Color defaultColor) {
this.defaultColor = defaultColor;
}
@Override
public void setValue(Object value) {
if (value == null) {
setText(null);
return;
}
Usericon icon = (Usericon) value;
if (icon.matchType == Usericon.MatchType.UNDEFINED) {
setText(icon.restriction+" (error)");
setForeground(Color.red);
} else {
setText(icon.restrictionValue
+(icon.first ? " (first)" : "")
+(icon.stop ? " (stop)" : "")
+(!icon.badgeTypeRestriction.isEmpty() ? " ["+icon.badgeTypeRestriction.toString()+"]" : "")
);
setForeground(defaultColor);
}
}
}
/**
* The renderer for the image column, displaying the image or the reference
* to another icon if the filename starts with "$".
*/
private static class ImageRenderer extends DefaultTableCellRenderer {
ImageRenderer() {
setHorizontalAlignment(SwingConstants.CENTER);
}
@Override
public void setValue(Object value) {
if (value == null) {
setIcon(null);
setText(null);
return;
}
Usericon icon = (Usericon) value;
if (icon.fileName != null && icon.fileName.startsWith("$")) {
setIcon(null);
if (icon.fileName.equalsIgnoreCase("$default")) {
setText("Default badge");
} else {
setText(icon.fileName.substring(1));
}
} else if (icon.removeBadge) {
setIcon(null);
setText("No image");
} else {
setIcon(icon.image);
setText(null);
}
}
}
/**
* The renderer for the channel restriction column, showing if the channel
* value is valid and displaying the "NOT" if used.
*/
private static class ChannelRenderer extends DefaultTableCellRenderer {
private final Color defaultColor;
ChannelRenderer(Color defaultColor) {
this.defaultColor = defaultColor;
}
@Override
public void setValue(Object value) {
if (value == null) {
setText(null);
return;
}
Usericon icon = (Usericon) value;
if (!icon.channelRestriction.isEmpty() && icon.channel.isEmpty()) {
setText(icon.channelRestriction+" (error)");
setForeground(Color.red);
} else {
setText((icon.channelInverse ? "NOT " : "") + icon.channel);
setForeground(defaultColor);
}
}
}
/**
* The editor for a single usericon, which does the most work here, having
* to load a list of icons for use, creating the icon when selected,
* updating the preview and so on.
*/
private static class MyItemEditor implements ItemEditor<Usericon> {
private static final String HELP = "<html><body style='width:220px;'>"
+ "Define which badge to target and what image to replace it "
+ "with. [help-settings:CustomUsericons More Information..]";
private static final String ERROR_LOADING_IMAGE = "Error loading image.";
private final JDialog dialog;
private final GenericComboSetting<String> fileName;
private final GenericComboSetting<Type> type;
private final JTextField restriction = new JTextField(16);
private final JTextField stream = new JTextField();
private final GenericComboSetting<String> idVersion;
private final JButton okButton = new JButton("Done");
private final JButton cancelButton = new JButton("Cancel");
private final JButton openDir = new JButton("Open dir");
private final JButton scanDir = new JButton("Rescan");
private final JPanel folderPanel;
private final JLabel scanResult = new JLabel(ERROR_LOADING_IMAGE);
private final JLabel preview = new JLabel();
private boolean save;
private Usericon currentIcon;
public MyItemEditor(Window owner, LinkLabelListener linkLabelListener) {
dialog = new JDialog(owner);
dialog.setLayout(new GridBagLayout());
dialog.setResizable(false);
dialog.setModal(true);
type = new GenericComboSetting<>(typeNames);
type.setToolTipText("Choosing a type other than Addon replaces the corresponding default icon.");
type.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (type.getSettingValue() == Usericon.Type.TWITCH) {
idVersion.setEnabled(true);
} else {
idVersion.setEnabled(false);
idVersion.setSettingValue("");
}
}
});
idVersion = new GenericComboSetting<>();
idVersion.setEditable(true);
fileName = new GenericComboSetting<>();
fileName.setEditable(true);
fileName.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("comboBoxChanged")) {
update();
}
}
});
GridBagConstraints gbc;
dialog.add(new LinkLabel(HELP, linkLabelListener), GuiUtil.makeGbc(0, 0, 3, 1));
gbc = GuiUtil.makeGbc(0, 1, 3, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
dialog.add(createMainPanel(), gbc);
folderPanel = createFolderPanel();
folderPanel.setVisible(false);
gbc = GuiUtil.makeGbc(0, 2, 3, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
dialog.add(folderPanel, gbc);
gbc = GuiUtil.makeGbc(0, 6, 1, 1);
dialog.add(new JLabel(), gbc);
gbc = GuiUtil.makeGbc(1, 6, 1, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 0.7;
dialog.add(okButton, gbc);
gbc = GuiUtil.makeGbc(2, 6, 1, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 0.3;
dialog.add(cancelButton, gbc);
ActionListener buttonAction = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == okButton) {
save = true;
}
if (e.getSource() == okButton || e.getSource() == cancelButton) {
dialog.setVisible(false);
}
else if (e.getSource() == openDir) {
MiscUtil.openFolder(new File(Chatty.getImageDirectory()), dialog);
}
else if (e.getSource() == scanDir) {
scanFiles();
}
}
};
okButton.addActionListener(buttonAction);
cancelButton.addActionListener(buttonAction);
openDir.addActionListener(buttonAction);
scanDir.addActionListener(buttonAction);
dialog.pack();
}
private JPanel createFolderPanel() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder("Image Source"));
GridBagConstraints gbc;
gbc = GuiUtil.makeGbc(0, 0, 3, 1, GridBagConstraints.WEST);
panel.add(new JLabel("Chatty looks for image files (.png) in this "
+ "folder: "), gbc);
gbc = GuiUtil.makeGbc(0, 1, 3, 1);
JTextField path = new JTextField(Chatty.getImageDirectory());
path.setEditable(false);
path.setPreferredSize(new Dimension(0, path.getPreferredSize().height));
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
panel.add(path, gbc);
gbc = GuiUtil.makeGbc(0, 2, 1, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
panel.add(scanResult, gbc);
scanDir.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
gbc = GuiUtil.makeGbc(1, 2, 1, 1);
panel.add(scanDir, gbc);
openDir.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
gbc = GuiUtil.makeGbc(2, 2, 1, 1);
panel.add(openDir, gbc);
return panel;
}
private JPanel createMainPanel() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc;
panel.add(new JLabel("Type:"), GuiUtil.makeGbc(0, 1, 1, 1));
gbc = GuiUtil.makeGbc(1, 1, 2, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(type, gbc);
panel.add(new JLabel("ID/Version:"), GuiUtil.makeGbc(0, 2, 1, 1));
gbc = GuiUtil.makeGbc(1, 2, 2, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(idVersion, gbc);
panel.add(new JLabel("Restriction:"), GuiUtil.makeGbc(0, 3, 1, 1));
gbc = GuiUtil.makeGbc(1, 3, 2, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
//id.setColumns(14);
panel.add(restriction, gbc);
panel.add(new JLabel("Channel:"), GuiUtil.makeGbc(0, 4, 1, 1));
gbc = GuiUtil.makeGbc(1, 4, 2, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
//stream.setColumns(14);
panel.add(stream, gbc);
panel.add(new JLabel("Image File:"), GuiUtil.makeGbc(0, 5, 1, 1));
gbc = GuiUtil.makeGbc(1, 5, 2, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(fileName, gbc);
panel.add(new JLabel("Preview:"), GuiUtil.makeGbc(0, 6, 1, 1));
gbc = GuiUtil.makeGbc(1, 6, 1, 1, GridBagConstraints.WEST);
panel.add(preview, gbc);
final JToggleButton sourceInfoButton = new JToggleButton("Image Folder");
sourceInfoButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
folderPanel.setVisible(sourceInfoButton.isSelected());
updateSize();
}
});
sourceInfoButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
gbc = GuiUtil.makeGbc(2, 6, 1, 1);
panel.add(sourceInfoButton, gbc);
return panel;
}
private void update() {
createIcon(true);
updatePreview();
updateOkButton();
}
private void createIcon(boolean preview) {
String file = (String)fileName.getSettingValue();
if (preview) {
currentIcon = UsericonFactory.createCustomIcon(Type.UNDEFINED, null, null, file, null);
} else if (type.getSettingValue() != null) {
currentIcon = UsericonFactory.createCustomIcon(
type.getSettingValue(), idVersion.getSettingValue(),
restriction.getText(), file, stream.getText());
} else {
currentIcon = null;
}
}
private void updateOkButton() {
okButton.setEnabled(currentIcon != null && type.getSettingValue() != null);
}
/**
* Sets the icon preview and the text depending on the state of the
* current icon.
*/
private void updatePreview() {
preview.setText(null);
preview.setIcon(null);
if (currentIcon == null) {
preview.setText("No image.");
} else if (currentIcon.removeBadge) {
preview.setText("No image.");
} else if (currentIcon.fileName.startsWith("$")) {
preview.setText("Ref image.");
} else if (currentIcon.image == null) {
preview.setText(ERROR_LOADING_IMAGE);
} else {
ImageIcon image = currentIcon.image;
preview.setIcon(image);
preview.setText(image.getIconWidth()+"x"+image.getIconHeight());
}
updateSize();
}
private void updateSize() {
dialog.pack();
}
@Override
public Usericon showEditor(Usericon preset, Component c, boolean edit) {
scanFiles();
if (edit) {
dialog.setTitle("Edit item");
} else {
dialog.setTitle("Add item");
}
if (preset != null) {
restriction.setText(preset.restriction);
type.setSettingValue(preset.type);
idVersion.setSettingValue(preset.getIdAndVersion());
fileName.setSettingValue(preset.fileName);
stream.setText(preset.channelRestriction);
currentIcon = preset;
} else {
restriction.setText(null);
type.setSelectedIndex(0);
idVersion.setSettingValue(null);
// Must contain items to set to index 0, which it always should
// due to <no image> and stuff
fileName.setSelectedIndex(0);
stream.setText(null);
currentIcon = null;
}
update();
save = false;
dialog.setLocationRelativeTo(c);
dialog.setVisible(true);
// Modal dialog, so blocks here and stuff can be changed via the GUI
// until the dialog is closed
if (save) {
createIcon(false);
return currentIcon;
}
return null;
}
private void scanFiles() {
File file = new File(Chatty.getImageDirectory());
File[] files = file.listFiles(new ImageFilenameFilter());
String resultText = "";
Object selected = fileName.getSelectedItem();
fileName.removeAllItems();
fileName.add("", "<no image>");
fileName.add("$default", "$default");
if (files == null) {
resultText = "Error scanning folder.";
}
else {
if (files.length == 0) {
resultText = "No files found.";
} else {
resultText = files.length + " files found.";
}
String[] fileNames = new String[files.length];
for (int i = 0; i < files.length; i++) {
fileNames[i] = files[i].getName();
}
Arrays.sort(fileNames);
for (String item : fileNames) {
fileName.add(item);
}
}
fileName.setSelectedItem(selected);
scanResult.setText(resultText);
}
public void setTwitchBadgeTypes(Set<String> types) {
idVersion.clear();
for (String type : types) {
idVersion.add(type);
}
}
}
private static class ImageFilenameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
if (name.endsWith(".png")) {
return true;
}
return false;
}
}
}