package chatty.gui.components.srl; import chatty.gui.GuiUtil; import chatty.gui.HtmlColors; import chatty.gui.LinkListener; import chatty.gui.TwitchUrl; import chatty.gui.UrlOpener; import chatty.gui.components.ExtendedTextPane; import chatty.gui.components.LinkLabel; import chatty.gui.components.LinkLabelListener; import chatty.gui.components.menus.ContextMenu; import chatty.gui.components.menus.ContextMenuAdapter; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.menus.RaceContextMenu; import chatty.gui.components.menus.RaceEntrantContextMenu; import chatty.gui.components.settings.ListTableModel; import chatty.util.DateTime; import chatty.util.srl.Race; import chatty.util.srl.Race.Entrant; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.Timer; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableColumnModel; import javax.swing.table.TableRowSorter; /** * * @author tduva */ public class SRLRace extends JDialog { private static final int UPDATE_DELAY = 500; private final LinkLabel id; private final JLabel state = new JLabel(); private final JTextField game = new JTextField(); private final ExtendedTextPane goal = new ExtendedTextPane(); private final JTable entrantsTable = new JTable(); private final JLabel time = new JLabel(); private final ContextMenuListener streamsContextMenuListener; private final ContextMenuListener raceContextMenuListener; private long timeStarted; private Race currentRace; private final MyTableModel entrants = new MyTableModel(); public SRLRace(Window parent, ContextMenuListener streamsContextMenuListener) { super(parent); this.streamsContextMenuListener = streamsContextMenuListener; this.raceContextMenuListener = new RaceContextMenuListener(); Timer timer = new Timer(UPDATE_DELAY, new UpdateTimer()); timer.setRepeats(true); timer.start(); setLayout(new GridBagLayout()); GridBagConstraints gbc; gbc = GuiUtil.makeGbc(1, 0, 1, 1, GridBagConstraints.EAST); id = new LinkLabel("", new MyLinkLabelListener()); id.addMouseListener(new ContextMenuOpener()); gbc.weightx = 0.5; gbc.insets = new Insets(5, 5, 0, 5); add(id, gbc); gbc = GuiUtil.makeGbc(0, 4, 2, 1, GridBagConstraints.WEST); add(state, gbc); gbc = GuiUtil.makeGbc(1, 4, 1, 1, GridBagConstraints.EAST); add(time, gbc); gbc = GuiUtil.makeGbc(0, 0, 1, 1, GridBagConstraints.WEST); gbc.insets = new Insets(5, 5, 0, 5); add(new JLabel("Game:"), gbc); gbc = GuiUtil.makeGbc(0, 1, 2, 1, GridBagConstraints.WEST); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1; game.setEditable(false); add(game, gbc); gbc = GuiUtil.makeGbc(0, 2, 1, 1, GridBagConstraints.WEST); gbc.insets = new Insets(5, 5, 0, 5); add(new JLabel("Goal:"), gbc); gbc = GuiUtil.makeGbc(0, 3, 2, 1, GridBagConstraints.WEST); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1; goal.setEditable(false); goal.setBackground(game.getBackground()); goal.setBorder(game.getBorder()); goal.setLinkListener(new MyLinkListener()); goal.setMargin(game.getMargin()); add(goal, gbc); entrantsTable.setModel(entrants); entrantsTable.setFillsViewportHeight(true); entrantsTable.addMouseListener(new MouseClicks()); TableRowSorter<MyTableModel> sorter = new TableRowSorter<>(entrants); entrantsTable.setRowSorter(sorter); TableColumnModel columns = entrantsTable.getColumnModel(); columns.getColumn(0).setMaxWidth(40); columns.getColumn(2).setMaxWidth(80); columns.getColumn(3).setMinWidth(70); columns.getColumn(3).setMaxWidth(120); columns.getColumn(4).setMinWidth(40); columns.getColumn(4).setMaxWidth(40); sorter.setComparator(0, new PlaceSorter()); columns.getColumn(0).setCellRenderer(new PlaceRenderer()); columns.getColumn(3).setCellRenderer(new StateRenderer()); //sorter.toggleSortOrder(0); gbc = GuiUtil.makeGbc(0, 5, 2, 1, GridBagConstraints.WEST); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1; gbc.weighty = 1; add(new JScrollPane(entrantsTable), gbc); setPreferredSize(new Dimension(350, 410)); setSize(new Dimension(340, 410)); setMinimumSize(new Dimension(200,300)); } public void open(Race race) { setRace(race); setVisible(true); } public void setRace(Race race) { currentRace = race; state.setText(race.statetext+" ("+race.getEntrants().size()+" entrants)"); game.setText(race.game); goal.setText(race.goal); entrants.setData(race.getEntrants()); id.setText("[srl:"+race.id+" #srl-"+race.id+"]"); timeStarted = race.time; update(); setTitle("Race: "+race.game+" ["+race.statetext+"]"); } /** * Give this list of races, whereas the dialog picks out the one it * currently has open (if present). * * @param races */ public void update(List<Race> races) { if (currentRace == null) { return; } if (races.contains(currentRace)) { setRace(races.get(races.indexOf(currentRace))); } } public void error() { if (currentRace != null) { setTitle("(error) "+currentRace.game); } } public Race getRace() { return currentRace; } private void update() { if (timeStarted > 0 && currentRace.state == Race.IN_PROGRESS) { time.setText(DateTime.agoClock(timeStarted, true)); } else { time.setText(""); } } private class ContextMenuOpener extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { openContextMenu(e); } @Override public void mouseReleased(MouseEvent e) { openContextMenu(e); } private void openContextMenu(MouseEvent e) { if (e.isPopupTrigger()) { if (currentRace != null) { ContextMenu m = new RaceContextMenu(currentRace, raceContextMenuListener, true); m.show(e.getComponent(), e.getX(), e.getY()); } } } } private class RaceContextMenuListener extends ContextMenuAdapter { @Override public void menuItemClicked(ActionEvent e) { Race selectedRace = currentRace; if (selectedRace == null) { return; } if (e.getActionCommand().equals("srlRacePage")) { String url = TwitchUrl.makeSrlRaceLink(selectedRace.id); UrlOpener.openUrlPrompt(SRLRace.this, url, true); } else if (e.getActionCommand().equals("speedruntv")) { String url = TwitchUrl.makeSrtRaceLink(selectedRace.id); UrlOpener.openUrlPrompt(SRLRace.this, url, true); } else if (e.getActionCommand().equals("joinSrlChannel")) { String url = TwitchUrl.makeSrlIrcLink(selectedRace.id); UrlOpener.openUrlPrompt(SRLRace.this, url, true); } } } /** * Updates the dialog on a regular interval (mainly the time). */ private class UpdateTimer implements ActionListener { @Override public void actionPerformed(ActionEvent e) { update(); } } /** * Sorts the place, which returns an Entrant object, so just sort by that, * which should sort by place (unless SRL changes something and other sort * fields have to be included). */ private static class PlaceSorter implements Comparator<Entrant> { @Override public int compare(Entrant o1, Entrant o2) { return o1.compareTo(o2); } } private static class MyTableModel extends ListTableModel<Race.Entrant> { public MyTableModel() { super(new String[]{"Place", "Name", "Time", "State", "Points"}); } @Override public Object getValueAt(int rowIndex, int columnIndex) { Entrant e = get(rowIndex); switch (columnIndex) { case 0: return e; case 1: return e.name; case 2: return formatTime(e.time); case 3: return e; case 4: return e.points; } return null; } @Override public Class getColumnClass(int c) { if (c == 4) { return Integer.class; } else if (c == 0 || c == 3) { return Entrant.class; } else { return String.class; } } private String getPlace(Entrant e) { if (e.time > 0) { return String.valueOf(e.place); } return "-"; } private String formatTime(long time) { if (time > 0) { return DateTime.durationClock(time, true); } return "-"; } } private List<Entrant> getSelectedEntrants() { int[] selected = entrantsTable.getSelectedRows(); List<Entrant> selectedEntrants = new ArrayList<>(); for (int i : selected) { selectedEntrants.add(entrants.get( entrantsTable.convertRowIndexToModel(i))); } return selectedEntrants; } private class MyLinkListener implements LinkListener { @Override public void linkClicked(String url) { UrlOpener.openUrlPrompt(SRLRace.this, url); } } private class MyLinkLabelListener implements LinkLabelListener { @Override public void linkClicked(String type, String ref) { UrlOpener.openUrlPrompt(SRLRace.this, TwitchUrl.makeSrlRaceLink(ref)); } } private class MouseClicks extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { selectClicked(e); openContextMenu(e); } @Override public void mouseReleased(MouseEvent e) { openContextMenu(e); } private void openContextMenu(MouseEvent e) { if (e.isPopupTrigger()) { List<Entrant> selectedEntrants = getSelectedEntrants(); if (!selectedEntrants.isEmpty()) { ContextMenu m = new RaceEntrantContextMenu(selectedEntrants, streamsContextMenuListener); m.show(e.getComponent(), e.getX(), e.getY()); } } } private void selectClicked(MouseEvent e) { int clickedRow = entrantsTable.rowAtPoint(e.getPoint()); if (clickedRow != -1 && !entrantsTable.isRowSelected(clickedRow)) { entrantsTable.setRowSelectionInterval(clickedRow, clickedRow); } } } /** * Turns the place into an appropriate String. This uses an Entrant, which * can also be sorted properly. */ private static class PlaceRenderer extends DefaultTableCellRenderer { @Override public void setValue(Object value) { if (value == null) { return; } Entrant e = (Entrant)value; if (e.time > 0) { setText(String.valueOf(e.place)); } else { setText("-"); } setToolTipText(e.message); } } /** * Draws the state text in different colors. */ private static class StateRenderer extends DefaultTableCellRenderer { private static final Color DEFAULT = Color.BLACK; private static final Color FORFEIT = Color.RED; private static final Color ENTERED = new Color(120,120,120); private static final Color FINISHED = HtmlColors.getNamedColor("BlueViolet"); @Override public void setValue(Object value) { if (value == null) { return; } Entrant e = (Entrant)value; String state = e.statetext; if (state.equals("Forfeit") || state.equals("Disqualified")) { setForeground(FORFEIT); } else if (state.equals("Entered")) { setForeground(ENTERED); } else if (state.equals("Finished")) { setForeground(FINISHED); } else { setForeground(DEFAULT); } setText(state+(e.message.isEmpty() ? "" : " [..]")); setToolTipText(e.message); } } }