/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.app; import com.jme3.system.AppSettings; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import javax.swing.*; /** * <code>PropertiesDialog</code> provides an interface to make use of the * <code>GameSettings</code> class. The <code>GameSettings</code> object * is still created by the client application, and passed during construction. * * @see com.jme.system.GameSettings * @author Mark Powell * @author Eric Woroshow * @author Joshua Slack - reworked for proper use of GL commands. * @version $Id: LWJGLPropertiesDialog.java 4131 2009-03-19 20:15:28Z blaine.dev $ */ public final class SettingsDialog extends JDialog { public static interface SelectionListener { public void onSelection(int selection); } private static final Logger logger = Logger.getLogger(SettingsDialog.class.getName()); private static final long serialVersionUID = 1L; public static final int NO_SELECTION = 0, APPROVE_SELECTION = 1, CANCEL_SELECTION = 2; // connection to properties file. private final AppSettings source; // Title Image private URL imageFile = null; // Array of supported display modes private DisplayMode[] modes = null; // Array of windowed resolutions private String[] windowedResolutions = {"320 x 240", "640 x 480", "800 x 600", "1024 x 768", "1152 x 864", "1280 x 720"}; // UI components private JCheckBox vsyncBox = null; private JCheckBox fullscreenBox = null; private JComboBox displayResCombo = null; private JComboBox colorDepthCombo = null; private JComboBox displayFreqCombo = null; // private JComboBox rendererCombo = null; private JComboBox antialiasCombo = null; private JLabel icon = null; private int selection = 0; private SelectionListener selectionListener = null; /** * Constructor for the <code>PropertiesDialog</code>. Creates a * properties dialog initialized for the primary display. * * @param source * the <code>AppSettings</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 NullPointerException * if the source is <code>null</code> */ public SettingsDialog(AppSettings source, String imageFile, boolean loadSettings) { this(source, getURL(imageFile), loadSettings); } /** * Constructor for the <code>PropertiesDialog</code>. Creates a * properties dialog initialized for the primary display. * * @param source * the <code>GameSettings</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 * @param loadSettings * @throws JmeException * if the source is <code>null</code> */ public SettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) { if (source == null) { throw new NullPointerException("Settings source cannot be null"); } this.source = source; this.imageFile = imageFile; // setModalityType(Dialog.ModalityType.APPLICATION_MODAL); setModal(true); AppSettings registrySettings = new AppSettings(true); String appTitle; if(source.getTitle()!=null){ appTitle = source.getTitle(); }else{ appTitle = registrySettings.getTitle(); } try { registrySettings.load(appTitle); } catch (BackingStoreException ex) { logger.log(Level.WARNING, "Failed to load settings", ex); } if (loadSettings) { source.copyFrom(registrySettings); } else if(!registrySettings.isEmpty()) { source.mergeFrom(registrySettings); } GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); modes = device.getDisplayModes(); Arrays.sort(modes, new DisplayModeSorter()); createUI(); } public void setSelectionListener(SelectionListener sl) { this.selectionListener = sl; } public int getUserSelection() { return selection; } private void setUserSelection(int selection) { this.selection = selection; selectionListener.onSelection(selection); } /** * <code>setImage</code> sets the background image of the dialog. * * @param image * <code>String</code> representing the image file. */ public void setImage(String image) { try { URL file = new URL("file:" + image); setImage(file); // We can safely ignore the exception - it just means that the user // gave us a bogus file } catch (MalformedURLException e) { } } /** * <code>setImage</code> sets the background image of this dialog. * * @param image * <code>URL</code> pointing to the image file. */ public void setImage(URL image) { icon.setIcon(new ImageIcon(image)); pack(); // Resize to accomodate the new image setLocationRelativeTo(null); // put in center } /** * <code>showDialog</code> sets this dialog as visble, and brings it to * the front. */ public void showDialog() { setLocationRelativeTo(null); setVisible(true); toFront(); } /** * <code>init</code> creates the components to use the dialog. */ private void createUI() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { logger.warning("Could not set native look and feel."); } addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { setUserSelection(CANCEL_SELECTION); dispose(); } }); if (source.getIcons() != null) { safeSetIconImages( (List<BufferedImage>) Arrays.asList((BufferedImage[]) source.getIcons()) ); } 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()) { setUserSelection(APPROVE_SELECTION); dispose(); } } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { setUserSelection(CANCEL_SELECTION); dispose(); } } }; displayResCombo = setUpResolutionChooser(); displayResCombo.addKeyListener(aListener); colorDepthCombo = new JComboBox(); colorDepthCombo.addKeyListener(aListener); displayFreqCombo = new JComboBox(); displayFreqCombo.addKeyListener(aListener); antialiasCombo = new JComboBox(); antialiasCombo.addKeyListener(aListener); fullscreenBox = new JCheckBox("Fullscreen?"); fullscreenBox.setSelected(source.isFullscreen()); fullscreenBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { updateResolutionChoices(); } }); vsyncBox = new JCheckBox("VSync?"); vsyncBox.setSelected(source.isVSync()); // rendererCombo = setUpRendererChooser(); // rendererCombo.addKeyListener(aListener); updateResolutionChoices(); updateAntialiasChoices(); displayResCombo.setSelectedItem(source.getWidth() + " x " + source.getHeight()); colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp"); optionsPanel.add(displayResCombo); optionsPanel.add(colorDepthCombo); optionsPanel.add(displayFreqCombo); optionsPanel.add(antialiasCombo); optionsPanel.add(fullscreenBox); optionsPanel.add(vsyncBox); // 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()) { setUserSelection(APPROVE_SELECTION); dispose(); } } }); cancel.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setUserSelection(CANCEL_SELECTION); dispose(); } }); 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(); } /* Access JDialog.setIconImages by reflection in case we're running on JRE < 1.6 */ private void safeSetIconImages(List<? extends Image> icons) { try { // Due to Java bug 6445278, we try to set icon on our shared owner frame first. // Otherwise, our alt-tab icon will be the Java default under Windows. Window owner = getOwner(); if (owner != null) { Method setIconImages = owner.getClass().getMethod("setIconImages", List.class); setIconImages.invoke(owner, icons); return; } Method setIconImages = getClass().getMethod("setIconImages", List.class); setIconImages.invoke(this, icons); } catch (Exception e) { return; } } /** * <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(); boolean fullscreen = fullscreenBox.isSelected(); boolean vsync = vsyncBox.isSelected(); 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 = -1; if (depthString.equals("???")) { depth = 0; } else { depth = Integer.parseInt(depthString.substring(0, depthString.indexOf(' '))); } String freqString = (String) displayFreqCombo.getSelectedItem(); int freq = -1; if (fullscreen) { if (freqString.equals("???")) { freq = 0; } else { freq = Integer.parseInt(freqString.substring(0, freqString.indexOf(' '))); } } String aaString = (String) antialiasCombo.getSelectedItem(); int multisample = -1; if (aaString.equals("Disabled")) { multisample = 0; } else { multisample = Integer.parseInt(aaString.substring(0, aaString.indexOf('x'))); } // 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 = "LWJGL-OpenGL2";//(String) rendererCombo.getSelectedItem(); boolean valid = false; // test valid display mode when going full screen if (!fullscreen) { valid = true; } else { GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); valid = device.isFullScreenSupported(); } if (valid) { //use the GameSettings class to save it. source.setWidth(width); source.setHeight(height); source.setBitsPerPixel(depth); source.setFrequency(freq); source.setFullscreen(fullscreen); source.setVSync(vsync); //source.setRenderer(renderer); source.setSamples(multisample); String appTitle = source.getTitle(); try { source.save(appTitle); } catch (BackingStoreException ex) { logger.log(Level.WARNING, "Failed to save setting changes", ex); } } else { showError( 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; } /** * <code>setUpChooser</code> retrieves all available display modes and * places them in a <code>JComboBox</code>. The resolution specified by * GameSettings 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 GameSettings is used as the default value. * * @return the list of renderers. */ private JComboBox setUpRendererChooser() { String modes[] = {"NULL", "JOGL-OpenGL1", "LWJGL-OpenGL2", "LWJGL-OpenGL3", "LWJGL-OpenGL3.1"}; JComboBox nameBox = new JComboBox(modes); nameBox.setSelectedItem(source.getRenderer()); return nameBox; } /** * <code>updateDisplayChoices</code> updates the available color depth and * display frequency options to match the currently selected resolution. */ private void updateDisplayChoices() { if (!fullscreenBox.isSelected()) { // don't run this function when changing windowed settings return; } String resolution = (String) displayResCombo.getSelectedItem(); String colorDepth = (String) colorDepthCombo.getSelectedItem(); if (colorDepth == null) { colorDepth = source.getBitsPerPixel() + " bpp"; } String displayFreq = (String) displayFreqCombo.getSelectedItem(); if (displayFreq == null) { displayFreq = source.getFrequency() + " Hz"; } // grab available depths String[] depths = getDepths(resolution, modes); colorDepthCombo.setModel(new DefaultComboBoxModel(depths)); colorDepthCombo.setSelectedItem(colorDepth); // grab available frequencies String[] freqs = getFrequencies(resolution, modes); displayFreqCombo.setModel(new DefaultComboBoxModel(freqs)); // Try to reset freq displayFreqCombo.setSelectedItem(displayFreq); } /** * <code>updateResolutionChoices</code> updates the available resolutions * list to match the currently selected window mode (fullscreen or * windowed). It then sets up a list of standard options (if windowed) or * calls <code>updateDisplayChoices</code> (if fullscreen). */ private void updateResolutionChoices() { if (!fullscreenBox.isSelected()) { displayResCombo.setModel(new DefaultComboBoxModel( windowedResolutions)); colorDepthCombo.setModel(new DefaultComboBoxModel(new String[]{ "24 bpp", "16 bpp"})); displayFreqCombo.setModel(new DefaultComboBoxModel( new String[]{"n/a"})); displayFreqCombo.setEnabled(false); } else { displayResCombo.setModel(new DefaultComboBoxModel( getResolutions(modes))); displayFreqCombo.setEnabled(true); updateDisplayChoices(); } } private void updateAntialiasChoices() { // maybe in the future will add support for determining this info // through pbuffer String[] choices = new String[]{"Disabled", "2x", "4x", "6x", "8x", "16x"}; antialiasCombo.setModel(new DefaultComboBoxModel(choices)); antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples()/2,5)]); } // // Utility methods // /** * 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; } private static void showError(java.awt.Component parent, String message) { JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE); } /** * Returns every unique resolution from an array of <code>DisplayMode</code>s. */ private static String[] getResolutions(DisplayMode[] modes) { ArrayList<String> resolutions = new ArrayList<String>(modes.length); for (int i = 0; i < modes.length; i++) { String res = modes[i].getWidth() + " x " + modes[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 static String[] getDepths(String resolution, DisplayMode[] modes) { ArrayList<String> depths = new ArrayList<String>(4); for (int i = 0; i < modes.length; i++) { // Filter out all bit depths lower than 16 - Java incorrectly // reports // them as valid depths though the monitor does not support them if (modes[i].getBitDepth() < 16 && modes[i].getBitDepth() > 0) { continue; } String res = modes[i].getWidth() + " x " + modes[i].getHeight(); String depth = modes[i].getBitDepth() + " bpp"; if (res.equals(resolution) && !depths.contains(depth)) { depths.add(depth); } } if (depths.size() == 1 && depths.contains("-1 bpp")) { // add some default depths, possible system is multi-depth supporting depths.clear(); depths.add("24 bpp"); } String[] res = new String[depths.size()]; depths.toArray(res); return res; } /** * Returns every possible refresh rate for the given resolution. */ private static String[] getFrequencies(String resolution, DisplayMode[] modes) { ArrayList<String> freqs = new ArrayList<String>(4); for (int i = 0; i < modes.length; i++) { String res = modes[i].getWidth() + " x " + modes[i].getHeight(); String freq; if (modes[i].getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) { freq = "???"; } else { freq = modes[i].getRefreshRate() + " Hz"; } if (res.equals(resolution) && !freqs.contains(freq)) { freqs.add(freq); } } String[] res = new String[freqs.size()]; freqs.toArray(res); return res; } /** * Utility class for sorting <code>DisplayMode</code>s. Sorts by * resolution, then bit depth, and then finally refresh rate. */ private class DisplayModeSorter implements Comparator<DisplayMode> { /** * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ public int compare(DisplayMode a, DisplayMode b) { // 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.getBitDepth() != b.getBitDepth()) { return (a.getBitDepth() > b.getBitDepth()) ? 1 : -1; } // Refresh rate if (a.getRefreshRate() != b.getRefreshRate()) { return (a.getRefreshRate() > b.getRefreshRate()) ? 1 : -1; } // All fields are equal return 0; } } }