Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (c) 2009-2012 jMonkeyEngine
      3  * All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  * * Redistributions of source code must retain the above copyright
     10  *   notice, this list of conditions and the following disclaimer.
     11  *
     12  * * Redistributions in binary form must reproduce the above copyright
     13  *   notice, this list of conditions and the following disclaimer in the
     14  *   documentation and/or other materials provided with the distribution.
     15  *
     16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
     17  *   may be used to endorse or promote products derived from this software
     18  *   without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 package com.jme3.app;
     33 
     34 import com.jme3.system.AppSettings;
     35 import java.awt.*;
     36 import java.awt.event.*;
     37 import java.awt.image.BufferedImage;
     38 import java.lang.reflect.Method;
     39 import java.net.MalformedURLException;
     40 import java.net.URL;
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.Comparator;
     44 import java.util.List;
     45 import java.util.logging.Level;
     46 import java.util.logging.Logger;
     47 import java.util.prefs.BackingStoreException;
     48 import javax.swing.*;
     49 
     50 /**
     51  * <code>PropertiesDialog</code> provides an interface to make use of the
     52  * <code>GameSettings</code> class. The <code>GameSettings</code> object
     53  * is still created by the client application, and passed during construction.
     54  *
     55  * @see com.jme.system.GameSettings
     56  * @author Mark Powell
     57  * @author Eric Woroshow
     58  * @author Joshua Slack - reworked for proper use of GL commands.
     59  * @version $Id: LWJGLPropertiesDialog.java 4131 2009-03-19 20:15:28Z blaine.dev $
     60  */
     61 public final class SettingsDialog extends JDialog {
     62 
     63     public static interface SelectionListener {
     64 
     65         public void onSelection(int selection);
     66     }
     67     private static final Logger logger = Logger.getLogger(SettingsDialog.class.getName());
     68     private static final long serialVersionUID = 1L;
     69     public static final int NO_SELECTION = 0,
     70             APPROVE_SELECTION = 1,
     71             CANCEL_SELECTION = 2;
     72     // connection to properties file.
     73     private final AppSettings source;
     74     // Title Image
     75     private URL imageFile = null;
     76     // Array of supported display modes
     77     private DisplayMode[] modes = null;
     78     // Array of windowed resolutions
     79     private String[] windowedResolutions = {"320 x 240", "640 x 480", "800 x 600",
     80         "1024 x 768", "1152 x 864", "1280 x 720"};
     81     // UI components
     82     private JCheckBox vsyncBox = null;
     83     private JCheckBox fullscreenBox = null;
     84     private JComboBox displayResCombo = null;
     85     private JComboBox colorDepthCombo = null;
     86     private JComboBox displayFreqCombo = null;
     87 //    private JComboBox rendererCombo = null;
     88     private JComboBox antialiasCombo = null;
     89     private JLabel icon = null;
     90     private int selection = 0;
     91     private SelectionListener selectionListener = null;
     92 
     93     /**
     94      * Constructor for the <code>PropertiesDialog</code>. Creates a
     95      * properties dialog initialized for the primary display.
     96      *
     97      * @param source
     98      *            the <code>AppSettings</code> object to use for working with
     99      *            the properties file.
    100      * @param imageFile
    101      *            the image file to use as the title of the dialog;
    102      *            <code>null</code> will result in to image being displayed
    103      * @throws NullPointerException
    104      *             if the source is <code>null</code>
    105      */
    106     public SettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
    107         this(source, getURL(imageFile), loadSettings);
    108     }
    109 
    110     /**
    111      * Constructor for the <code>PropertiesDialog</code>. Creates a
    112      * properties dialog initialized for the primary display.
    113      *
    114      * @param source
    115      *            the <code>GameSettings</code> object to use for working with
    116      *            the properties file.
    117      * @param imageFile
    118      *            the image file to use as the title of the dialog;
    119      *            <code>null</code> will result in to image being displayed
    120      * @param loadSettings
    121      * @throws JmeException
    122      *             if the source is <code>null</code>
    123      */
    124     public SettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
    125         if (source == null) {
    126             throw new NullPointerException("Settings source cannot be null");
    127         }
    128 
    129         this.source = source;
    130         this.imageFile = imageFile;
    131 
    132 //        setModalityType(Dialog.ModalityType.APPLICATION_MODAL);
    133         setModal(true);
    134 
    135         AppSettings registrySettings = new AppSettings(true);
    136 
    137         String appTitle;
    138         if(source.getTitle()!=null){
    139             appTitle = source.getTitle();
    140         }else{
    141            appTitle = registrySettings.getTitle();
    142         }
    143         try {
    144             registrySettings.load(appTitle);
    145         } catch (BackingStoreException ex) {
    146             logger.log(Level.WARNING,
    147                     "Failed to load settings", ex);
    148         }
    149 
    150         if (loadSettings) {
    151             source.copyFrom(registrySettings);
    152         } else if(!registrySettings.isEmpty()) {
    153             source.mergeFrom(registrySettings);
    154         }
    155 
    156         GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    157 
    158         modes = device.getDisplayModes();
    159         Arrays.sort(modes, new DisplayModeSorter());
    160 
    161         createUI();
    162     }
    163 
    164     public void setSelectionListener(SelectionListener sl) {
    165         this.selectionListener = sl;
    166     }
    167 
    168     public int getUserSelection() {
    169         return selection;
    170     }
    171 
    172     private void setUserSelection(int selection) {
    173         this.selection = selection;
    174         selectionListener.onSelection(selection);
    175     }
    176 
    177     /**
    178      * <code>setImage</code> sets the background image of the dialog.
    179      *
    180      * @param image
    181      *            <code>String</code> representing the image file.
    182      */
    183     public void setImage(String image) {
    184         try {
    185             URL file = new URL("file:" + image);
    186             setImage(file);
    187             // We can safely ignore the exception - it just means that the user
    188             // gave us a bogus file
    189         } catch (MalformedURLException e) {
    190         }
    191     }
    192 
    193     /**
    194      * <code>setImage</code> sets the background image of this dialog.
    195      *
    196      * @param image
    197      *            <code>URL</code> pointing to the image file.
    198      */
    199     public void setImage(URL image) {
    200         icon.setIcon(new ImageIcon(image));
    201         pack(); // Resize to accomodate the new image
    202         setLocationRelativeTo(null); // put in center
    203     }
    204 
    205     /**
    206      * <code>showDialog</code> sets this dialog as visble, and brings it to
    207      * the front.
    208      */
    209     public void showDialog() {
    210         setLocationRelativeTo(null);
    211         setVisible(true);
    212         toFront();
    213     }
    214 
    215     /**
    216      * <code>init</code> creates the components to use the dialog.
    217      */
    218     private void createUI() {
    219         try {
    220             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    221         } catch (Exception e) {
    222             logger.warning("Could not set native look and feel.");
    223         }
    224 
    225         addWindowListener(new WindowAdapter() {
    226 
    227             public void windowClosing(WindowEvent e) {
    228                 setUserSelection(CANCEL_SELECTION);
    229                 dispose();
    230             }
    231         });
    232 
    233         if (source.getIcons() != null) {
    234             safeSetIconImages( (List<BufferedImage>) Arrays.asList((BufferedImage[]) source.getIcons()) );
    235         }
    236 
    237         setTitle("Select Display Settings");
    238 
    239         // The panels...
    240         JPanel mainPanel = new JPanel();
    241         JPanel centerPanel = new JPanel();
    242         JPanel optionsPanel = new JPanel();
    243         JPanel buttonPanel = new JPanel();
    244         // The buttons...
    245         JButton ok = new JButton("Ok");
    246         JButton cancel = new JButton("Cancel");
    247 
    248         icon = new JLabel(imageFile != null ? new ImageIcon(imageFile) : null);
    249 
    250         mainPanel.setLayout(new BorderLayout());
    251 
    252         centerPanel.setLayout(new BorderLayout());
    253 
    254         KeyListener aListener = new KeyAdapter() {
    255 
    256             public void keyPressed(KeyEvent e) {
    257                 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
    258                     if (verifyAndSaveCurrentSelection()) {
    259                         setUserSelection(APPROVE_SELECTION);
    260                         dispose();
    261                     }
    262                 }
    263                 else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
    264                     setUserSelection(CANCEL_SELECTION);
    265                     dispose();
    266                 }
    267             }
    268         };
    269 
    270         displayResCombo = setUpResolutionChooser();
    271         displayResCombo.addKeyListener(aListener);
    272         colorDepthCombo = new JComboBox();
    273         colorDepthCombo.addKeyListener(aListener);
    274         displayFreqCombo = new JComboBox();
    275         displayFreqCombo.addKeyListener(aListener);
    276         antialiasCombo = new JComboBox();
    277         antialiasCombo.addKeyListener(aListener);
    278         fullscreenBox = new JCheckBox("Fullscreen?");
    279         fullscreenBox.setSelected(source.isFullscreen());
    280         fullscreenBox.addActionListener(new ActionListener() {
    281 
    282             public void actionPerformed(ActionEvent e) {
    283                 updateResolutionChoices();
    284             }
    285         });
    286         vsyncBox = new JCheckBox("VSync?");
    287         vsyncBox.setSelected(source.isVSync());
    288 //        rendererCombo = setUpRendererChooser();
    289 //        rendererCombo.addKeyListener(aListener);
    290 
    291 
    292 
    293         updateResolutionChoices();
    294         updateAntialiasChoices();
    295         displayResCombo.setSelectedItem(source.getWidth() + " x " + source.getHeight());
    296         colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
    297 
    298         optionsPanel.add(displayResCombo);
    299         optionsPanel.add(colorDepthCombo);
    300         optionsPanel.add(displayFreqCombo);
    301         optionsPanel.add(antialiasCombo);
    302         optionsPanel.add(fullscreenBox);
    303         optionsPanel.add(vsyncBox);
    304 //        optionsPanel.add(rendererCombo);
    305 
    306         // Set the button action listeners. Cancel disposes without saving, OK
    307         // saves.
    308         ok.addActionListener(new ActionListener() {
    309 
    310             public void actionPerformed(ActionEvent e) {
    311                 if (verifyAndSaveCurrentSelection()) {
    312                     setUserSelection(APPROVE_SELECTION);
    313                     dispose();
    314                 }
    315             }
    316         });
    317 
    318         cancel.addActionListener(new ActionListener() {
    319 
    320             public void actionPerformed(ActionEvent e) {
    321                 setUserSelection(CANCEL_SELECTION);
    322                 dispose();
    323             }
    324         });
    325 
    326         buttonPanel.add(ok);
    327         buttonPanel.add(cancel);
    328 
    329         if (icon != null) {
    330             centerPanel.add(icon, BorderLayout.NORTH);
    331         }
    332         centerPanel.add(optionsPanel, BorderLayout.SOUTH);
    333 
    334         mainPanel.add(centerPanel, BorderLayout.CENTER);
    335         mainPanel.add(buttonPanel, BorderLayout.SOUTH);
    336 
    337         this.getContentPane().add(mainPanel);
    338 
    339         pack();
    340     }
    341 
    342     /* Access JDialog.setIconImages by reflection in case we're running on JRE < 1.6 */
    343     private void safeSetIconImages(List<? extends Image> icons) {
    344         try {
    345             // Due to Java bug 6445278, we try to set icon on our shared owner frame first.
    346             // Otherwise, our alt-tab icon will be the Java default under Windows.
    347             Window owner = getOwner();
    348             if (owner != null) {
    349                 Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
    350                 setIconImages.invoke(owner, icons);
    351                 return;
    352             }
    353 
    354             Method setIconImages = getClass().getMethod("setIconImages", List.class);
    355             setIconImages.invoke(this, icons);
    356         } catch (Exception e) {
    357             return;
    358         }
    359     }
    360 
    361     /**
    362      * <code>verifyAndSaveCurrentSelection</code> first verifies that the
    363      * display mode is valid for this system, and then saves the current
    364      * selection as a properties.cfg file.
    365      *
    366      * @return if the selection is valid
    367      */
    368     private boolean verifyAndSaveCurrentSelection() {
    369         String display = (String) displayResCombo.getSelectedItem();
    370         boolean fullscreen = fullscreenBox.isSelected();
    371         boolean vsync = vsyncBox.isSelected();
    372 
    373         int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
    374         display = display.substring(display.indexOf(" x ") + 3);
    375         int height = Integer.parseInt(display);
    376 
    377         String depthString = (String) colorDepthCombo.getSelectedItem();
    378         int depth = -1;
    379         if (depthString.equals("???")) {
    380             depth = 0;
    381         } else {
    382             depth = Integer.parseInt(depthString.substring(0, depthString.indexOf(' ')));
    383         }
    384 
    385         String freqString = (String) displayFreqCombo.getSelectedItem();
    386         int freq = -1;
    387         if (fullscreen) {
    388             if (freqString.equals("???")) {
    389                 freq = 0;
    390             } else {
    391                 freq = Integer.parseInt(freqString.substring(0, freqString.indexOf(' ')));
    392             }
    393         }
    394 
    395         String aaString = (String) antialiasCombo.getSelectedItem();
    396         int multisample = -1;
    397         if (aaString.equals("Disabled")) {
    398             multisample = 0;
    399         } else {
    400             multisample = Integer.parseInt(aaString.substring(0, aaString.indexOf('x')));
    401         }
    402 
    403         // FIXME: Does not work in Linux
    404         /*
    405          * if (!fullscreen) { //query the current bit depth of the desktop int
    406          * curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
    407          * .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
    408          * curDepth) { showError(this,"Cannot choose a higher bit depth in
    409          * windowed " + "mode than your current desktop bit depth"); return
    410          * false; } }
    411          */
    412 
    413         String renderer = "LWJGL-OpenGL2";//(String) rendererCombo.getSelectedItem();
    414 
    415         boolean valid = false;
    416 
    417         // test valid display mode when going full screen
    418         if (!fullscreen) {
    419             valid = true;
    420         } else {
    421             GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    422             valid = device.isFullScreenSupported();
    423         }
    424 
    425         if (valid) {
    426             //use the GameSettings class to save it.
    427             source.setWidth(width);
    428             source.setHeight(height);
    429             source.setBitsPerPixel(depth);
    430             source.setFrequency(freq);
    431             source.setFullscreen(fullscreen);
    432             source.setVSync(vsync);
    433             //source.setRenderer(renderer);
    434             source.setSamples(multisample);
    435 
    436             String appTitle = source.getTitle();
    437 
    438             try {
    439                 source.save(appTitle);
    440             } catch (BackingStoreException ex) {
    441                 logger.log(Level.WARNING,
    442                         "Failed to save setting changes", ex);
    443             }
    444         } else {
    445             showError(
    446                     this,
    447                     "Your monitor claims to not support the display mode you've selected.\n"
    448                     + "The combination of bit depth and refresh rate is not supported.");
    449         }
    450 
    451         return valid;
    452     }
    453 
    454     /**
    455      * <code>setUpChooser</code> retrieves all available display modes and
    456      * places them in a <code>JComboBox</code>. The resolution specified by
    457      * GameSettings is used as the default value.
    458      *
    459      * @return the combo box of display modes.
    460      */
    461     private JComboBox setUpResolutionChooser() {
    462         String[] res = getResolutions(modes);
    463         JComboBox resolutionBox = new JComboBox(res);
    464 
    465         resolutionBox.setSelectedItem(source.getWidth() + " x "
    466                 + source.getHeight());
    467         resolutionBox.addActionListener(new ActionListener() {
    468 
    469             public void actionPerformed(ActionEvent e) {
    470                 updateDisplayChoices();
    471             }
    472         });
    473 
    474         return resolutionBox;
    475     }
    476 
    477     /**
    478      * <code>setUpRendererChooser</code> sets the list of available renderers.
    479      * Data is obtained from the <code>DisplaySystem</code> class. The
    480      * renderer specified by GameSettings is used as the default value.
    481      *
    482      * @return the list of renderers.
    483      */
    484     private JComboBox setUpRendererChooser() {
    485         String modes[] = {"NULL", "JOGL-OpenGL1", "LWJGL-OpenGL2", "LWJGL-OpenGL3", "LWJGL-OpenGL3.1"};
    486         JComboBox nameBox = new JComboBox(modes);
    487         nameBox.setSelectedItem(source.getRenderer());
    488         return nameBox;
    489     }
    490 
    491     /**
    492      * <code>updateDisplayChoices</code> updates the available color depth and
    493      * display frequency options to match the currently selected resolution.
    494      */
    495     private void updateDisplayChoices() {
    496         if (!fullscreenBox.isSelected()) {
    497             // don't run this function when changing windowed settings
    498             return;
    499         }
    500         String resolution = (String) displayResCombo.getSelectedItem();
    501         String colorDepth = (String) colorDepthCombo.getSelectedItem();
    502         if (colorDepth == null) {
    503             colorDepth = source.getBitsPerPixel() + " bpp";
    504         }
    505         String displayFreq = (String) displayFreqCombo.getSelectedItem();
    506         if (displayFreq == null) {
    507             displayFreq = source.getFrequency() + " Hz";
    508         }
    509 
    510         // grab available depths
    511         String[] depths = getDepths(resolution, modes);
    512         colorDepthCombo.setModel(new DefaultComboBoxModel(depths));
    513         colorDepthCombo.setSelectedItem(colorDepth);
    514         // grab available frequencies
    515         String[] freqs = getFrequencies(resolution, modes);
    516         displayFreqCombo.setModel(new DefaultComboBoxModel(freqs));
    517         // Try to reset freq
    518         displayFreqCombo.setSelectedItem(displayFreq);
    519     }
    520 
    521     /**
    522      * <code>updateResolutionChoices</code> updates the available resolutions
    523      * list to match the currently selected window mode (fullscreen or
    524      * windowed). It then sets up a list of standard options (if windowed) or
    525      * calls <code>updateDisplayChoices</code> (if fullscreen).
    526      */
    527     private void updateResolutionChoices() {
    528         if (!fullscreenBox.isSelected()) {
    529             displayResCombo.setModel(new DefaultComboBoxModel(
    530                     windowedResolutions));
    531             colorDepthCombo.setModel(new DefaultComboBoxModel(new String[]{
    532                         "24 bpp", "16 bpp"}));
    533             displayFreqCombo.setModel(new DefaultComboBoxModel(
    534                     new String[]{"n/a"}));
    535             displayFreqCombo.setEnabled(false);
    536         } else {
    537             displayResCombo.setModel(new DefaultComboBoxModel(
    538                     getResolutions(modes)));
    539             displayFreqCombo.setEnabled(true);
    540             updateDisplayChoices();
    541         }
    542     }
    543 
    544     private void updateAntialiasChoices() {
    545         // maybe in the future will add support for determining this info
    546         // through pbuffer
    547         String[] choices = new String[]{"Disabled", "2x", "4x", "6x", "8x", "16x"};
    548         antialiasCombo.setModel(new DefaultComboBoxModel(choices));
    549         antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples()/2,5)]);
    550     }
    551 
    552     //
    553     // Utility methods
    554     //
    555     /**
    556      * Utility method for converting a String denoting a file into a URL.
    557      *
    558      * @return a URL pointing to the file or null
    559      */
    560     private static URL getURL(String file) {
    561         URL url = null;
    562         try {
    563             url = new URL("file:" + file);
    564         } catch (MalformedURLException e) {
    565         }
    566         return url;
    567     }
    568 
    569     private static void showError(java.awt.Component parent, String message) {
    570         JOptionPane.showMessageDialog(parent, message, "Error",
    571                 JOptionPane.ERROR_MESSAGE);
    572     }
    573 
    574     /**
    575      * Returns every unique resolution from an array of <code>DisplayMode</code>s.
    576      */
    577     private static String[] getResolutions(DisplayMode[] modes) {
    578         ArrayList<String> resolutions = new ArrayList<String>(modes.length);
    579         for (int i = 0; i < modes.length; i++) {
    580             String res = modes[i].getWidth() + " x " + modes[i].getHeight();
    581             if (!resolutions.contains(res)) {
    582                 resolutions.add(res);
    583             }
    584         }
    585 
    586         String[] res = new String[resolutions.size()];
    587         resolutions.toArray(res);
    588         return res;
    589     }
    590 
    591     /**
    592      * Returns every possible bit depth for the given resolution.
    593      */
    594     private static String[] getDepths(String resolution, DisplayMode[] modes) {
    595         ArrayList<String> depths = new ArrayList<String>(4);
    596         for (int i = 0; i < modes.length; i++) {
    597             // Filter out all bit depths lower than 16 - Java incorrectly
    598             // reports
    599             // them as valid depths though the monitor does not support them
    600             if (modes[i].getBitDepth() < 16 && modes[i].getBitDepth() > 0) {
    601                 continue;
    602             }
    603 
    604             String res = modes[i].getWidth() + " x " + modes[i].getHeight();
    605             String depth = modes[i].getBitDepth() + " bpp";
    606             if (res.equals(resolution) && !depths.contains(depth)) {
    607                 depths.add(depth);
    608             }
    609         }
    610 
    611         if (depths.size() == 1 && depths.contains("-1 bpp")) {
    612             // add some default depths, possible system is multi-depth supporting
    613             depths.clear();
    614             depths.add("24 bpp");
    615         }
    616 
    617         String[] res = new String[depths.size()];
    618         depths.toArray(res);
    619         return res;
    620     }
    621 
    622     /**
    623      * Returns every possible refresh rate for the given resolution.
    624      */
    625     private static String[] getFrequencies(String resolution,
    626             DisplayMode[] modes) {
    627         ArrayList<String> freqs = new ArrayList<String>(4);
    628         for (int i = 0; i < modes.length; i++) {
    629             String res = modes[i].getWidth() + " x " + modes[i].getHeight();
    630             String freq;
    631             if (modes[i].getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) {
    632                 freq = "???";
    633             } else {
    634                 freq = modes[i].getRefreshRate() + " Hz";
    635             }
    636 
    637             if (res.equals(resolution) && !freqs.contains(freq)) {
    638                 freqs.add(freq);
    639             }
    640         }
    641 
    642         String[] res = new String[freqs.size()];
    643         freqs.toArray(res);
    644         return res;
    645     }
    646 
    647     /**
    648      * Utility class for sorting <code>DisplayMode</code>s. Sorts by
    649      * resolution, then bit depth, and then finally refresh rate.
    650      */
    651     private class DisplayModeSorter implements Comparator<DisplayMode> {
    652 
    653         /**
    654          * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
    655          */
    656         public int compare(DisplayMode a, DisplayMode b) {
    657             // Width
    658             if (a.getWidth() != b.getWidth()) {
    659                 return (a.getWidth() > b.getWidth()) ? 1 : -1;
    660             }
    661             // Height
    662             if (a.getHeight() != b.getHeight()) {
    663                 return (a.getHeight() > b.getHeight()) ? 1 : -1;
    664             }
    665             // Bit depth
    666             if (a.getBitDepth() != b.getBitDepth()) {
    667                 return (a.getBitDepth() > b.getBitDepth()) ? 1 : -1;
    668             }
    669             // Refresh rate
    670             if (a.getRefreshRate() != b.getRefreshRate()) {
    671                 return (a.getRefreshRate() > b.getRefreshRate()) ? 1 : -1;
    672             }
    673             // All fields are equal
    674             return 0;
    675         }
    676     }
    677 }
    678