package eu.irreality.age.swing.newloader; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextPane; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.Document; import eu.irreality.age.i18n.UIMessages; import eu.irreality.age.swing.newloader.download.DownloadProgressKeeper; import eu.irreality.age.swing.newloader.download.ProgressKeepingDelegate; import eu.irreality.age.swing.newloader.download.ProgressKeepingReadableByteChannel; import eu.irreality.age.swing.sdi.SwingSDIInterface; import eu.irreality.age.util.xml.XMLfromURL; /** * A panel with a list of games that can be downloaded and/or played. * @author carlos * */ public class NewLoaderGamePanel extends JPanel implements ProgressKeepingDelegate { private final Frame parentFrame; private JTable gameTable; private JTextPane infoPane = new JTextPane(); private GameTableModel gameTableModel; private XTableColumnModel gameTableColumnModel; private JScrollPane tableScrollPane; private JPanel downloadOrPlayButtonPanel = new JPanel(); private JPanel progressBarPanel = new JPanel(); private JButton currentDownloadOrPlayButton; //this can be a play button or a download button private JButton playButton = new JButton( UIMessages.getInstance().getMessage("gameloader.play") ); private JButton downloadButton = new JButton( UIMessages.getInstance().getMessage("gameloader.download") ); private JButton downloadingButton = new JButton( UIMessages.getInstance().getMessage("gameloader.downloading") ); private JButton syncButton = new JButton( UIMessages.getInstance().getMessage("gameloader.sync") ); /**This maps game entries to the objects keeping their download progress and holding their progress bars*/ private Map downloadProgressKeepers = Collections.synchronizedMap(new HashMap()); public void writeData() { try { gameTableModel.writeCatalog(); } catch (Exception e) { this.showError(e.getLocalizedMessage(), UIMessages.getInstance().getMessage("gameloader.error.saving.catalog") ); } } /** * Obtains the DownloadProgressKeeper for the given game entry, or initializes it if there is none. * @param ge * @return */ private DownloadProgressKeeper getProgressKeeper ( GameEntry ge ) { DownloadProgressKeeper result = (DownloadProgressKeeper) downloadProgressKeepers.get(ge); if ( result != null ) return result; result = new DownloadProgressKeeper(ge); downloadProgressKeepers.put(ge, result); return result; } /** * Associates a new DownloadProgressKeeper with the given game entry, based on its current download progress data, and * discarding the already stored progress keeper if present. * This is used if we thought a game was downloaded, but it was actually not present in the hard disk, so we have to reset * its progress. * @param ge * @return */ private void resetProgressKeeper ( GameEntry ge ) { DownloadProgressKeeper newKeeper = new DownloadProgressKeeper(ge); downloadProgressKeepers.put( ge, newKeeper ); } private GameEntry getSelectedGameEntry() { int index = gameTable.getSelectedRow(); return (GameEntry) gameTableModel.getGameEntry(index); } void showError(String message, String title) { JOptionPane.showMessageDialog(this,"<html><p>"+message+"</p>",title,JOptionPane.ERROR_MESSAGE); } /** * Shows an error message as soon as possible in the event dispatching thread (i.e. via invokeLater()). * Can be called from any thread. * @param message * @param title */ private void showErrorWhenPossible(final String message, final String title) { SwingUtilities.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog(NewLoaderGamePanel.this,"<html><p>"+message+"</p>",title,JOptionPane.ERROR_MESSAGE); } }); } /** * Tries to add all the games contained in a catalog to the table model, but it this fails for some reason, this method does not throw exceptions but return false instead. * If the overwrite parameter is true, then the added entries overwrite existing entries with the same local path / remote URL. * @param catalogURL */ public boolean loadGameCatalogIfPossible ( URL catalogURL , boolean overwrite ) { try { gameTableModel.loadGameCatalog ( catalogURL , overwrite ); return true; } catch ( Exception e ) { return false; } } /** * Loads the games contained in a catalog in a URL when possible. Shows dialogs showing the number of games updated, * or the errors found, if any. * This method is not blocking. It will open a new thread to download the catalog, and add the games to the table * in the event dispatch thread when it is ready. The dialogs will also be enqueued on the event dispatch thread. * While it is possible to call this method for a local URL as well, it wouldn't make much sense to go through all * the threading complexity for a local catalog - just call loadCatalog on the GameTableModel for that. * @param catalogURL * @param overwrite * @throws IOException * @throws TransformerException */ public void syncWithRemoteCatalog ( final URL catalogURL , final boolean overwrite ) { //If we ask for java 1.6, this could be done better with SwingWorker. doInBackground(), throw exception, and in done catch in get() ExecutedException, InterruptedException Thread thr = new Thread() { public void run() { final Document doc; try { doc = XMLfromURL.getXMLFromURL(catalogURL); } catch (FileNotFoundException e1) { e1.printStackTrace(); showErrorWhenPossible(UIMessages.getInstance().getMessage("exception.file.not.found") + ": " + e1.getLocalizedMessage(),"Whoops!"); return; } catch (IOException e1) { e1.printStackTrace(); showErrorWhenPossible(UIMessages.getInstance().getMessage("exception.io") + ": " + e1.getLocalizedMessage(),"Whoops!"); return; } catch (TransformerException e1) { e1.printStackTrace(); showErrorWhenPossible(UIMessages.getInstance().getMessage("exception.transformer") + ": " + e1.getLocalizedMessage(),"Whoops!"); return; } SwingUtilities.invokeLater( new Runnable() { public void run() { try { int nGamesUpdated = gameTableModel.addGameCatalog(doc,catalogURL,overwrite); if ( nGamesUpdated == 0 ) JOptionPane.showMessageDialog(NewLoaderGamePanel.this,"<html><p>"+UIMessages.getInstance().getMessage("gameloader.no.games.updated")+"</p>",UIMessages.getInstance().getMessage("gameloader.sync.result"),JOptionPane.INFORMATION_MESSAGE); else JOptionPane.showMessageDialog(NewLoaderGamePanel.this,"<html><p>"+nGamesUpdated + " " + UIMessages.getInstance().getMessage("gameloader.games.updated")+"</p>",UIMessages.getInstance().getMessage("gameloader.sync.result"),JOptionPane.INFORMATION_MESSAGE); } catch (MalformedGameEntryException e) { e.printStackTrace(); showError(UIMessages.getInstance().getMessage("exception.malformed.game.entry") + ": " + e.getLocalizedMessage(),"Whoops!"); return; } } } ); } }; thr.start(); } public NewLoaderGamePanel( final Frame parentFrame ) { setLayout(new BoxLayout(this,BoxLayout.LINE_AXIS)); this.parentFrame = parentFrame; gameTableModel = new GameTableModel(); try { loadGameCatalogIfPossible(new File("maincatalog.xml").toURI().toURL(),false); //this will exist only if the application has been ran in the past gameTableModel.loadGameCatalog(this.getClass().getClassLoader().getResource("catalog.xml"),false); //this will exist always, distributed with AGE gameTableModel.setCatalogWritePath(new File("maincatalog.xml")); } catch (Exception e) { showError(e.getLocalizedMessage(),"Whoops!"); e.printStackTrace(); } gameTableColumnModel = new XTableColumnModel(); gameTable = new JTable ( gameTableModel , gameTableColumnModel ); gameTable.createDefaultColumnsFromModel(); gameTableColumnModel.setAllColumnsVisible(); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(2),false); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(3),false); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(4),false); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(5),false); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(7),false); gameTableColumnModel.setColumnVisible(gameTableColumnModel.getColumnByModelIndex(8),false); tableScrollPane = new JScrollPane(gameTable); gameTable.setFillsViewportHeight(true); gameTable.setShowHorizontalLines(false); ListSelectionModel selModel = gameTable.getSelectionModel(); selModel.addListSelectionListener ( new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { //Ignore extra messages. if (e.getValueIsAdjusting()) return; ListSelectionModel lsm = (ListSelectionModel)e.getSource(); if (lsm.isSelectionEmpty()) { //no rows are selected - do nothing. } else { int selectedRow = lsm.getMinSelectionIndex(); showGameEntry ( (GameEntry) gameTableModel.getGameEntry(selectedRow) ); } } } ); gameTable.setRowSelectionInterval(0,0); downloadingButton.setEnabled(false); infoPane.setEditable(false); infoPane.setPreferredSize(new Dimension(300,400)); infoPane.setFont(new Font(Font.SANS_SERIF,Font.PLAIN,18)); infoPane.setMargin(new Insets(10,10,10,10)); Font tableFont = new Font(Font.SANS_SERIF,Font.PLAIN,18); gameTable.setFont(tableFont); FontMetrics fm = gameTable.getFontMetrics(tableFont); gameTable.setRowHeight(fm.getHeight()); gameTable.getColumn(UIMessages.getInstance().getMessage("gameinfo.downloaded")).setPreferredWidth(80); gameTable.getColumn(UIMessages.getInstance().getMessage("gameinfo.downloaded")).setMaxWidth(120); tableScrollPane.setPreferredSize(new Dimension(800,400)); JPanel leftPanel = new JPanel(); leftPanel.setLayout(new BoxLayout(leftPanel,BoxLayout.PAGE_AXIS)); leftPanel.add(tableScrollPane); JPanel leftDownPanel = new JPanel(); leftDownPanel.setLayout(new BoxLayout(leftDownPanel,BoxLayout.LINE_AXIS)); leftPanel.add(leftDownPanel); leftDownPanel.add(syncButton); JPanel rightPanel = new JPanel(); rightPanel.setLayout(new BoxLayout(rightPanel,BoxLayout.PAGE_AXIS)); rightPanel.add(infoPane); JPanel rightDownPanel = new JPanel(); rightDownPanel.setLayout(new BoxLayout(rightDownPanel,BoxLayout.LINE_AXIS)); rightPanel.add(rightDownPanel); progressBarPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5)); rightDownPanel.add(progressBarPanel); rightDownPanel.add(Box.createHorizontalGlue()); downloadOrPlayButtonPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5)); rightDownPanel.add(downloadOrPlayButtonPanel); add(leftPanel); add(rightPanel); downloadButton.addActionListener( new ActionListener() { public void actionPerformed ( ActionEvent e ) { launchDownload(); /* GameEntry toDownload = getSelectedGameEntry(); try { activateDownloadingButton(); toDownload.download(NewLoaderGamePanel.this); activatePlayButton(); //gameTableModel.fireTableDataChanged(); //game has changed to downloaded } catch (IOException e1) { showError(e1.getLocalizedMessage(),"Whoops!"); e1.printStackTrace(); activateDownloadButton(); } */ } }); playButton.addActionListener ( new ActionListener() { public void actionPerformed ( ActionEvent e ) { launchGame(); } } ); syncButton.addActionListener ( new ActionListener() { public void actionPerformed ( ActionEvent e ) { new SyncWithServerDialog(parentFrame,true,NewLoaderGamePanel.this); } } ); //double click: play or download! gameTable.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { GameEntry game = getSelectedGameEntry(); if ( game.isDownloaded() ) launchGame(); else if ( game.isDownloadable() ) { if ( !game.isDownloadInProgress() ) launchDownload(); } else JOptionPane.showMessageDialog(NewLoaderGamePanel.this, UIMessages.getInstance().getMessage("gameloader.game.missing.undownloadable"), "Oops!", JOptionPane.INFORMATION_MESSAGE); } } }); //System.err.println(gameTable.getRowCount()); //System.err.println(gameTable.getValueAt(0, 0)); } /** * Launches the game that is currently selected in the table. * Must be invoked from the Swing event dispatching thread. */ private void launchGame() { final GameEntry toPlay = getSelectedGameEntry(); if ( !toPlay.getMainResource().checkLocalFileExists() ) //game is marked as downloaded, but the game file doesn't exist (removed, moved, not actually downloaded, etc.) { toPlay.setDownloaded(false); resetProgressKeeper(toPlay); showGameEntry(toPlay); if ( toPlay.isDownloadable() ) //we ask the user if she wants to download the game { int opt = JOptionPane.showConfirmDialog(this, UIMessages.getInstance().getMessage("gameloader.game.missing.downloadable"), "Oops!", JOptionPane.YES_NO_OPTION); if ( opt == JOptionPane.YES_OPTION ) { showGameEntry(toPlay); //refresh display (downloading button, etc.) refreshTable(); launchDownload(); return; } } else //if the game is not downloadable (we don't have its remote URL) we show a dialog telling the user that life is hard { JOptionPane.showMessageDialog(NewLoaderGamePanel.this, UIMessages.getInstance().getMessage("gameloader.game.missing.undownloadable"), "Oops!", JOptionPane.INFORMATION_MESSAGE); return; } } Thread thr = new Thread() { public void run() { SwingUtilities.invokeLater( new Runnable() { public void run() { new SwingSDIInterface(toPlay.getMainResource().getLocalPath().getAbsolutePath(),false,null,null); } } ); } }; thr.start(); } private void refreshTable() { int selIndex = gameTable.getSelectedRow(); gameTableModel.fireTableDataChanged(); gameTable.setRowSelectionInterval(selIndex, selIndex); } /** * Launches the download of the game currently selected in the table. */ private void launchDownload() { final GameEntry toDownload = getSelectedGameEntry(); final DownloadProgressKeeper progressKeeper = getProgressKeeper(toDownload); progressBarPanel.removeAll(); progressBarPanel.add(progressKeeper.getBar()); activateDownloadingButton(); Thread thr = new Thread() { public void run() { try { toDownload.download(progressKeeper); SwingUtilities.invokeLater( new Runnable() { public void run() { activatePlayButton(); int selIndex = gameTable.getSelectedRow(); refreshTable(); //this fires a data changed event gameTable.setRowSelectionInterval(selIndex, selIndex); progressKeeper.progressUpdate(1.0, UIMessages.getInstance().getMessage("gameloader.game.available") ); } } ); //gameTableModel.fireTableDataChanged(); //game has changed to downloaded } catch (final IOException e1) { SwingUtilities.invokeLater( new Runnable() { public void run() { progressKeeper.progressUpdate(0.0, UIMessages.getInstance().getMessage("gameloader.download.problem") ); showError(e1.getLocalizedMessage(),"Whoops!"); e1.printStackTrace(); activateDownloadButton(); } } ); } } }; thr.start(); } private void activatePlayButton() { if ( currentDownloadOrPlayButton != null && currentDownloadOrPlayButton != playButton ) { downloadOrPlayButtonPanel.remove(currentDownloadOrPlayButton); currentDownloadOrPlayButton = null; } if ( currentDownloadOrPlayButton == null ) { downloadOrPlayButtonPanel.add(playButton); currentDownloadOrPlayButton = playButton; } downloadOrPlayButtonPanel.revalidate(); } private void activateDownloadButton() { if ( currentDownloadOrPlayButton != null && currentDownloadOrPlayButton != downloadButton ) { downloadOrPlayButtonPanel.remove(currentDownloadOrPlayButton); currentDownloadOrPlayButton = null; } if ( currentDownloadOrPlayButton == null ) { downloadOrPlayButtonPanel.add(downloadButton); currentDownloadOrPlayButton = downloadButton; } downloadOrPlayButtonPanel.revalidate(); } private void activateDownloadingButton() { if ( currentDownloadOrPlayButton != null && currentDownloadOrPlayButton != downloadingButton ) { downloadOrPlayButtonPanel.remove(currentDownloadOrPlayButton); currentDownloadOrPlayButton = null; } if ( currentDownloadOrPlayButton == null ) { downloadOrPlayButtonPanel.add(downloadingButton); currentDownloadOrPlayButton = downloadingButton; } downloadOrPlayButtonPanel.revalidate(); } //TODO: Color table entries green/red depending on downloaded or not? //TODO: Handle zipped games private String yesNo ( boolean b ) { if ( b ) return UIMessages.getInstance().getMessage("boolean.yes"); else return UIMessages.getInstance().getMessage("boolean.no"); } /** * Shows the information associated with a game entry (title, author, etc.) as well as its progress bar and the relevant button (play, download, etc.) * @param ge */ private void showGameEntry ( GameEntry ge ) { StringBuilder sb = new StringBuilder(); sb.append( UIMessages.getInstance().getMessage("gameinfo.name") + " " + ge.getTitle() + "\n" ); sb.append( UIMessages.getInstance().getMessage("gameinfo.author") + " " + ge.getAuthor() + "\n" ); if ( ge.getLanguage() != null ) sb.append( UIMessages.getInstance().getMessage("gameinfo.language") + " " + new Locale(ge.getLanguage()).getDisplayLanguage() + "\n" ); sb.append( UIMessages.getInstance().getMessage("gameinfo.date") + " " + ge.getDate() + "\n" ); sb.append( UIMessages.getInstance().getMessage("gameinfo.version") + " " + ge.getVersion() + "\n" ); sb.append( UIMessages.getInstance().getMessage("gameinfo.required") + " " + ge.getAgeVersion() + "\n" ); sb.append( UIMessages.getInstance().getMessage("gameinfo.downloaded") + " " + yesNo(ge.isDownloaded()) + "\n" ); if ( !ge.isDownloaded() ) { sb.append( UIMessages.getInstance().getMessage("gameinfo.downloadable") + " " + yesNo(ge.isDownloadable()) + "\n" ); } infoPane.setText(sb.toString()); infoPane.revalidate(); //show the progress bar if applicable progressBarPanel.removeAll(); progressBarPanel.add(getProgressKeeper(ge).getBar()); progressBarPanel.revalidate(); //and show the play button or the download button as needed. if ( ge.isDownloaded() ) activatePlayButton(); else if ( ge.isDownloadInProgress() ) activateDownloadingButton(); else { activateDownloadButton(); downloadButton.setEnabled(ge.isDownloadable()); } } //temporary public void progressUpdate(double progress , String progressString) { System.err.println("Progress " + progress); infoPane.setText(infoPane.getText()+ progressString + ": " + progress + "\n"); } public void addGameEntry ( GameEntry ge , boolean overwrite ) { gameTableModel.addGameEntry(ge,false); } }