package chatty.gui.components.admin; import chatty.gui.GuiUtil; import chatty.gui.MainGui; import chatty.gui.UrlOpener; import chatty.util.api.CommunitiesManager.Community; import chatty.util.api.TwitchApi; import java.awt.Color; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.swing.*; import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.HyperlinkEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; /** * Select a game by manually entering it, searching for it on Twitch or * selecting one from the favorites. * * @author tduva */ public class SelectCommunityDialog extends JDialog { private static final String INFO = "<html><body style='width:340px'>" + "Twitch currently does not offer a search API, so enter the exact " + "name of a community and click 'Search' (or press Enter) to verify it's name."; private final MainGui main; private final TwitchApi api; // General Buttons private final JButton ok = new JButton("Ok"); private final JButton cancel = new JButton("Cancel"); // Game search/fav buttons private final JButton searchButton = new JButton("Search"); private final JButton addToFavoritesButton = new JButton("Favorite"); private final JButton removeFromFavoritesButton = new JButton("Unfavorite"); private final JButton clearSearchButton = new JButton("Clear"); private final JButton openUrl = new JButton("Open URL"); private final JButton top100 = new JButton("Top 100"); // Current info elements private final JLabel searchResultInfo = new JLabel("No search performed yet."); private final JTextField input = new JTextField(30); private final JList<Community> list = new JList<>(); private final DefaultListModel<Community> listData = new DefaultListModel<>(); private final JTextPane description = new JTextPane(); // Current games data seperate from GUI private final Set<Community> favorites = new TreeSet<>(); private final Set<Community> searchResult = new TreeSet<>(); private Community current; private final Timer timer; private long lastSelectionTime; private boolean loading; private Community shouldMaybeRequest; // Whether to use the current game private boolean save; public SelectCommunityDialog(MainGui main, TwitchApi api) { super(main, "Select community", true); setResizable(true); this.main = main; this.api = api; setLayout(new GridBagLayout()); list.setModel(listData); list.setVisibleRowCount(12); list.setCellRenderer(new ListRenderer()); GridBagConstraints gbc; Action doneAction = new DoneAction(); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "useSelectedGame"); list.getActionMap().put("useSelectedGame", doneAction); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0), "toggleFavorite"); list.getActionMap().put("toggleFavorite", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { toggleFavorite(); } }); timer = new Timer(100, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (System.currentTimeMillis() - lastSelectionTime > 500) { loadCurrentInfo(); } } }); timer.setRepeats(true); gbc = makeGbc(0,0,5,1); add(new JLabel(INFO), gbc); gbc = makeGbc(0,1,2,1); gbc.fill = GridBagConstraints.HORIZONTAL; add(input, gbc); searchButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc = makeGbc(2,1,1,1); gbc.fill = GridBagConstraints.HORIZONTAL; add(searchButton, gbc); gbc = makeGbc(0,2,3,1); gbc.anchor = GridBagConstraints.WEST; gbc.insets = new Insets(2,4,4,4); gbc.fill = GridBagConstraints.HORIZONTAL; add(searchResultInfo, gbc); gbc = makeGbc(2, 2, 1, 1); clearSearchButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.insets = new Insets(2,4,4,4); add(clearSearchButton, gbc); gbc = makeGbc(4, 2, 1, 1); openUrl.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc.insets = new Insets(2,4,4,4); gbc.anchor = GridBagConstraints.EAST; add(openUrl, gbc); gbc = makeGbc(3, 1, 1, 1); top100.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc.insets = new Insets(2,4,4,4); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.SOUTHEAST; add(top100, gbc); gbc = makeGbc(0, 4, 2, 1); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 0.1; gbc.weighty = 1; add(new JScrollPane(list), gbc); gbc = makeGbc(2, 4, 3, 1); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 0.9; gbc.weighty = 1; description.setEditable(false); description.setContentType("text/html"); add(new JScrollPane(description), gbc); gbc = makeGbc(0,5,1,1); addToFavoritesButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 0.5; add(addToFavoritesButton, gbc); removeFromFavoritesButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); gbc = makeGbc(1,5,1,1); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 0.5; add(removeFromFavoritesButton, gbc); ok.setMnemonic(KeyEvent.VK_ENTER); gbc = makeGbc(0,6,3,1); gbc.weightx = 0.5; gbc.fill = GridBagConstraints.HORIZONTAL; add(ok, gbc); cancel.setMnemonic(KeyEvent.VK_C); gbc = makeGbc(3,6,2,1); gbc.fill = GridBagConstraints.HORIZONTAL; add(cancel, gbc); ActionListener actionListener = new MyActionListener(); searchButton.addActionListener(actionListener); openUrl.addActionListener(actionListener); top100.addActionListener(actionListener); input.addActionListener(actionListener); ok.addActionListener(actionListener); cancel.addActionListener(actionListener); list.addListSelectionListener(new MyListSelectionListener()); list.addMouseListener(new ListClickListener()); addToFavoritesButton.addActionListener(actionListener); removeFromFavoritesButton.addActionListener(actionListener); clearSearchButton.addActionListener(actionListener); input.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { updateOkButton(); } @Override public void removeUpdate(DocumentEvent e) { updateOkButton(); } @Override public void changedUpdate(DocumentEvent e) { updateOkButton(); } }); description.addHyperlinkListener(e -> { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { String url = e.getURL().toString(); UrlOpener.openUrlPrompt(SelectCommunityDialog.this, url, true); } }); updateFavoriteButtons(); pack(); setMinimumSize(getSize()); } /** * Open the dialog with the given game preset. * * @param preset * @return The name of the game to use, or {@code null} if the game should * not be changed */ public Community open(Community preset) { timer.start(); setCurrent(preset); loadFavorites(); if (preset != Community.EMPTY && !favorites.contains(preset)) { searchResult.add(preset); } update(); save = false; setVisible(true); // Blocking dialog, so stuff can change in the meantime timer.stop(); if (save) { return current != null ? current : Community.EMPTY; } return null; } /** * Closes the dialog, using the current game. */ private void useGameAndClose() { save = true; setVisible(false); } private void setCurrent(Community c) { if (c == null) { current = null; input.setText(null); } else { current = c; input.setText(c.toString()); } updateInfo(); } private void updateInfo() { if (current == null || current.isValid()) { description.setText("Nothing to see here."); return; } Community maybe = api.getCachedCommunityInfo(current.getId()); if (maybe != null) { description.setText(String.format( "<html><body style='font: sans-serif 9pt;padding:3px;'>%s<h2 style='font-size:12pt;border-bottom: 1px solid black'>Rules</h2>%s", maybe.getSummary(), maybe.getRules())); description.setCaretPosition(0); } else { description.setText("Loading.."); lastSelectionTime = System.currentTimeMillis(); shouldMaybeRequest = current; } } private void loadCurrentInfo() { if (!loading && current != null && current == shouldMaybeRequest) { final Community forRequest = current; // This should only be done if cached info could not be found loading = true; shouldMaybeRequest = null; // The request will also add it to cached infos, so we don't need // to retrieve the result directly api.getCommunityById(forRequest.getId(), (r, e) -> { SwingUtilities.invokeLater(() -> { updateInfo(); // In case same one was selected again, reset if (forRequest.equals(shouldMaybeRequest)) { shouldMaybeRequest = null; } loading = false; updateName(r); }); }); } } private void updateName(Community c) { if (c != null && favorites.remove(c)) { favorites.add(c); saveFavorites(); } } /** * Clear the list and fill it with the current search result and favorites. * Also update the status text. */ private void update() { listData.clear(); for (Community game : searchResult) { listData.addElement(game); } if (!searchResult.isEmpty() && !favorites.isEmpty()) { listData.addElement(Community.EMPTY); } for (Community c : favorites) { listData.addElement(c); } searchResultInfo.setText("Search: "+searchResult.size()+" / " +"Favorites: "+favorites.size()+""); list.setSelectedValue(current, false); } private void doSearch() { String searchString = input.getText().trim(); if (searchString.isEmpty()) { searchResultInfo.setText("Enter something to search."); return; } api.getCommunityByName(searchString, (r, e) -> { SwingUtilities.invokeLater(() -> { if (r == null) { if (e != null) { searchResultInfo.setText(e); } else { searchResultInfo.setText("An error occured."); } } else { setCurrent(r); // Update cached name, if necessary (not sure if Communities // can even change name, but it's certainly not impossible). // Do it here because the result from the API should be // correct. updateName(r); searchResult.clear(); searchResult.add(r); update(); searchResultInfo.setText("Community found."); } }); }); searchResultInfo.setText("Searching.."); } private void showTop() { api.getCommunityTop((r) -> { SwingUtilities.invokeLater(() -> { searchResult.clear(); searchResult.addAll(r); update(); searchResultInfo.setText("Loaded current Top 100 (alphabetical)"); }); }); searchResultInfo.setText("Loading.."); } /** * Adds the currently selected games to the favorites. */ private void addToFavorites() { for (Community game : list.getSelectedValuesList()) { if (!game.isValid()) { favorites.add(game); } } saveFavorites(); update(); } /** * Removes the currently selected games from the favorites. */ private void removeFromFavorites() { for (Community c : list.getSelectedValuesList()) { favorites.remove(c); } saveFavorites(); update(); } /** * Removes all selected favorites and adds all selected non-favorites as * favorites. */ private void toggleFavorite() { for (Community c : list.getSelectedValuesList()) { if (favorites.contains(c) || c.isValid()) { favorites.remove(c); } else { favorites.add(c); } } saveFavorites(); update(); } private void saveFavorites() { Map<String, String> favs = new HashMap<>(); for (Community c : favorites) { favs.put(c.getId(), c.getName()); } main.setCommunityFavorites(favs); } private void loadFavorites() { favorites.clear(); Map<String, String> favs = main.getCommunityFavorites(); for (String id : favs.keySet()) { Community c = new Community(id, favs.get(id)); favorites.add(c); } } /** * Sets the state of the favorites buttons depending on the current * selection. */ private void updateFavoriteButtons() { boolean favoriteSelected = false; boolean nonFavoriteSelected = false; for (Community c : list.getSelectedValuesList()) { if (!c.isValid()) { if (favorites.contains(c)) { favoriteSelected = true; } else { nonFavoriteSelected = true; } } } addToFavoritesButton.setEnabled(nonFavoriteSelected); removeFromFavoritesButton.setEnabled(favoriteSelected); } private void updateOkButton() { boolean enabled = current != null && input.getText().equals(current.getName()); ok.setEnabled(enabled); openUrl.setEnabled(enabled); } private GridBagConstraints makeGbc(int x, int y, int w, int h) { GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = x; gbc.gridy = y; gbc.gridwidth = w; gbc.gridheight = h; gbc.insets = new Insets(4,4,4,4); return gbc; } /** * Change the game to be used to the one currently selected in the given * JList. * * @param list */ private void updateGameFromSelection() { Community selected = list.getSelectedValue(); if (selected != null) { setCurrent(selected); } } /** * Called when an item is selected either by changing the selected item * or clicking an already selected item. * * @param source */ private void itemSelected() { updateGameFromSelection(); } private class MyActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == input || e.getSource() == searchButton) { doSearch(); } if (e.getSource() == top100) { showTop(); } if (e.getSource() == ok) { useGameAndClose(); } if (e.getSource() == cancel) { save = false; setVisible(false); } if (e.getSource() == addToFavoritesButton) { addToFavorites(); } if (e.getSource() == removeFromFavoritesButton) { removeFromFavorites(); } if (e.getSource() == clearSearchButton) { searchResult.clear(); update(); } if (e.getSource() == openUrl) { if (current != null && !current.getName().isEmpty()) { UrlOpener.openUrlPrompt(main, "https://www.twitch.tv/communities/"+current.getName()); } } } } private class MyListSelectionListener implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { itemSelected(); updateFavoriteButtons(); } } /** * Use game by double-click. */ private class ListClickListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { itemSelected(); if (e.getClickCount() == 2) { useGameAndClose(); } } } private class DoneAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { useGameAndClose(); } } /** * Custom list item renderer, showing the star icon for favorites and adding * a seperating line. */ private class ListRenderer extends DefaultListCellRenderer { private final ImageIcon icon = new ImageIcon(MainGui.class.getResource("star.png")); private final Border seperatorBorder = BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY); @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value == null) { return label; } String text = value.toString(); if (text == null || text.isEmpty()) { label.setText(null); label.setBorder(seperatorBorder); } if (favorites.contains(value)) { label.setIcon(icon); } return label; } } }