package chatty.gui.components; import chatty.gui.MouseClickedListener; import chatty.gui.StyleManager; import chatty.gui.StyleServer; import chatty.gui.MainGui; import chatty.User; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.textpane.ChannelTextPane; import chatty.gui.components.textpane.Message; import chatty.util.StringUtil; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.swing.AbstractAction; import javax.swing.InputMap; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; /** * A single channel window, combining styled text pane, userlist and input box. * * @author tduva */ public class Channel extends JPanel { public enum Type { NONE, CHANNEL, WHISPER, SPECIAL } private static final int DIVIDER_SIZE = 5; private final ChannelEditBox input; private final ChannelTextPane text; private final UserList users; private final JSplitPane mainPane; private final JScrollPane userlist; private final JScrollPane west; private final StyleServer styleManager; private final MainGui main; private Type type; private boolean userlistEnabled = true; private int previousUserlistWidth; private int userlistMinWidth; private String name; public Channel(final String name, Type type, MainGui main, StyleManager styleManager, ContextMenuListener contextMenuListener) { this.setLayout(new BorderLayout()); this.styleManager = styleManager; this.name = name; this.main = main; this.type = type; // Text Pane text = new ChannelTextPane(main,styleManager); text.setContextMenuListener(contextMenuListener); setTextPreferredSizeTemporarily(); west = new JScrollPane(text); text.setScrollPane(west); //System.out.println(west.getVerticalScrollBarPolicy()); //System.out.println(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); west.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); // PageUp/Down hotkeys / Scrolling InputMap westScrollInputMap = west.getInputMap(WHEN_IN_FOCUSED_WINDOW); westScrollInputMap.put(KeyStroke.getKeyStroke("PAGE_UP"), "pageUp"); west.getActionMap().put("pageUp", new ScrollAction("pageUp", west.getVerticalScrollBar())); westScrollInputMap.put(KeyStroke.getKeyStroke("PAGE_DOWN"), "pageDown"); west.getActionMap().put("pageDown", new ScrollAction("pageDown", west.getVerticalScrollBar())); west.getVerticalScrollBar().setUnitIncrement(40); // User list users = new UserList(contextMenuListener, main.getUserListener()); updateUserlistSettings(); userlist = new JScrollPane(users); // Split Pane mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,west,userlist); mainPane.setResizeWeight(1); mainPane.setDividerSize(DIVIDER_SIZE); // Text input input = new ChannelEditBox(40); input.addActionListener(main.getActionListener()); input.setCompletionServer(new InputCompletionServer()); // Add components add(mainPane, BorderLayout.CENTER); add(input, BorderLayout.SOUTH); input.requestFocusInWindow(); setStyles(); input.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { String name = Channel.this.name; if (onceOffEditListener != null && !name.isEmpty()) { onceOffEditListener.edited(name); onceOffEditListener = null; } } @Override public void removeUpdate(DocumentEvent e) { } @Override public void changedUpdate(DocumentEvent e) { } }); } public void cleanUp() { text.cleanUp(); input.cleanUp(); } public void setType(Type type) { this.type = type; } public Type getType() { return type; } public void setScrollbarAlways(boolean always) { west.setVerticalScrollBarPolicy(always ? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS : JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); } public void setMouseClickedListener(MouseClickedListener listener) { text.setMouseClickedListener(listener); } @Override public String getName() { return name; } /** * Gets the name of the stream (without leading #) if it is a stream channel * (and thus has a leading #) ;) * @return */ public String getStreamName() { if (name.startsWith("#")) { return name.substring(1); } return null; } @Override public void setName(String name) { this.name = name; refreshBufferSize(); } public void addUser(User user) { users.addUser(user); } public void removeUser(User user) { users.removeUser(user); } public void updateUser(User user) { users.updateUser(user); } public void resortUserlist() { users.resort(); } public void clearUsers() { users.clearUsers(); } public int getNumUsers() { return users.getNumUsers(); } public final void updateUserlistSettings() { users.setDisplayNamesMode(main.getSettings().getLong("displayNamesModeUserlist")); } private class InputCompletionServer implements AutoCompletionServer { private final UserSorterNew userSorterNew = new UserSorterNew(); private final UserSorterAlphabetic userSorterAlphabetical = new UserSorterAlphabetic(); private final Set<String> commands = new TreeSet<>(Arrays.asList(new String[]{ "subscribers", "subscribersOff", "timeout", "ban", "unban", "host", "unhost", "clear", "mods", "part", "close", "reconnect", "slow", "slowOff", "r9k", "r9koff", "emoteonly", "emoteonlyoff", "connection", "uptime", "dir", "wdir", "openDir", "openWdir", "releaseInfo", "openBackupDir", "clearChat", "refresh", "changetoken", "testNotification", "server", "set", "add", "clearSetting", "remove", "customCompletion", "clearStreamChat", "getStreamChatSize", "setStreamChatSize", "streamChatTest", "openStreamChat", "customEmotes", "reloadCustomEmotes", "addStreamHighlight", "openStreamHighlights", "ignore", "unignore", "ignoreWhisper", "unignoreWhisper", "ignoreChat", "unignoreChat", "follow", "unfollow", "ffzws", "followers", "followersoff", "setcolor", "untimeout" })); private final Set<String> prefixesPreferUsernames = new HashSet<>(Arrays.asList(new String[]{ "/ban ", "/to ", "/setname ", "/resetname ", "/timeout ", "/host ", "/unban ", "/ignore ", "/unignore ", "/ignoreChat ", "/unignoreChat ", "/ignoreWhisper ", "/unignoreWhisper ", "/follow ", "/unfollow ", "/untimeout " })); private void updateSettings() { input.setCompletionMaxItemsShown((int) main.getSettings().getLong("completionMaxItemsShown")); input.setCompletionShowPopup(main.getSettings().getBoolean("completionShowPopup")); input.setCompleteToCommonPrefix(main.getSettings().getBoolean("completionCommonPrefix")); } @Override public CompletionItems getCompletionItems(String type, String prefix, String search) { updateSettings(); search = search.toLowerCase(Locale.ENGLISH); if (type == null) { return getRegularCompletionItems(prefix, search); } else if (type.equals("special")) { return getSpecialItems(prefix, search); } return new CompletionItems(); } private CompletionItems getRegularCompletionItems(String prefix, String search) { List<String> items; if (prefix.startsWith("/") && (prefix.equals("/set ") || prefix.equals("/get ") || prefix.equals("/add ") || prefix.equals("/remove ") || prefix.equals("/clearSetting ") || prefix.equals("/reset "))) { items = filterCompletionItems(main.getSettingNames(), search); input.setCompleteToCommonPrefix(true); } else if (prefix.equals("/")) { items = filterCompletionItems(commands, search); } else { boolean preferUsernames = prefixesPreferUsernames.contains(prefix) && main.getSettings().getBoolean("completionPreferUsernames"); return getCompletionItemsNames(search, preferUsernames); } return new CompletionItems(items, ""); } private CompletionItems getSpecialItems(String prefix, String search) { if (prefix.endsWith(".")) { return new CompletionItems(getCustomCompletionItems(search), "."); } else { Collection<String> emotes = new LinkedList<>(main.getEmoteNames()); emotes.addAll(main.getEmoteNamesPerStream(getStreamName())); return new CompletionItems(filterCompletionItems(emotes, search), ""); } } private List<String> getCustomCompletionItems(String search) { String result = main.getCustomCompletionItem(search); List<String> list = new ArrayList<>(); if (result != null) { list.add(result); } return list; } private List<String> filterCompletionItems(Collection<String> data, String search) { List<String> matched = new ArrayList<>(); for (String name : data) { if (name.toLowerCase().startsWith(search)) { matched.add(name); } } Collections.sort(matched); return matched; } private CompletionItems getCompletionItemsNames(String search, boolean preferUsernames) { List<User> matchedUsers = new ArrayList<>(); Set<User> regularMatched = new HashSet<>(); Set<User> customMatched = new HashSet<>(); Set<User> localizedMatched = new HashSet<>(); for (User user : users.getData()) { boolean matched = false; if (user.getName().startsWith(search)) { matched = true; regularMatched.add(user); } if (!user.hasRegularDisplayNick() && user.getDisplayNick().toLowerCase(Locale.ROOT).startsWith(search)) { matched = true; localizedMatched.add(user); } if (user.hasCustomNickSet() && user.getCustomNick().toLowerCase(Locale.ROOT).startsWith(search)) { matched = true; customMatched.add(user); } if (matched) { matchedUsers.add(user); } } switch (main.getSettings().getString("completionSorting")) { case "predictive": Collections.sort(matchedUsers, userSorterNew); break; case "alphabetical": Collections.sort(matchedUsers, userSorterAlphabetical); break; default: Collections.sort(matchedUsers); } boolean includeAllNameTypes = main.getSettings().getBoolean("completionAllNameTypes"); boolean includeAllNameTypesRestriction = main.getSettings().getBoolean("completionAllNameTypesRestriction"); List<String> nicks = new ArrayList<>(); Map<String, String> info = new HashMap<>(); for (User user : matchedUsers) { if (includeAllNameTypes && (!includeAllNameTypesRestriction || matchedUsers.size() <= 2)) { if (customMatched.contains(user) && !preferUsernames) { nicks.add(user.getCustomNick()); if (!user.hasRegularDisplayNick()) { nicks.add(user.getDisplayNick()); } if (user.hasCustomNickSet() && !user.getCustomNick().equalsIgnoreCase(user.getRegularDisplayNick())) { nicks.add(user.getRegularDisplayNick()); } } else if (localizedMatched.contains(user) && !preferUsernames) { nicks.add(user.getDisplayNick()); if (user.hasCustomNickSet() && !user.getCustomNick().equalsIgnoreCase(user.getRegularDisplayNick())) { nicks.add(user.getCustomNick()); } nicks.add(user.getRegularDisplayNick()); } else { nicks.add(user.getRegularDisplayNick()); if (!user.hasRegularDisplayNick()) { nicks.add(user.getDisplayNick()); } if (user.hasCustomNickSet() && !user.getCustomNick().equalsIgnoreCase(user.getRegularDisplayNick())) { nicks.add(user.getCustomNick()); } } } else { if (regularMatched.contains(user) || preferUsernames) { nicks.add(user.getRegularDisplayNick()); } if (localizedMatched.contains(user) && !preferUsernames) { nicks.add(user.getDisplayNick()); } if (customMatched.contains(user) && !preferUsernames) { nicks.add(user.getCustomNick()); } } if (!user.hasRegularDisplayNick()) { info.put(user.getDisplayNick(), user.getRegularDisplayNick()); info.put(user.getRegularDisplayNick(), user.getDisplayNick()); } if (user.hasCustomNickSet()) { info.put(user.getCustomNick(), user.getRegularDisplayNick()); info.put(user.getRegularDisplayNick(), user.getCustomNick()); } // if (!user.hasRegularDisplayNick()) { // if (localizedMatched.contains(user) && !preferUsernames) { // nicks.add(user.getDisplayNick()); // if (localizedBoth) { // nicks.add(user.getRegularDisplayNick()); // } // } else { // nicks.add(user.getRegularDisplayNick()); // if (localizedBoth) { // nicks.add(user.getDisplayNick()); // } // } // info.put(user.getDisplayNick(), user.getRegularDisplayNick()); // info.put(user.getRegularDisplayNick(), user.getDisplayNick()); // } else { // nicks.add(user.getRegularDisplayNick()); // if (!user.hasRegularDisplayNick()) { // info.put(user.getRegularDisplayNick(), user.getDisplayNick()); // } // } } return new CompletionItems(nicks, info, ""); } private class UserSorterNew implements Comparator<User> { @Override public int compare(User o1, User o2) { int s1 = o1.getActivityScore(); int s2 = o2.getActivityScore(); //System.out.println(o1+" "+s1+" "+o2+" "+s2); if (s1 == s2) { return o1.compareTo(o2); } else if (s1 > s2) { return -1; } return 1; } } private class UserSorterAlphabetic implements Comparator<User> { @Override public int compare(User o1, User o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } } } public ChannelEditBox getInput() { return input; } public String getInputText() { return input.getText(); } @Override public boolean requestFocusInWindow() { // Invoke later, because otherwise it wouldn't get focus for some // reason. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { //System.out.println("requesting focus for " + name); input.requestFocusInWindow(); } }); return input.requestFocusInWindow(); } // Messages public boolean search(String searchText) { return text.search(searchText); } public void resetSearch() { text.resetSearch(); } public void printLine(String line) { text.printLine(line); } public void userBanned(User user, long duration, String reason, String id) { text.userBanned(user, duration, reason, id); } public void printCompact(String type, User user) { text.printCompact(type, user); } public void printMessage(Message message) { text.printMessage(message); } // Style public void refreshStyles() { text.refreshStyles(); setStyles(); } private void setStyles() { input.setFont(styleManager.getFont()); input.setBackground(styleManager.getColor("inputBackground")); input.setCaretColor(styleManager.getColor("inputForeground")); input.setForeground(styleManager.getColor("inputForeground")); users.setBackground(styleManager.getColor("background")); users.setForeground(styleManager.getColor("foreground")); refreshBufferSize(); } private void refreshBufferSize() { Long bufferSize = (Long)main.getSettings().mapGet("bufferSizes", StringUtil.toLowerCase(name)); text.setBufferSize(bufferSize != null ? bufferSize.intValue() : -1); } public void clearChat() { text.clearAll(); } /** * Insert text into the input box at the current caret position. * * @param text * @param withSpace * @throws NullPointerException if the text is null */ public void insertText(String text, boolean withSpace) { input.insertAtCaret(text, withSpace); } private static class ScrollAction extends AbstractAction { private final String action; private final JScrollBar scrollbar; ScrollAction(String action, JScrollBar scrollbar) { this.scrollbar = scrollbar; this.action = action; } @Override public void actionPerformed(ActionEvent e) { int now = scrollbar.getValue(); int height = scrollbar.getVisibleAmount(); height = height - height / 10; int newValue = 0; switch (action) { case "pageUp": newValue = now - height; break; case "pageDown": newValue = now + height; break; } scrollbar.setValue(newValue); } } public final void setUserlistWidth(int width, int minWidth) { userlist.setPreferredSize(new Dimension(width, 10)); userlist.setMinimumSize(new Dimension(minWidth, 0)); userlistMinWidth = minWidth; } /** * Setting the preferred size to 0, so the text pane doesn't influence the * size of the userlist. Setting it back later so it doesn't flicker when * being scrolled up (and possibly other issues). This is an ugly hack, but * I don't know enough about this to find a proper solution. */ private void setTextPreferredSizeTemporarily() { text.setPreferredSize(new Dimension(0, 0)); Timer t = new Timer(5000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { text.setPreferredSize(null); } }); t.setRepeats(false); t.start(); } /** * Toggle visibility for the text input box. */ public final void toggleInput() { input.setVisible(!input.isVisible()); revalidate(); } /** * Enable or disable the userlist. As with setting the initial size, this * requires some hacky stuff to get the size back correctly. * * @param enable */ public final void setUserlistEnabled(boolean enable) { if (enable == userlistEnabled) { return; } if (enable) { userlist.setVisible(true); mainPane.setDividerSize(DIVIDER_SIZE); setUserlistWidth(previousUserlistWidth, userlistMinWidth); setTextPreferredSizeTemporarily(); mainPane.setDividerLocation(-1); } else { previousUserlistWidth = userlist.getWidth(); userlist.setVisible(false); mainPane.setDividerSize(0); } userlistEnabled = enable; revalidate(); } /** * Toggle the userlist. */ public final void toggleUserlist() { setUserlistEnabled(!userlistEnabled); } public void selectPreviousUser() { text.selectPreviousUser(); } public void selectNextUser() { text.selectNextUser(); } public void selectNextUserExitAtBottom() { text.selectNextUserExitAtBottom(); } public void exitUserSelection() { text.exitUserSelection(); } public void toggleUserSelection() { text.toggleUserSelection(); } public User getSelectedUser() { return text.getSelectedUser(); } @Override public String toString() { return String.format("%s '%s'", type, name); } private OnceOffEditListener onceOffEditListener; public void setOnceOffEditListener(OnceOffEditListener listener) { onceOffEditListener = listener; } public interface OnceOffEditListener { public void edited(String channel); } }