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 onSelection(int selection)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 */ SettingsDialog(AppSettings source, String imageFile, boolean loadSettings)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 */ SettingsDialog(AppSettings source, URL imageFile, boolean loadSettings)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 setSelectionListener(SelectionListener sl)164 public void setSelectionListener(SelectionListener sl) { 165 this.selectionListener = sl; 166 } 167 getUserSelection()168 public int getUserSelection() { 169 return selection; 170 } 171 setUserSelection(int selection)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 */ setImage(String image)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 */ setImage(URL image)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 */ showDialog()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 */ createUI()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 */ safeSetIconImages(List<? extends Image> icons)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 */ verifyAndSaveCurrentSelection()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 */ setUpResolutionChooser()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 */ setUpRendererChooser()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 */ updateDisplayChoices()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 */ updateResolutionChoices()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 updateAntialiasChoices()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 */ getURL(String file)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 showError(java.awt.Component parent, String message)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 */ getResolutions(DisplayMode[] modes)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 */ getDepths(String resolution, DisplayMode[] modes)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 */ getFrequencies(String resolution, DisplayMode[] modes)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 */ compare(DisplayMode a, DisplayMode b)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