New Dialog

Hi all,

This new dialog stuff uses an interface to communicate between jme and the required dialog (can be custom made). Below is the new classes and the modifications of the major ones.



Applications that directly use "properties" to create the window will have to be refactored slightly, the refactoration is obvious. I will not post those here as they are too many. The changes are from "getWidth()" to "getDisplayWidth()"…etc. Obvious :slight_smile:



com.jme.util.LWJGLDisplayModeSorter


package com.jme.util.lwjgl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;

import com.jme.system.DisplayModeDesc;

public class LWJGLDisplayModeSorter {

   private ArrayList displayModes;

   public LWJGLDisplayModeSorter() {
      DisplayMode[] modes = null;
      try {
         modes = Display.getAvailableDisplayModes();
      } catch (LWJGLException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }

      Arrays.sort(modes, new DisplayModeSorter());
      
      displayModes = new ArrayList();
      for (int i = 0; i < modes.length; i++) {
         DisplayModeDesc desc = new DisplayModeDesc();
         DisplayMode mode = modes[i];
         desc.setDepth(mode.getBitsPerPixel());
         desc.setFrequency(mode.getFrequency());
         desc.setHeight(mode.getHeight());
         desc.setWidth(mode.getWidth());
         displayModes.add(desc);
      }
   }

   public ArrayList getOrderedDisplayModes() {
      return displayModes;
   }

   /**
    * Utility class for sorting <code>DisplayMode</code>s. Sorts by
    * resolution, then bit depth, and then finally refresh rate.
    */
   class DisplayModeSorter implements Comparator {
      /**
       * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
       */
      public int compare(Object o1, Object o2) {
         DisplayMode a = (DisplayMode) o1;
         DisplayMode b = (DisplayMode) o2;

         // Width
         if (a.getWidth() != b.getWidth())
            return (a.getWidth() > b.getWidth()) ? 1 : -1;
         // Height
         if (a.getHeight() != b.getHeight())
            return (a.getHeight() > b.getHeight()) ? 1 : -1;
         // Bit depth
         if (a.getBitsPerPixel() != b.getBitsPerPixel())
            return (a.getBitsPerPixel() > b.getBitsPerPixel()) ? 1 : -1;
         // Refresh rate
         if (a.getFrequency() != b.getFrequency())
            return (a.getFrequency() > b.getFrequency()) ? 1 : -1;
         // All fields are equal
         return 0;
      }
   }

}



com.jme.system.DisplayModeDesc


package com.jme.system;

public class DisplayModeDesc {

   private int width, height, frequency, depth;

   public DisplayModeDesc() {
      width = 0;
      height = 0;
      frequency = 0;
      depth = 0;
   }

   public int getDepth() {
      return depth;
   }

   public void setDepth(int depth) {
      this.depth = depth;
   }

   public int getFrequency() {
      return frequency;
   }

   public void setFrequency(int frequency) {
      this.frequency = frequency;
   }

   public int getHeight() {
      return height;
   }

   public void setHeight(int height) {
      this.height = height;
   }

   public int getWidth() {
      return width;
   }

   public void setWidth(int width) {
      this.width = width;
   }
}



com.jme.system.RendererDesc


package com.jme.system;

public interface RendererDesc {
   
   public int getDisplayWidth();
   public int getDisplayHeight();
   public int getDisplayDepth();
   public int getFrequency();
   public boolean isFullscreen();
   public String getRenderer();

}



com.jme.system.lwjgl.LWJGLPropertiesDialog2


package com.jme.system.lwjgl;

import java.awt.BorderLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.logging.Level;

import javax.swing.DefaultComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.UIManager;

import com.jme.system.DisplayModeDesc;
import com.jme.system.DisplaySystem;
import com.jme.system.JmeException;
import com.jme.system.PropertiesIO;
import com.jme.system.RendererDesc;
import com.jme.util.LoggingSystem;
import com.jme.util.lwjgl.LWJGLDisplayModeSorter;

public class LWJGLPropertiesDialog2 extends JDialog implements RendererDesc {
   private static final long serialVersionUID = 1L;
   
   // the display mode
   private PropertiesIO source;
   
   // UI components
   private JCheckBox fullscreenBox = null;
   private JComboBox displayResCombo = null;
   private JComboBox colorDepthCombo = null;
   private JComboBox displayFreqCombo = null;
   private JComboBox rendererCombo = null;
   private JLabel icon = null;

   // Title Image
   private URL imageFile = null;

   // the display modes
   private ArrayList modes;

   /**
    * Constructor for the <code>PropertiesDialog</code>. Creates a
    * properties dialog initialized for the primary display.
    *
    * @param source
    *            the <code>PropertiesIO</code> object to use for working with
    *            the properties file.
    * @param imageFile
    *            the image file to use as the title of the dialog;
    *            <code>null</code> will result in to image being displayed
    * @throws JmeException
    *             if the source is <code>null</code>
    */
   public LWJGLPropertiesDialog2(String imageFile) {
      this(getURL(imageFile));
   }

   public LWJGLPropertiesDialog2(URL image) {
      this.source = new PropertiesIO("properties.cfg");
      this.imageFile = image;
      
      modes = new LWJGLDisplayModeSorter().getOrderedDisplayModes();

      try {
         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      } catch (Exception e) {
         LoggingSystem.getLogger().log(Level.WARNING, "Could not set native look and feel.");
      }

      addWindowListener(new WindowAdapter() {
         public void windowClosing(WindowEvent e) {
            dispose();
            System.exit(0);
         }
      });

      setTitle("Select Display Settings");

      // The panels...
      JPanel mainPanel = new JPanel();
      JPanel centerPanel = new JPanel();
      JPanel optionsPanel = new JPanel();
      JPanel buttonPanel = new JPanel();
      // The buttons...
      JButton ok = new JButton("Ok");
      JButton cancel = new JButton("Cancel");

      icon = new JLabel(imageFile != null ? new ImageIcon(imageFile) : null);

      mainPanel.setLayout(new BorderLayout());

      centerPanel.setLayout(new BorderLayout());

      KeyListener aListener = new KeyAdapter() {
         public void keyPressed(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_ENTER) {
               if (verifyAndSaveCurrentSelection())
                  dispose();
            }
         }
      };

      displayResCombo = setUpResolutionChooser();
      displayResCombo.addKeyListener(aListener);
      colorDepthCombo = new JComboBox();
      colorDepthCombo.addKeyListener(aListener);
      displayFreqCombo = new JComboBox();
      displayFreqCombo.addKeyListener(aListener);
      fullscreenBox = new JCheckBox("Fullscreen?");
      fullscreenBox.setSelected(source.getFullscreen());
      rendererCombo = setUpRendererChooser();
      rendererCombo.addKeyListener(aListener);

      updateDisplayChoices();

      optionsPanel.add(displayResCombo);
      optionsPanel.add(colorDepthCombo);
      optionsPanel.add(displayFreqCombo);
      optionsPanel.add(fullscreenBox);
      optionsPanel.add(rendererCombo);

      // Set the button action listeners. Cancel disposes without saving, OK
      // saves.
      ok.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            if (verifyAndSaveCurrentSelection())
               dispose();
         }
      });

      cancel.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            dispose();
            System.exit(0);
         }
      });

      buttonPanel.add(ok);
      buttonPanel.add(cancel);

      if (icon != null)
         centerPanel.add(icon, BorderLayout.NORTH);
      centerPanel.add(optionsPanel, BorderLayout.SOUTH);

      mainPanel.add(centerPanel, BorderLayout.CENTER);
      mainPanel.add(buttonPanel, BorderLayout.SOUTH);

      this.getContentPane().add(mainPanel);

      pack();
      center();
      setVisible(true);
      toFront();
   }

   /**
    * <code>center</code> places this <code>PropertiesDialog</code> in the
    * center of the screen.
    */
   private void center() {
      int x, y;
      x = (Toolkit.getDefaultToolkit().getScreenSize().width - this.getWidth()) / 2;
      y = (Toolkit.getDefaultToolkit().getScreenSize().height - this.getHeight()) / 2;
      this.setLocation(x, y);
   }

   /**
    * <code>verifyAndSaveCurrentSelection</code> first verifies that the
    * display mode is valid for this system, and then saves the current
    * selection as a properties.cfg file.
    *
    * @return if the selection is valid
    */
   private boolean verifyAndSaveCurrentSelection() {
      String display = (String) displayResCombo.getSelectedItem();

      int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
      display = display.substring(display.indexOf(" x ") + 3);
      int height = Integer.parseInt(display);

      String depthString = (String) colorDepthCombo.getSelectedItem();
      int depth = Integer.parseInt(depthString.substring(0, depthString.indexOf(" ")));

      String freqString = (String) displayFreqCombo.getSelectedItem();
      int freq = Integer.parseInt(freqString.substring(0, freqString.indexOf(" ")));

      boolean fullscreen = fullscreenBox.isSelected();
      // FIXME: Does not work in Linux
      /*
       * if (!fullscreen) { //query the current bit depth of the desktop int
       * curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
       * .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
       * curDepth) { showError(this,"Cannot choose a higher bit depth in
       * windowed " + "mode than your current desktop bit depth"); return
       * false; } }
       */

      String renderer = (String) rendererCombo.getSelectedItem();

      // test valid display mode
      DisplaySystem disp = DisplaySystem.getDisplaySystem(renderer);
      boolean valid = (disp != null) ? disp.isValidDisplayMode(width, height, depth, freq)
            : false;

      if (valid)
         // use the PropertiesIO class to save it.
         source.save(width, height, depth, freq, fullscreen, renderer);
      else
         JOptionPane.showMessageDialog(this,
               "Your monitor claims to not support the display mode you've selected.n"
                     + "The combination of bit depth and refresh rate is not supported.");

      return valid;
   }

   private void updateDisplayChoices() {
      String resolution = (String) displayResCombo.getSelectedItem();
      // grab available depths
      String[] depths = getDepths(resolution, modes);
      colorDepthCombo.setModel(new DefaultComboBoxModel(depths));
      colorDepthCombo.setSelectedItem(source.getDepth() + " bpp");
      // grab available frequencies
      String[] freqs = getFrequencies(resolution, modes);
      displayFreqCombo.setModel(new DefaultComboBoxModel(freqs));
      displayFreqCombo.setSelectedItem(source.getFreq() + " Hz");
   }

   /**
    * Reutrns every unique resolution from an array of <code>DisplayMode</code>s.
    */
   private String[] getResolutions(ArrayList modes) {
      ArrayList resolutions = new ArrayList(modes.size());
      for (int i = 0; i < modes.size(); i++) {
         String res = ((DisplayModeDesc) modes.get(i)).getWidth() + " x "
               + ((DisplayModeDesc) modes.get(i)).getHeight();
         if (!resolutions.contains(res))
            resolutions.add(res);
      }

      String[] res = new String[resolutions.size()];
      resolutions.toArray(res);
      return res;
   }

   /**
    * Returns every possible bit depth for the given resolution.
    */
   private String[] getDepths(String resolution, ArrayList modes) {
      ArrayList depths = new ArrayList(4);
      for (int i = 0; i < modes.size(); i++) {
         // Filter out all bit depths lower than 16 - Java incorrectly
         // reports
         // them as valid depths though the monitor does not support them
         if (((DisplayModeDesc) modes.get(i)).getDepth() < 16)
            continue;

         String res = ((DisplayModeDesc) modes.get(i)).getWidth() + " x "
               + ((DisplayModeDesc) modes.get(i)).getHeight();
         String depth = String.valueOf(((DisplayModeDesc) modes.get(i)).getDepth()) + " bpp";
         if (res.equals(resolution) && !depths.contains(depth))
            depths.add(depth);
      }

      String[] res = new String[depths.size()];
      depths.toArray(res);
      return res;
   }

   /**
    * Returns every possible refresh rate for the given resolution.
    */
   private String[] getFrequencies(String resolution, ArrayList modes) {
      ArrayList freqs = new ArrayList(4);
      for (int i = 0; i < modes.size(); i++) {
         String res = ((DisplayModeDesc) modes.get(i)).getWidth() + " x "
               + ((DisplayModeDesc) modes.get(i)).getHeight();
         String freq = String.valueOf(((DisplayModeDesc) modes.get(i)).getFrequency()) + " Hz";
         if (res.equals(resolution) && !freqs.contains(freq))
            freqs.add(freq);
      }

      String[] res = new String[freqs.size()];
      freqs.toArray(res);
      return res;
   }

   /**
    * <code>setUpChooser</code> retrieves all available display modes and
    * places them in a <code>JComboBox</code>. The resolution specified by
    * PropertiesIO is used as the default value.
    *
    * @return the combo box of display modes.
    */
   private JComboBox setUpResolutionChooser() {
      String[] res = getResolutions(modes);
      JComboBox resolutionBox = new JComboBox(res);

      resolutionBox.setSelectedItem(source.getWidth() + " x " + source.getHeight());
      resolutionBox.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            updateDisplayChoices();
         }
      });

      return resolutionBox;
   }
   
   /**
     * <code>setUpRendererChooser</code> sets the list of available renderers.
     * Data is obtained from the <code>DisplaySystem</code> class. The
     * renderer specified by PropertiesIO is used as the default value.
     *
     * @return the list of renderers.
     */
    private JComboBox setUpRendererChooser() {
        String modes[] = DisplaySystem.rendererNames;
        JComboBox nameBox = new JComboBox(modes);
        nameBox.setSelectedItem(source.getRenderer());
        return nameBox;
    }

   /**
    * Utility method for converting a String denoting a file into a URL.
    *
    * @return a URL pointing to the file or null
    */
   private static URL getURL(String file) {
      URL url = null;
      try {
         url = new URL("file:" + file);
      } catch (MalformedURLException e) {
      }
      return url;
   }

   public int getDisplayWidth() {
      return source.getWidth();
   }

   public int getDisplayHeight() {
      return source.getHeight();
   }

   public int getDisplayDepth() {
      return source.getDepth();
   }

   public int getFrequency() {
      return source.getFreq();
   }

   public boolean isFullscreen() {
      return source.getFullscreen();
   }
   
   public String getRenderer() {
      return source.getRenderer();
   }

}



Thats the new classes, the modification for AbstractGame is:


// from: protected PropertiesIO properties
/** Game display properties. */
protected RendererDesc properties;

// change getAttributes() to:
/**
    * <code>getAttributes</code> attempts to first obtain the properties
    * information from the "properties.cfg" file, then a dialog depending on
    * the dialog behaviour.
    */
   protected void getAttributes() {
      if (properties == null) {
         if (dialogBehaviour == FIRSTRUN_OR_NOCONFIGFILE_SHOW_PROPS_DIALOG
               || dialogBehaviour == ALWAYS_SHOW_PROPS_DIALOG) {

            LWJGLPropertiesDialog2 dialog = new LWJGLPropertiesDialog2(dialogImage);
            setRendererDesc(dialog);

            while (dialog.isVisible()) {
               try {
                  Thread.sleep(5);
               } catch (InterruptedException e) {
                  LoggingSystem.getLogger().log(Level.WARNING,
                        "Error waiting for dialog system, using defaults.");
               }
            }
         }
      }
   }

// add the following method:
/**
    * <code>setRendererDesc</code> sets the RendererDesc used to initialise
    * the window. This is useful if the user wants to implement their own
    * custom dialog
    *
    * @param desc
    */
   public void setRendererDesc(RendererDesc desc) {
      this.properties = desc;
   }



And done.

Now to create your own dialog, simply implement RendererDesc, before calling AbstractGame.start(), call AbstractGame.setRendererDesc(myDialog); then call AbstractGame.start();

I think this is about as modular and easy as you can get it to be... :)

/DP

Is it necessary to introduce all that  :?



Custom Game classes can override getAttributes and subclass PropertiesIO and everything should be fine. No refactoring needed from my point of view…



E.g. custom dialogs with "max fps" and alike should be no problem with the current design already, are they?

Say i wanted to have the Ok, cancel buttons at the left, the drop down menus at the right and the image at the centre, i can do that now, whereas before I couldn't (easily).



Take Per's MarbleFun for example, when he implemented his own dialog, it took him sometime and he needed intimate knowledge of how AbstractGame works, were from a user's point of view, that shouldn't be necessary…So from a users perspective, "max fps" things are a problem IMO.



DP

ok, maybe one should simply put an example for that in FixedFramerateGame to let the user choose the max fps.

I will put something together as soon as I have time - maybe it looks easier then and don't need 'intimate knowledge' any more  :wink:

Why would you ever show the properties dialog to a USER? Much give the user a FPS lock choice? The only people who should make these decisions are developers testing various settings. Hence, properties dialog should never be shown in a released product, it's a developer tool.



A user will want to set his Resolution and nothing more, and you shouldn't be presenting them with the properties dialog for that. That should be in-game.

Alot of games ive recently played presented their dialog first. (not maxfps, but general system setup)



Prince of Persia: Warrior within

TrackMania: Sunrise



those two are the ones that poped in my head, i can name a few more if I went and thought about it…



Heck, trackmania has options for Antialiasing, look perspective, Max filtering (Ansiotropic), Shadow algorithm, texture quality, shader quality…etc. And thats only part of the graphics settings, theres audio, networking, Input, and general game settings.



It also has a button to update the game without fully launching the game so you can do other things while its updating…



Enough reasons? DP

Doesn't mean it's a smart thing to do. Exiting and restarting a game so you can change a visual setting is cumbersome.

I agree with you mojo - in a finished game these dialogs should be gone.



Though it's nice for testing / rapid prototyping / quick shots - so why not letting the devs add some of their own parameters there?



Btw. Choosing Max FPS does not really make sense, using the refresh rate as max fps should be fine - but as an example for changing the dialog it suites

Giving the user the option to change setting outside of the game does have its advantages though, like what if a user makes a setting change that causes the game to crash (i do this all the time), if the only way to change these setting is in the game then how can a normal user easily sort this?



Matty.

<sarcasim>

So you would prefer to update the game files while your in the game, fullscreen and you can't do anything else but wait for the game to update right?



I also take it you like queing for hours on end to get a chocolate bar, and grid locks are your favourite past-time.

</sarcasim>



I dont understand why you are reluctant to take this in, I want it for my game, im sure other people want it in theirs, yet you are restricting us of what to do and it will have no impact on yours method anyway. Meh, its your engine, you do whatever you want with it

I like the idea of have resolution settings and such on the outside, that having been said apart from fullscreen, resolution and what not, what else would you want change on the outside, because fps seems a "dangerous" option to leave up to the player unless its presets defined by the developer who would best know the conditions under which the app would run cleanly.





besides it open source right always good to have options



we often say in my country "why get angry over small shit"

I removed the launcher-window from Marble Fun quite some time ago, exactly for the reasons people have mentioned above.



But that isn't to say it wasn't useful during development, so a generic way to alter the functionality of the standard dialog couldn't be that way off, could it? On the other hand it was a very easy procedure to alter the standard dialog to suit my needs, so if making it generic and stuff would require an ugly solution (not to say your is DP – I havn't even glanced at it) I doubt it'd be worth it.

First, I'm not sure why you are getting pissed off DP.



Secondly, it's sarcasm not sarcasim.



Third, let's look at what happens when you change a setting: In Game versus Out of Game…



In Game:


  1. Press escape.
  2. Select menu item.
  3. Change settings (this can include EVERYTHING you mentioned before, anisotropic, shadows, resolution, etc. Plenty of games let you alter everything).
  4. Apply
  5. Wait for screen to refresh.



    Out of Game:
  6. press escape.

    1a. Save game?
  7. exit.
  8. Wait for game to clean up, release resources.
  9. Start game.
  10. Get Dialog.
  11. Change settings.
  12. Apply.
  13. Wait for game to load (including all game data, models, levels, etc).

    8a. load saved game?



    Now… imagine if you are playing a network game. It just got much worse.



    This is what I meant by cumbersome. No, I don't like queuing for chocolate bars, and I don't like exiting a game because I need to change a setting so it runs faster.



    Forth, none of this is directed at your code. It could very well be useful for developers, I don't know, I haven't looked at it yet. In fact, I was responding to Irrisor, not you.




I have written a short example for putting a custom dialog without changing anything in jME:

The sketched Version looks like this (not compilable):


    protected void getAttributes() {
        properties = new PropertiesIO("properties.cfg");
        properties.load();

        if ( dialogShouldBeShown )
        {
           MyDialog dialog = new MyDialog( properties, getDialogImage() );
           //then show the custom dialog and wait for an 'ok' (modal)
           dialog.setVisible( true );
           //read custom additions (should be done in dialog class, too)
           chosenFPS = dialog.getMaxFPS();
        }
    }



The following code could be put into FixedFramerateGame to show a max framerate option in the starting dialog (I have tested it). Though I can't recommed to do this without writing an own dialog class (which is not possible at this time without copying the whole standard dialog, as it is final and does some nasty things in the constructor). Additionally the dialogBehaviour is not regarded here (as it is private in AbstractGame and has no accessor yet).


    protected void getAttributes() {
        properties = new PropertiesIO("properties.cfg");
        properties.load();

        //following is the dialog setup, which should be put into a separate class
        JPanel frameratePanel = new JPanel();
        frameratePanel.add( new JLabel("max. fps ") );
        JTextField fpsField = new JTextField( "60" );  //could be read from properties here
        frameratePanel.add( fpsField );

        LWJGLPropertiesDialog dialog = new LWJGLPropertiesDialog(properties, (URL) null);
        dialog.setVisible( false );
        // the following line is very bad - make an own dialog instead
        ((JComponent)((JComponent)dialog.getContentPane().getComponent( 0 )).getComponent(1)).add( frameratePanel );
        dialog.pack();
        dialog.setModal( true );

        //then show the custom dialog and wait for an 'ok' (modal)
        dialog.setVisible( true );

        //read custom additions (should be done in dialog class, too)
        try {
            chosenFPS = Integer.valueOf( fpsField.getText() ).intValue();
            //could be put in properties here to store it
        } catch ( NumberFormatException e ) {
            System.err.println( "Max fps was not a valid integer!" );
            chosenFPS = 60;
        }
    }


the chosenFPS must then be used in start() instead of the hardcoded 60

After viewing the current properties dialog stuff I agree with DP that it could be improved to become more flexible - but at different places:
The dialog class
- should not be final and provide some protected accessors for useful gui hooks
- abstract from the used display system
- should be modal to avoid busy waiting
AbstractGame
- should have an extracted method for showing the dialog to allow custom dialogs without copying code

I'll work on that if someone wants it (don't need it myself, yet)

Mojo, in that case you should make your posts clearer to whoever your addressing as somepeople are easily offended (me) and can be pissed off quite easily too. And you know that english is not my first language (ive only learnt english 3 years ago), but I do think I hide it well, so really, that comment was unnecessary.



Main - Extending one of the many Game classes we have…


private static MyDialog myDiag;

public void initSystem() {
 display = ....
 this.setFrameRate(myDiag.getFrameRate());
}

public static void main(String[] args) {
Main app = new Main();
myDiag = new MyDiag();
app.setRendererDesc(myDiag);
app.start();
}



I also agree with irrisor about the dialog tho, it needs better code in there so we can extend it easily.

DP