/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.launcher; import aphelion.client.net.Ping; import aphelion.client.net.PingListener; import aphelion.shared.swissarmyknife.ThreadSafe; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.util.ArrayList; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; /** * * @author Joris */ public class MainFrame extends JFrame implements PingListener { private static final Logger log = Logger.getLogger("aphelion.launcher"); private ArrayList<ZoneEntry> zones = new ArrayList<>(); private Ping ping; private JTextArea zoneDesc; private ZoneList zoneList; private JButton playButton; private JButton quitButton; private JTextField username; private JTextField password; public MainFrame() throws HeadlessException { JPanel wrap; JPanel left; JPanel right; JPanel profile; JPanel profile_username; JPanel profile_password; JPanel zonelistButtons; ping = new Ping(this); this.setIconImages(Main.getFrameIcons()); this.setTitle("Aphelion"); add(wrap = new JPanel()); wrap.setBorder(new EmptyBorder(10, 10, 10, 10) ); wrap.setLayout(new GridLayout(0, 2, 10, 10)); wrap.add(left = new JPanel()); wrap.add(right = new JPanel()); left.setLayout(new GridLayout(0, 1)); right.setLayout(new BorderLayout(10, 10)); // todo: order by favorite first // display warning while unfavoriting if the entry is a saved favorite (not if it was just added) left.add(profile = new JPanel()); profile.setLayout(new GridLayout(0, 1, 10, 10)); profile.add(profile_username = new JPanel()); profile_username.setLayout(new GridLayout(1, 2)); profile_username.add(new JLabel("Username:")); profile_username.add(username = new JFormattedTextField(usernameFormatFactory)); profile.add(profile_password = new JPanel()); profile_password.setLayout(new GridLayout(1, 2)); profile_password.add(new JLabel("Password:")); profile_password.add(password = new JTextField("(not implemented yet)")); profile.add(new JPanel()); profile.add(new JPanel()); profile.add(new JPanel()); left.add(zoneDesc = new JTextArea()); zoneDesc.setEditable(false); zoneDesc.setWrapStyleWord(true); zoneDesc.setLineWrap(true); right.add(zoneList = new ZoneList(zones), BorderLayout.CENTER); right.add(zonelistButtons = new JPanel(), BorderLayout.SOUTH); zonelistButtons.setLayout(new GridLayout(1, 0, 10, 10)); zonelistButtons.add(playButton = new JButton("Play")); zonelistButtons.add(quitButton = new JButton("Quit")); setDefaultCloseOperation(DISPOSE_ON_CLOSE); setSize(700, 400); setLocationRelativeTo(null); quitButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }); addWindowListener(null); zoneList.addSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { ZoneEntry entry = zoneList.getSelectedZoneEntry(); zoneDesc.setText(entry == null ? "" : entry.description); } }); addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { ping.interrupt(); ping = null; } }); // TODO: from file or something addZoneEntry(true, null, "singeplayer", "Try a local game without any human opponents! Feel free to experiment with modifying the zip files in the folder 'assets'."); try { addZoneEntry(true, new URI("ws://localhost:80/"), "localhost", "Test your own locally hosted game server"); //addZoneEntry(false, new URI("ws://aphelion-test.welcome-to-the-machine.com:81/"), "Test server", "JoWie's test server in Europe!"); } catch (URISyntaxException ex) { throw new Error(ex); } updatedZoneEntries(); ping.start(); } private static JFormattedTextField.AbstractFormatterFactory usernameFormatFactory = new JFormattedTextField.AbstractFormatterFactory() { private Pattern firstChar = Pattern.compile("[a-z]"); private Pattern otherChars = Pattern.compile("[a-zA-Z0-9\\\\-\\\\[\\\\]\\\\\\\\`^{}_ ]*"); @Override public JFormattedTextField.AbstractFormatter getFormatter(JFormattedTextField tf) { return new JFormattedTextField.AbstractFormatter() { @Override public Object stringToValue(String text) throws ParseException { // legal: [a-z][a-zA-Z0-9\-\[\]\\`^{}_ ]* if (text == null || text.isEmpty()) { return text; } StringBuilder ret = new StringBuilder(text.length()); for (int i = 0; i < text.length(); ++i) { char c = text.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { // always legal ret.append(c); continue; } if (ret.length() > 0) // not the first char { if (c >= '0' && c <= '9') { ret.append(c); continue; } if ( c == '-' || c == '[' || c == ']' || c == '\\' || c == '`' || c == '^' || c == '{' || c == '}' || c == '_' || c == ' ') { ret.append(c); continue; } } } return ret.toString(); } @Override public String valueToString(Object value) throws ParseException { return (String) value; } }; } }; public final void addZoneEntry(boolean favorite, URI url, String name, String description) { zones.add(new ZoneEntry(favorite, url, name, description)); if (url != null) { ping.startPing(url); } } /** Always call me after calling addZoneEntry 1 or more times. */ public final void updatedZoneEntries() { zoneList.updatedEntries(); } public String getUserName() { return username.getText(); } public void addPlayActionListener(ActionListener l) { playButton.addActionListener(l); } public ZoneEntry getSelectedZoneEntry() { return zoneList.getSelectedZoneEntry(); } public void addPingResult(URI uri, long rttNanos, int players, int playing) { for (ZoneEntry entry : zones) { if (entry.url != null && entry.url.equals(uri)) { entry.ping = rttNanos > 0 ? (int) (rttNanos / 1_000_000L) : -1; entry.players = players; entry.playing = playing; } updatedZoneEntries(); } } @Override @ThreadSafe public void pingResult(final URI uri, final long rttNanos, final int players, final int playing) { // run in swing thread SwingUtilities.invokeLater(new Runnable() { @Override public void run() { addPingResult(uri, rttNanos, players, playing); } }); } }