StandardGame, wish to show Exception dialog

jME 1.0



Whenever the default UncaughtExceptionHandler catches and exception, I'd like to quit the game as usual and then show a dialog box or something so the user can see the exception.



I tried some stuff with JDialog but it just wouldn't show up, and I'm not very experienced in coding swing. How would you guys do it?



I tried calling this in the method uncaughtException:



public void showExceptionDialog(final Throwable ex, final Thread t) {
      final JFrame parent = new JFrame() {
         public Dimension getPreferredSize() {
            return new Dimension(200,100);
         }
      };
      parent.setTitle("Debugging frame");
      parent.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      parent.pack();
      parent.setVisible(false);            
      JPanel panel = new JPanel();
      JPanel tiny = new JPanel();
      tiny.setLayout(new BoxLayout(tiny, BoxLayout.Y_AXIS));
      tiny.add(new JLabel(ex.getClass().getName()));

      JTextField field = new JTextField(ex.getMessage(), 15);
      field.setEditable(false);
      tiny.add(field);
      panel.add(tiny);

      JButton button = new JButton("details");
      button.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ev) {
            JTextArea ta = new JTextArea(ex.getMessage());
            ta.setEditable(false);
            ta.setCaretPosition(0);

            JDialog dialog = new JDialog(parent, "debug");
            JPanel panel = new JPanel(new BorderLayout());
            panel.setPreferredSize(new Dimension(400, 200));
            panel.add(new JScrollPane(ta));
            dialog.getContentPane().add(panel);
            dialog.pack();
            dialog.setVisible(true);
         }
      });
      panel.add(button);
      JOptionPane.showMessageDialog(parent, panel, "Exception encountered",
            JOptionPane.ERROR_MESSAGE);
   }



maybe you need to do this stuff in the swing thread with SwingUtilities.invokeLater()

Thanks, but it didn't work.


public void uncaughtException(Thread t, Throwable e) {
final Thread ft = t;
final Throwable fe = e;
logger.log(Level.SEVERE, "Main game loop broken by uncaught exception", e);
game.shutdown();
game.cleanup();
game.quit();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
showExceptionDialog(fe, ft);
}
});
}


I pounded this out because I wanted a similar handler. This is for jME 2.0, but I’m sure you can back-port easily if it doesn’t already ‘just work’.



@Tobias

You were really close!  :wink: I’ve never seen a panel added to a JOptionPane before, so I don’t know if it works that way.



The uncaught exception handler will display this dialog…





Upon clicking the details button the dialog is changed to this…





And here is the complete test…


import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.Callable;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;

import com.jme.util.GameTaskQueueManager;
import com.jmex.editors.swing.settings.GameSettingsPanel;
import com.jmex.game.StandardGame;
import com.jmex.game.state.GameState;
import com.jmex.game.state.GameStateManager;

/**
 * Testing an UncaughtExceptionHandler implementation with StandardGame.
 *
 * No liscense attached, free to use or modify in absolutely any form with or without
 * credit given.
 *
 * @author Andrew Carter
 */
public class TestExceptionHandler {

   public static void main(String[] args) throws Exception {

      StandardGame game = new StandardGame("A Simple Test");

      if(GameSettingsPanel.prompt(game.getSettings())) {

         game.start();

         game.setUncaughtExceptionHandler(new ExceptionHandler(game));

         GameTaskQueueManager.getManager().update(new Callable<Void>() {

            public Void call() throws Exception {

               ExceptionGameState state = new ExceptionGameState();

               GameStateManager.getInstance().attachChild(state);
               state.setActive(true);

               return null;
            }
         });
      }
   }

   /**
    * Uncaught exception handler showing the use of a Swing dialog.
    */
   public static class ExceptionHandler implements UncaughtExceptionHandler {

      private StandardGame game = null;

      public ExceptionHandler(StandardGame game) {

         this.game = game;
      }

      public void uncaughtException(Thread t, final Throwable e) {

         try {
            SwingUtilities.invokeLater(new Runnable() {

               public void run() {

                  ExceptionDialog dialog = new ExceptionDialog("<Your game>", e);
                  center(dialog);
                  dialog.setVisible(true);

                  // Dispose of the dialog to prevent the swing thread
                  // from keeping the application alive.
                  dialog.dispose();
               }
            });
         }
         catch (Exception ex) {
            ex.printStackTrace();
         }

         // After getting swing started, be sure to shutdown standard game
         game.shutdown();
      }
   }

   /**
    * Gamestate used to throw an uncaught exception.
    */
   public static class ExceptionGameState extends GameState {

      @Override
      public void cleanup() {

      }

      @Override
      public void render(float tpf) {

      }

      @Override
      public void update(float tpf) {

         throw new NullPointerException("This is an uncaught exception.");
      }
   }

   /**
    * This dialog will present the details of a throwable exception to a user.
    */
   public static class ExceptionDialog extends JDialog {

      private static final long serialVersionUID = 1L;

      public ExceptionDialog(String appName, final Throwable e) {

         setTitle(appName + " Error");
         setModal(true);
         setResizable(false);
         setPreferredSize(new Dimension(500, 150));

         final JPanel contentPane = new JPanel();
         contentPane.setLayout(new BorderLayout());
         contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

         setContentPane(contentPane);

         JPanel messagePanel = new JPanel();
         messagePanel.setLayout(new BorderLayout());
         contentPane.add(messagePanel, BorderLayout.PAGE_START);

         messagePanel.add(new JLabel(appName + " has been terminated due to an unrecoverable error."), BorderLayout.PAGE_START);

         JLabel errorType = new JLabel(e.getClass().getSimpleName());
         errorType.setFont(errorType.getFont().deriveFont(Font.PLAIN));
         messagePanel.add(errorType, BorderLayout.CENTER);

         JPanel buttonPanel = new JPanel();
         buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT, 10, 0));

         JButton close = new JButton("Close");
         close.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {

               setVisible(false);
            }
         });

         buttonPanel.add(close);

         final JButton details = new JButton("Details");
         details.addActionListener(new ActionListener() {

            /**
             * When the user clicks the details button, add the stack trace
             * to the dialog and disable the details button.
             */
            public void actionPerformed(ActionEvent evt) {

               JPanel detailsPanel = new JPanel();
               detailsPanel.setLayout(new BorderLayout());
               detailsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));

               contentPane.add(detailsPanel, BorderLayout.CENTER);

               JLabel detailsLabel = new JLabel("Error Details");
               detailsPanel.add(detailsLabel, BorderLayout.PAGE_START);

               /*
                * This special construction of a JTextPane prevents line
                * wrapping, allowing the scrollpane to do its job
                * horizontally.
                */
               final JTextPane detailsText = new JTextPane() {

                  private static final long serialVersionUID = 1L;

                  public boolean getScrollableTracksViewportWidth() {
                     return (getSize().width < getParent().getSize().width);
                  }

                  public void setSize(Dimension d) {
                     if(d.width < getParent().getSize().width) {
                        d.width = getParent().getSize().width;
                     }
                     super.setSize(d);
                  }
               };

               detailsText.setEditable(false);
               /* Uncomment this line if you don't like the white background of the text pane */
               //detailsText.setBackground(contentPane.getBackground());

               // Print the stack trace to a string...
               StringWriter sw = new StringWriter();
               PrintWriter pw = new PrintWriter(sw, true);
               e.printStackTrace(pw);
               pw.flush();
               sw.flush();
               detailsText.setText(sw.toString());

               JScrollPane scrollPane = new JScrollPane(detailsText);

               detailsPanel.add(scrollPane, BorderLayout.CENTER);

               details.setEnabled(false);

               // expand the dialog size to make room for the details
               setPreferredSize(new Dimension(500, 300));

               pack();
            }
         });

         buttonPanel.add(details);
         contentPane.add(buttonPanel, BorderLayout.PAGE_END);

         pack();
      }
   }

   /**
    * Utility method for centering swing components onscreen.
    */
   public static void center(Component component) {

      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
      Dimension frameSize = component.getSize();

      if(frameSize.height > screenSize.height) {
         frameSize.height = screenSize.height;
      }
      if(frameSize.width > screenSize.width) {
         frameSize.width = screenSize.width;
      }

      component.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
   }
}



Thanks, mate.



I actually got it working by try-catching the entire update/render loop since I read somewhere that the application is "already closing" when the uncaughtExceptionHandler sets in. Whatever that means.



I like your solution better, naturally, and will try it out. The whole panel on the pane thing was shamelessly stolen from somewhere.

Cool. Thanks.



Another brilliant piece of code I grab from the forums.



At this time I am starting to wonder if I have coded a single line  .