package chatty.gui.components; import chatty.Helper; import chatty.gui.GuiUtil; import chatty.gui.MainGui; import chatty.gui.components.menus.ContextMenu; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.menus.StreamsContextMenu; import chatty.util.BitEncoder; import chatty.util.DateTime; 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.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; /** * * @author tduva */ public class FavoritesDialog extends JDialog { private final JTable table; private final MyTableModel data; private final CustomSorter sorter; private final JTextField input = new JTextField(30); private final JButton addToFavoritesButton = new JButton("Add to favorites"); private final JButton removeFromFavoritesButton = new JButton("Remove selected from favorites"); private final JButton removeButton = new JButton("Remove selected"); private final JButton doneButton = new JButton("Use chosen channels"); private final JButton cancelButton = new JButton("Cancel"); private String doneButtonText = ""; private String doneButtonTextOneChannel = ""; public static final int ACTION_CANCEL = 0; public static final int ACTION_DONE = 1; public static final int BUTTON_ADD_FAVORITES = 2; public static final int BUTTON_REMOVE_FAVORITES = 3; public static final int BUTTON_REMOVE = 4; private static final int COLUMN_FAV = 0; private static final int COLUMN_CHANNEL = 1; private static final int COLUMN_AGO = 2; private static final int FAV_COLUMN_WIDTH = 50; private static final int TIME_COLUMN_WIDTH = 100; private int result = -1; private final ContextMenuListener contextMenuListener; public FavoritesDialog(MainGui main, ContextMenuListener contextMenuListener) { super(main); setTitle("Favorites / History"); setModal(true); this.contextMenuListener = contextMenuListener; setLayout(new GridBagLayout()); GridBagConstraints gbc; input.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { channelsChanged(); } @Override public void removeUpdate(DocumentEvent e) { channelsChanged(); } @Override public void changedUpdate(DocumentEvent e) { channelsChanged(); } }); // Table data = new MyTableModel(); table = new JTable(); table.setModel(data); TableRowSorter<TableModel> rowSorter = new TableRowSorter<>(table.getModel()); table.setRowSorter(rowSorter); sorter = new CustomSorter(rowSorter); setupTable(); gbc = makeGbc(0,0,2,1); gbc.fill = GridBagConstraints.HORIZONTAL; add(input, gbc); gbc = makeGbc(2,0,1,1); addToFavoritesButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); add(addToFavoritesButton, gbc); gbc = makeGbc(0,2,3,1); gbc.fill = GridBagConstraints.BOTH; gbc.weighty = 1; add(new JScrollPane(table), gbc); gbc = makeGbc(0,3,1,1); gbc.insets = new Insets(2, 5, 5, 5); removeFromFavoritesButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); add(removeFromFavoritesButton, gbc); gbc = makeGbc(1,3,1,1); gbc.insets = new Insets(2, 5, 5, 5); removeButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); add(removeButton, gbc); doneButton.setMnemonic(KeyEvent.VK_J); gbc = makeGbc(0,4,2,1); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1; add(doneButton, gbc); cancelButton.setMnemonic(KeyEvent.VK_C); gbc = makeGbc(2,4,1,1); gbc.fill = GridBagConstraints.HORIZONTAL; add(cancelButton, gbc); removeFromFavoritesButton.setToolTipText("Remove selected channel(s) " + "from favorites only"); removeButton.setToolTipText("Remove selected channel(s) from favorites " + "and history"); // Button Listeners ActionListener listener = new FavoritesActionListener(); cancelButton.addActionListener(listener); doneButton.addActionListener(listener); ActionListener actionListener = main.getActionListener(); addToFavoritesButton.addActionListener(actionListener); removeFromFavoritesButton.addActionListener(actionListener); removeButton.addActionListener(actionListener); input.addActionListener(actionListener); channelsChanged(); pack(); setMinimumSize(getSize()); } private void setupTable() { table.setShowGrid(false); table.setDefaultRenderer(Boolean.class, new TestRenderer()); table.setDefaultRenderer(Long.class, new TimeRenderer()); table.getTableHeader().addMouseListener(sorter); table.getTableHeader().setReorderingAllowed(false); table.getTableHeader().setDefaultRenderer(new MyDefaultTableHeaderCellRenderer()); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { selectionChanged(); } }); table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { doneButton.doClick(); } } @Override public void mousePressed(MouseEvent e) { openContextMenu(e); } @Override public void mouseReleased(MouseEvent e) { openContextMenu(e); } }); // Shortcuts table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_J, 0), "done"); table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "done"); table.getActionMap().put("done", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { doneButton.doClick(); } }); table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"); table.getActionMap().put("delete", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { removeButton.doClick(); } }); table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0), "addFav"); table.getActionMap().put("addFav", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { addToFavoritesButton.doClick(); } }); table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_U, 0), "unFav"); table.getActionMap().put("unFav", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { removeFromFavoritesButton.doClick(); } }); // Column Sizes TableColumn favColumn = table.getColumnModel().getColumn(COLUMN_FAV); favColumn.setMaxWidth(FAV_COLUMN_WIDTH); favColumn.setMinWidth(FAV_COLUMN_WIDTH); TableColumn timeColumn = table.getColumnModel().getColumn(COLUMN_AGO); timeColumn.setMaxWidth(TIME_COLUMN_WIDTH); timeColumn.setMinWidth(TIME_COLUMN_WIDTH); } /** * Shows the dialog with the given channel as preset and the given action * as button description. * * @param channelPreset * @param action The description of the done-button * @param actionOneChannel The secondary description, to be used for one channel * @return */ public int showDialog(String channelPreset, String action, String actionOneChannel) { doneButton.setText(action); doneButtonText = action; doneButtonTextOneChannel = actionOneChannel; setChannel(channelPreset); result = -1; updateEditButtons(); setVisible(true); return result; } public int showDialog(String channelPreset, String action) { return showDialog(channelPreset, action, null); } /** * Gets the action associated with this Object (should be JButton). * * @param source * @return */ public int getAction(Object source) { if (source == addToFavoritesButton || source == input) { return BUTTON_ADD_FAVORITES; } else if (source == removeFromFavoritesButton) { return BUTTON_REMOVE_FAVORITES; } else if (source == cancelButton) { return ACTION_CANCEL; } else if (source == doneButton) { return ACTION_DONE; } else if (source == removeButton) { return BUTTON_REMOVE; } return -1; } /** * Sets the chosen channel(s) to the input box. * * @param channel */ public void setChannel(String channel) { if (channel != null) { input.setText(channel); } } /** * When the chosen channels changed. Update the done button. */ private void channelsChanged() { boolean channelsPresent = !getChannels().isEmpty(); doneButton.setEnabled(channelsPresent); int count = getChannels().size(); if (count == 1 && doneButtonTextOneChannel != null) { doneButton.setText(doneButtonTextOneChannel); } else { doneButton.setText(doneButtonText); } } /** * Sets input to the currently selected channels. */ private void selectionChanged() { updateChosenFromSelected(); updateEditButtons(); } private void updateChosenFromSelected() { String selectedChannels = Helper.buildStreamsString(getSelectedChannels()); input.setText(selectedChannels); } private void updateEditButtons() { boolean selected = !getSelectedChannels().isEmpty(); removeButton.setEnabled(selected); removeFromFavoritesButton.setEnabled(selected); } public Set<String> getSelectedChannels() { Set<String> selectedChannels = new HashSet<>(); int[] selected = table.getSelectedRows(); for (int i=0;i<selected.length;i++) { int row = selected[i]; Object channel = table.getValueAt(row, 1); if (channel != null) { selectedChannels.add((String)channel); } } return selectedChannels; } /** * Gets the current chosen channels. This means the channels in the input * box, either entered manually or preset by the caller or by selecting * channels in the list. * * @return */ public Set<String> getChannels() { String channels = input.getText(); return Helper.parseChannelsFromString(channels, false); } public void setData(Set<String> favorites, Map<String, Long> history) { Map<String, Long> favoritesWithHistory = new HashMap<>(); for (String channel : favorites) { favoritesWithHistory.put(channel, history.get(channel)); } //favoritesWithHistory = MapUtil.sortByValue(favoritesWithHistory); //history = MapUtil.sortByValue(history); data.setData(favoritesWithHistory, history); } private void openContextMenu(MouseEvent e) { if (e.isPopupTrigger()) { int clickedRow = table.rowAtPoint(e.getPoint()); if (clickedRow != -1) { if (!Helper.arrayContainsInt(table.getSelectedRows(), clickedRow)) { table.setRowSelectionInterval(clickedRow, clickedRow); } } if (table.getSelectedRow() != -1) { Set<String> selected = getSelectedChannels(); ContextMenu m = new StreamsContextMenu(selected, contextMenuListener); m.show(table, e.getX(), e.getY()); } } } /** * The column id may not be associated with the same column forever, in case * the columns are changed. * * @param column */ public void setSorting(int column) { sorter.setSorting(column); } /** * The returned value may not be associated with the same column forever, in * case the columns are changed. * * @return */ public int getSorting() { return sorter.getSorting(); } 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(5,5,5,5); return gbc; } static class TestRenderer extends DefaultTableCellRenderer { ImageIcon icon = new ImageIcon(getClass().getResource("/chatty/gui/star.png")); @Override public void setValue(Object value) { if (value instanceof Boolean) { if ((Boolean)value) { this.setIcon(icon); } else { this.setIcon(null); } JLabel label = (JLabel)this; label.setHorizontalAlignment(JLabel.CENTER); //this.setText(value.toString()); } } } static class TimeRenderer extends DefaultTableCellRenderer { @Override public void setValue(Object value) { if (value == null) { return; } long time = (Long)value; if (time == -1) { setText("-"); } else { setText(DateTime.agoText(time)); } JLabel label = (JLabel)this; label.setHorizontalAlignment(JLabel.CENTER); } } /** * Table Header renderer that always uses the second SortKey to determine * which column is sorted, because the first one is always the favorites * row, to keep the favorites at the top. */ private static class MyDefaultTableHeaderCellRenderer extends ExtendableDefaultTableHeaderCellRenderer { @Override protected RowSorter.SortKey getSortKey(JTable table, int column) { RowSorter rowSorter = table.getRowSorter(); if (rowSorter == null) { return null; } List sortedColumns = rowSorter.getSortKeys(); if (sortedColumns.size() > 1) { return (RowSorter.SortKey) sortedColumns.get(1); } else if (sortedColumns.size() == 1) { return (RowSorter.SortKey) sortedColumns.get(0); } return null; } } class MyTableModel extends AbstractTableModel { Object[][] data = { {true,"joshimuz",new Integer(1000)}, {true,"ninkortek",new Integer(1234)}, {false,"abc",new Integer(1)} }; /** * The columns. Changing this requires adjustments of column stuff in * some places (e.g. sorter). */ private final String[] columns = {"Fav","Channel","Last joined"}; @Override public int getRowCount() { return data.length; } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { return data[rowIndex][columnIndex]; } @Override public Class getColumnClass(int c) { if (data.length == 0) { return Object.class; } return getValueAt(0, c).getClass(); } @Override public String getColumnName(int col) { return columns[col]; } public void setData(Map<String, Long> favorites, Map<String, Long> history) { int count = 0; for (String entry : favorites.keySet()) { if (!history.containsKey(entry)) { count++; } } count += history.size(); data = new Object[count][columns.length]; int i = 0; for (String entry : favorites.keySet()) { addEntry(i, true, entry, history.get(entry)); i++; } for (String channel : history.keySet()) { Long time = history.get(channel); if (!favorites.containsKey(channel)) { addEntry(i, false, channel, time); i++; } } fireTableDataChanged(); } private void addEntry(int i, boolean favorite, String channel, Long time) { if (time == null) { time = -1l; } //System.out.println(i+" "+channel); data[i] = new Object[3]; data[i][0] = favorite; data[i][1] = channel; data[i][2] = time; } } private class FavoritesActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == doneButton) { result = ACTION_DONE; setVisible(false); } else if (e.getSource() == cancelButton) { result = ACTION_CANCEL; setVisible(false); } } } /** * Custom sorter, that sorts the column that is clicked on, using the * TableRowSorter added to the table. */ private final class CustomSorter extends MouseAdapter { private final BitEncoder encoder = new BitEncoder(new int[]{COLUMN_FAV, COLUMN_CHANNEL, COLUMN_AGO}, new int[]{0,1}); /** * How often the current column was sorted */ private int sortCount; /** * Which column is currently sorted */ private int sortedColumn; private final TableRowSorter<TableModel> sorter; CustomSorter(TableRowSorter<TableModel> sorter) { this.sorter = sorter; } @Override public void mouseClicked(MouseEvent e) { int columnIndex = table.getColumnModel().getColumnIndexAtX(e.getX()); sortColumn(columnIndex); } /** * Sorts the given column, either starting with the default sort order * if this column wasn't sorted in the last request, or switching * between sort orders. * * @param columnIndex */ public void sortColumn(int columnIndex) { sortCount++; // Start counting everytime another column is sorted if (sortedColumn != columnIndex) { sortCount = 0; // Use another starting order for the 3rd column (time ago) if (columnIndex == COLUMN_AGO) { sortCount = 1; } } sort(columnIndex, sortCount); } /** * Sorts the given column with the given sortCount, which indicates * the sort order. * * @param columnIndex * @param sortCount */ private void sort(int columnIndex, int sortCount) { SortOrder order = SortOrder.values()[sortCount % 2]; List<RowSorter.SortKey> sortKeys = new ArrayList<>(); // Always have column 0 (favorites) sorted first, so the favorites // stay at the top if (columnIndex != 0) { sortKeys.add(new RowSorter.SortKey(0, SortOrder.DESCENDING)); } sortKeys.add(new RowSorter.SortKey(columnIndex, order)); sorter.setSortKeys(sortKeys); this.sortCount = sortCount; sortedColumn = columnIndex; } public int getSorting() { encoder.setValue(0, sortedColumn); encoder.setValue(1, sortCount % 2); return (int)encoder.encode(); } public void setSorting(int sorting) { encoder.decode(sorting); sort(encoder.getValue(0), encoder.getValue(1)); } } }