| /* |
| * 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; |
| } |
| } |
| } |