package net.classicube.launcher.gui; import java.awt.Color; import java.awt.Component; import java.awt.KeyboardFocusManager; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.RowSorter; import javax.swing.SwingWorker.StateValue; import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import net.classicube.launcher.GameServiceType; import net.classicube.launcher.GameSession; import net.classicube.launcher.GetExternalIPTask; import net.classicube.launcher.LogUtil; import net.classicube.launcher.Prefs; import net.classicube.launcher.ServerJoinInfo; import net.classicube.launcher.ServerListEntry; import net.classicube.launcher.SessionManager; public final class ServerListScreen extends javax.swing.JFrame { // ============================================================================================= // FIELDS & CONSTANTS // ============================================================================================= private final List<ServerListEntry> displayedServerList = new ArrayList<>(); private GameSession.GetServerDetailsTask getServerDetailsTask; private final GameSession.GetServerListTask getServerListTask; private ServerListEntry selectedServer; private ServerListEntry[] serverList; private final GameSession session; private final TableColumnAdjuster tableColumnAdjuster; // ============================================================================================= // INITIALIZATION // ============================================================================================= public ServerListScreen() { LogUtil.getLogger().log(Level.FINE, "ServerListScreen"); // Make a pretty background final ImagePanel bgPanel = new ImagePanel(null, true); bgPanel.setGradient(true); this.setContentPane(bgPanel); bgPanel.setBorder(new EmptyBorder(8, 8, 8, 8)); // init components and stuff this.initComponents(); this.setIconImages(Resources.getWindowIcons()); this.serverTableContainer.getViewport().setBackground(new Color(247, 247, 247)); // hook up context menus CutCopyPasteAdapter.addToComponent(this.tSearch, true, true); CutCopyPasteAdapter.addToComponent(this.tServerURL, true, true); // set window title this.session = SessionManager.getSession(); final String playerName = session.getAccount().playerName; if (session.getServiceType() == GameServiceType.ClassiCubeNetService) { setTitle(playerName + " @ ClassiCube.net - servers"); bgPanel.setImage(Resources.getClassiCubeBackground()); bgPanel.setGradientColor(Resources.ccGradient); serverTable.getColumnModel().getColumn(4).setHeaderValue("Software"); } else { setTitle(playerName + " @ Minecraft.net - servers"); bgPanel.setImage(Resources.getMinecraftNetBackground()); bgPanel.setGradientColor(Resources.mcGradient); } // prepare to auto-adjust table columns (when the data arrives) serverTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); tableColumnAdjuster = new TableColumnAdjuster(serverTable); // configure table sorting and selection serverTable.setAutoCreateRowSorter(true); serverTable.setCellSelectionEnabled(false); serverTable.setRowSelectionAllowed(true); // set table shortcuts setHandlers(); // center the form on screen (initially) setLocationRelativeTo(null); getRootPane().setDefaultButton(this.bConnect); // start fetching the server list tSearch.setPlaceholder("Loading server list..."); tSearch.setEnabled(false); getServerListTask = session.getServerListAsync(); getServerListTask.addPropertyChangeListener( new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent evt) { if ("state".equals(evt.getPropertyName())) { if (evt.getNewValue().equals(StateValue.DONE)) { onServerListDone(); } } } }); getServerListTask.execute(); } private void enableGui() { bChangeUser.setEnabled(true); bPreferences.setEnabled(true); tSearch.setEnabled(true); serverTable.setEnabled(true); tServerURL.setEnabled(true); bConnect.setEnabled(true); } private void disableGui() { bChangeUser.setEnabled(false); bPreferences.setEnabled(false); tSearch.setEnabled(false); serverTable.setEnabled(false); tServerURL.setEnabled(false); bConnect.setEnabled(false); } // ============================================================================================= // SERVER LIST FILLING // ============================================================================================= private void onServerListDone() { LogUtil.getLogger().log(Level.FINE, "ServerListScreen.onServerListDone"); try { serverList = getServerListTask.get(); fillServerTable(); tSearch.setPlaceholder("Search servers..."); tSearch.setEnabled(true); tSearch.selectAll(); tSearch.requestFocus(); progress.setVisible(false); tableColumnAdjuster.adjustColumns(); } catch (InterruptedException | ExecutionException ex) { LogUtil.getLogger().log(Level.SEVERE, "Error loading server list", ex); ErrorScreen.show("Could not load server list", "An error occured while loading the server list:<br>" + ex.getMessage(), ex); tSearch.setText("Could not load server list."); } } private void fillServerTable() { final DefaultTableModel model = (DefaultTableModel) serverTable.getModel(); // reset sort order final RowSorter<? extends TableModel> rowSorter = serverTable.getRowSorter(); rowSorter.setSortKeys(null); // remove all rows model.setNumRows(0); displayedServerList.clear(); tServerURL.setText(""); // add new rows boolean isCC = (session.getServiceType() == GameServiceType.ClassiCubeNetService); final String searchTerm = tSearch.getText().toLowerCase(); for (final ServerListEntry server : serverList) { if (server.name.toLowerCase().contains(searchTerm)) { displayedServerList.add(server); model.addRow(new Object[]{ server.name.replaceAll("\\s+", " "), // strip consecutive spaces server.players, server.maxPlayers, server.uptime, // CC.net servers show "Software" in the last column. // MC.net servers show "Country" instead. (isCC ? server.software : ServerListEntry.toCountryName(server.flag)) }); } } // select first server if (model.getRowCount() > 0) { serverTable.setRowSelectionInterval(0, 0); } } // ============================================================================================= // JOINING A SERVER // ============================================================================================= private void bConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bConnectActionPerformed LogUtil.getLogger().log(Level.FINE, "[Connect]"); joinSelectedServer(); }//GEN-LAST:event_bConnectActionPerformed private ServerListEntry getSelectedServer() { final int[] rowIndex = serverTable.getSelectedRows(); if (rowIndex.length == 1) { final int trueIndex = serverTable.convertRowIndexToModel(rowIndex[0]); return displayedServerList.get(trueIndex); } return null; } private void joinSelectedServer() { LogUtil.getLogger().log(Level.INFO, "Fetching details for server: {0}", selectedServer.name); final String url = tServerURL.getText(); final String trimmedInput = url.replaceAll("[\\r\\n\\s]", ""); final ServerJoinInfo joinInfo = session.getDetailsFromUrl(trimmedInput); if (joinInfo == null) { ErrorScreen.show("Cannot connect to given server", "Unrecognized server URL. Make sure that you are using the correct link.", null); } else if (joinInfo.passNeeded) { getServerDetailsTask = session.getServerDetailsAsync(trimmedInput); getServerDetailsTask.addPropertyChangeListener( new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent evt) { if ("state".equals(evt.getPropertyName())) { if (evt.getNewValue().equals(StateValue.DONE)) { onServerDetailsDone(); } } } }); progress.setVisible(true); if (Prefs.getKeepOpen()) { disableGui(); } getServerDetailsTask.execute(); } else { joinServer(joinInfo); } } private void onServerDetailsDone() { LogUtil.getLogger().log(Level.FINE, "onServerDetailsDone"); try { final boolean result = getServerDetailsTask.get(); if (result) { final ServerJoinInfo joinInfo = getServerDetailsTask.getJoinInfo(); joinServer(joinInfo); } else { ErrorScreen.show("Cannot connect", "There was a problem fetching server details.", null); enableGui(); } } catch (final InterruptedException | ExecutionException ex) { LogUtil.getLogger().log(Level.SEVERE, "Error loading server details", ex); ErrorScreen.show("Error fetching server details", ex.getMessage(), ex); enableGui(); } } private void joinServer(ServerJoinInfo joinInfo) { if (joinInfo == null) { throw new NullPointerException("joinInfo"); } if (joinInfo.playerName == null || "".equals(joinInfo.playerName)) { joinInfo.playerName = session.getAccount().playerName; } InetAddress serverAddress = joinInfo.address; try { InetAddress localAddress = GetExternalIPTask.getInstance().get(); if (serverAddress.equals(localAddress)) { InetAddress correctedAddress = SameIPScreen.show(serverAddress, joinInfo.port); if (correctedAddress == null) { enableGui(); progress.setVisible(false); return; // player canceled/closed dialog } else { joinInfo.address = correctedAddress; } } } catch (InterruptedException | ExecutionException ex) { GetExternalIPTask.logAndShowError(ex); } if (UpdateScreen.createAndShow(joinInfo)) { enableGui(); progress.setVisible(false); } else { dispose(); } } // ============================================================================================= // GUI EVENT LISTENERS // ============================================================================================= private void setHandlers() { // allow double-clicking servers on the list, to join them serverTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent e) { if (e.getClickCount() == 2) { joinSelectedServer(); } } }); // allow pressing <Enter> on the server table to join serverTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Enter"); serverTable.getActionMap().put("Enter", new AbstractAction() { @Override public void actionPerformed(final ActionEvent ae) { joinSelectedServer(); } }); // Fill in the "Server URL" field when a server is selected in the table serverTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(final ListSelectionEvent lse) { if (!lse.getValueIsAdjusting()) { selectedServer = getSelectedServer(); if (selectedServer != null) { final String playUrl = session.getPlayUrl(selectedServer.hash); tServerURL.setText(playUrl); } } } }); // Prevent the server list JTable from permanently stealing keyboard focus ActionMap tableActions = serverTable.getActionMap(); tableActions.put("selectPreviousColumnCell", new TableFocusPreviousComponentAction()); tableActions.put("selectNextColumnCell", new TableFocusNextComponentAction()); // Select contents of ServerURL field on-focus tServerURL.addFocusListener(new FocusListener() { @Override public void focusGained(final FocusEvent e) { tServerURL.selectAll(); } @Override public void focusLost(final FocusEvent e) { } }); } private static class TableFocusPreviousComponentAction extends AbstractAction { @Override public void actionPerformed(ActionEvent evt) { KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); manager.focusPreviousComponent(); } } private static class TableFocusNextComponentAction extends AbstractAction { @Override public void actionPerformed(ActionEvent evt) { KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); manager.focusNextComponent(); } } private static class UptimeCellRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (column == 3) { final int ticks = (int) value; this.setText(ServerListEntry.formatUptime(ticks)); } else { this.setText(""); } return this; } } private void bChangeUserActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bChangeUserActionPerformed LogUtil.getLogger().log(Level.INFO, "[Change User]"); dispose(); new SignInScreen().setVisible(true); }//GEN-LAST:event_bChangeUserActionPerformed private void tSearchKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_tSearchKeyReleased fillServerTable(); }//GEN-LAST:event_tSearchKeyReleased private void tSearchFocusGained(java.awt.event.FocusEvent evt) {//GEN-FIRST:event_tSearchFocusGained tSearch.selectAll(); }//GEN-LAST:event_tSearchFocusGained private void tSearchActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tSearchActionPerformed if (serverTable.getSelectedRows().length == 1) { joinSelectedServer(); } }//GEN-LAST:event_tSearchActionPerformed private void bPreferencesActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bPreferencesActionPerformed new PreferencesScreen(this).setVisible(true); }//GEN-LAST:event_bPreferencesActionPerformed private void tServerURLActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tServerURLActionPerformed joinSelectedServer(); }//GEN-LAST:event_tServerURLActionPerformed // ============================================================================================= // GENERATED GUI CODE // ============================================================================================= /** * This method is called from within the constructor to initialize the form. WARNING: Do NOT * modify this code. The content of this method is always regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { java.awt.GridBagConstraints gridBagConstraints; bChangeUser = new net.classicube.launcher.gui.JNiceLookingButton(); bPreferences = new net.classicube.launcher.gui.JNiceLookingButton(); javax.swing.JSeparator separator1 = new javax.swing.JSeparator(); tSearch = new net.classicube.launcher.gui.PlaceholderTextField(); serverTableContainer = new javax.swing.JScrollPane(); serverTable = new javax.swing.JTable(); javax.swing.JSeparator separator2 = new javax.swing.JSeparator(); tServerURL = new javax.swing.JTextField(); bConnect = new net.classicube.launcher.gui.JNiceLookingButton(); progress = new javax.swing.JProgressBar(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); setPreferredSize(new java.awt.Dimension(600, 500)); java.awt.GridBagLayout layout = new java.awt.GridBagLayout(); layout.columnWidths = new int[] {0, 5, 0, 5, 0}; layout.rowHeights = new int[] {0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 0}; getContentPane().setLayout(layout); bChangeUser.setText("< Change User"); bChangeUser.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { bChangeUserActionPerformed(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; getContentPane().add(bChangeUser, gridBagConstraints); bPreferences.setText("Preferences"); bPreferences.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { bPreferencesActionPerformed(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 4; gridBagConstraints.gridy = 0; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_END; getContentPane().add(bPreferences, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; gridBagConstraints.gridwidth = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; getContentPane().add(separator1, gridBagConstraints); tSearch.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { tSearchActionPerformed(evt); } }); tSearch.addFocusListener(new java.awt.event.FocusAdapter() { public void focusGained(java.awt.event.FocusEvent evt) { tSearchFocusGained(evt); } }); tSearch.addKeyListener(new java.awt.event.KeyAdapter() { public void keyReleased(java.awt.event.KeyEvent evt) { tSearchKeyReleased(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 4; gridBagConstraints.gridwidth = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; getContentPane().add(tSearch, gridBagConstraints); serverTableContainer.setBackground(new java.awt.Color(255, 255, 255)); serverTableContainer.setMinimumSize(new java.awt.Dimension(300, 150)); serverTableContainer.setPreferredSize(new java.awt.Dimension(550, 400)); serverTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { }, new String [] { "Name", "Players", "Max", "Uptime", "Location" } ) { Class[] types = new Class [] { java.lang.String.class, java.lang.Integer.class, java.lang.Integer.class, java.lang.Integer.class, java.lang.String.class }; boolean[] canEdit = new boolean [] { false, false, false, false, false }; public Class getColumnClass(int columnIndex) { return types [columnIndex]; } public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit [columnIndex]; } }); serverTable.setColumnSelectionAllowed(true); serverTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); serverTable.getTableHeader().setReorderingAllowed(false); serverTableContainer.setViewportView(serverTable); serverTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); if (serverTable.getColumnModel().getColumnCount() > 0) { serverTable.getColumnModel().getColumn(1).setPreferredWidth(60); serverTable.getColumnModel().getColumn(2).setPreferredWidth(60); serverTable.getColumnModel().getColumn(3).setPreferredWidth(60); serverTable.getColumnModel().getColumn(3).setCellRenderer(new UptimeCellRenderer()); serverTable.getColumnModel().getColumn(4).setPreferredWidth(60); } gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 6; gridBagConstraints.gridwidth = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.weightx = 0.1; gridBagConstraints.weighty = 0.1; getContentPane().add(serverTableContainer, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 8; gridBagConstraints.gridwidth = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; getContentPane().add(separator2, gridBagConstraints); tServerURL.setText("Server URL"); tServerURL.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { tServerURLActionPerformed(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 10; gridBagConstraints.gridwidth = 3; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.weightx = 0.1; getContentPane().add(tServerURL, gridBagConstraints); bConnect.setText("Connect >"); bConnect.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { bConnectActionPerformed(evt); } }); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 4; gridBagConstraints.gridy = 10; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.LAST_LINE_END; getContentPane().add(bConnect, gridBagConstraints); progress.setIndeterminate(true); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 12; gridBagConstraints.gridwidth = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; getContentPane().add(progress, gridBagConstraints); pack(); }// </editor-fold>//GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private net.classicube.launcher.gui.JNiceLookingButton bChangeUser; private net.classicube.launcher.gui.JNiceLookingButton bConnect; private net.classicube.launcher.gui.JNiceLookingButton bPreferences; private javax.swing.JProgressBar progress; private javax.swing.JTable serverTable; private javax.swing.JScrollPane serverTableContainer; private net.classicube.launcher.gui.PlaceholderTextField tSearch; private javax.swing.JTextField tServerURL; // End of variables declaration//GEN-END:variables }