001/** 002 * Copyright (c) 2008-2014 Ardor Labs, Inc. 003 * 004 * This file is part of Ardor3D. 005 * 006 * Ardor3D is free software: you can redistribute it and/or modify it 007 * under the terms of its license which may be found in the accompanying 008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>. 009 */ 010 011package com.ardor3d.example; 012 013import java.awt.BorderLayout; 014import java.awt.DisplayMode; 015import java.awt.GraphicsEnvironment; 016import java.awt.Toolkit; 017import java.awt.event.ActionEvent; 018import java.awt.event.ActionListener; 019import java.awt.event.KeyAdapter; 020import java.awt.event.KeyEvent; 021import java.awt.event.KeyListener; 022import java.awt.event.WindowAdapter; 023import java.awt.event.WindowEvent; 024import java.io.IOException; 025import java.net.MalformedURLException; 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.net.URL; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Comparator; 032import java.util.List; 033import java.util.Set; 034import java.util.Stack; 035import java.util.TreeSet; 036import java.util.logging.Level; 037import java.util.logging.Logger; 038 039import javax.swing.DefaultComboBoxModel; 040import javax.swing.ImageIcon; 041import javax.swing.JButton; 042import javax.swing.JCheckBox; 043import javax.swing.JComboBox; 044import javax.swing.JDialog; 045import javax.swing.JLabel; 046import javax.swing.JOptionPane; 047import javax.swing.JPanel; 048import javax.swing.UIManager; 049 050import com.ardor3d.util.Ardor3dException; 051 052public final class PropertiesDialog extends JDialog { 053 private static final Logger logger = Logger.getLogger(PropertiesDialog.class.getName()); 054 055 private static final long serialVersionUID = 1L; 056 057 // connection to properties file. 058 private final PropertiesGameSettings source; 059 060 // Title Image 061 private URL imageFile = null; 062 063 // Array of supported display modes 064 private DisplayMode[] modes = null; 065 066 // Array of windowed resolutions 067 private final String[] windowedResolutions = { "640 x 480", "800 x 600", "1024 x 768", "1152 x 864", 068 "1920 x 1080" }; 069 070 // Array of possible samples 071 private final String[] samples = { "0 samples", "1 samples", "2 samples", "4 samples", "8 samples" }; 072 073 // UI components 074 private JCheckBox fullscreenBox = null; 075 076 private JComboBox<String> displayResCombo = null; 077 078 private JComboBox<String> samplesCombo = null; 079 080 private JComboBox<String> colorDepthCombo = null; 081 082 private JComboBox<String> displayFreqCombo = null; 083 084 private JComboBox<String> rendererCombo = null; 085 086 private JLabel icon = null; 087 088 private boolean cancelled = false; 089 090 private final Stack<Runnable> mainThreadTasks; 091 092 /** 093 * Constructor for the <code>PropertiesDialog</code>. Creates a properties dialog initialized for the primary 094 * display. 095 * 096 * @param source 097 * the <code>GameSettings</code> object to use for working with the properties file. 098 * @param imageFile 099 * the image file to use as the title of the dialog; <code>null</code> will result in to image being 100 * displayed 101 * @throws Ardor3dException 102 * if the source is <code>null</code> 103 */ 104 public PropertiesDialog(final PropertiesGameSettings source, final String imageFile) { 105 this(source, imageFile, null); 106 } 107 108 /** 109 * Constructor for the <code>PropertiesDialog</code>. Creates a properties dialog initialized for the primary 110 * display. 111 * 112 * @param source 113 * the <code>GameSettings</code> object to use for working with the properties file. 114 * @param imageFile 115 * the image file to use as the title of the dialog; <code>null</code> will result in to image being 116 * displayed 117 * @throws Ardor3dException 118 * if the source is <code>null</code> 119 */ 120 public PropertiesDialog(final PropertiesGameSettings source, final URL imageFile) { 121 this(source, imageFile, null); 122 } 123 124 /** 125 * Constructor for the <code>PropertiesDialog</code>. Creates a properties dialog initialized for the primary 126 * display. 127 * 128 * @param source 129 * the <code>GameSettings</code> object to use for working with the properties file. 130 * @param imageFile 131 * the image file to use as the title of the dialog; <code>null</code> will result in to image being 132 * displayed 133 * @param mainThreadTasks 134 * the stack of tasks to run on the main thread 135 * @throws Ardor3dException 136 * if the source is <code>null</code> 137 */ 138 public PropertiesDialog(final PropertiesGameSettings source, final String imageFile, 139 final Stack<Runnable> mainThreadTasks) { 140 this(source, getURL(imageFile), mainThreadTasks); 141 } 142 143 /** 144 * Constructor for the <code>PropertiesDialog</code>. Creates a properties dialog initialized for the primary 145 * display. 146 * 147 * @param source 148 * the <code>GameSettings</code> object to use for working with the properties file. 149 * @param imageFile 150 * the image file to use as the title of the dialog; <code>null</code> will result in to image being 151 * displayed 152 * @param mainThreadTasks 153 * the stack of tasks to run on the main thread 154 * @throws Ardor3dException 155 * if the source is <code>null</code> 156 */ 157 public PropertiesDialog(final PropertiesGameSettings source, final URL imageFile, 158 final Stack<Runnable> mainThreadTasks) { 159 if (null == source) { 160 throw new Ardor3dException("PropertyIO source cannot be null"); 161 } 162 163 this.source = source; 164 this.imageFile = imageFile; 165 this.mainThreadTasks = mainThreadTasks; 166 167 final ModesRetriever retrieval = new ModesRetriever(); 168 if (mainThreadTasks != null) { 169 mainThreadTasks.add(retrieval); 170 } else { 171 retrieval.run(); 172 } 173 modes = retrieval.getModes(); 174 Arrays.sort(modes, new DisplayModeSorter()); 175 176 createUI(); 177 } 178 179 /** 180 * <code>setImage</code> sets the background image of the dialog. 181 * 182 * @param image 183 * <code>String</code> representing the image file. 184 */ 185 public void setImage(final String image) { 186 try { 187 final URL file = new URL("file:" + image); 188 //FIXME use this to get rid of a deprecated call: 189 //final URL file = new URI("file:" + image).toURL(); 190 setImage(file); 191 // We can safely ignore the exception - it just means that the user 192 // gave us a bogus file 193 } catch (final MalformedURLException/*|URISyntaxException*/ e) { 194 } 195 } 196 197 /** 198 * <code>setImage</code> sets the background image of this dialog. 199 * 200 * @param image 201 * <code>URL</code> pointing to the image file. 202 */ 203 public void setImage(final URL image) { 204 icon.setIcon(new ImageIcon(image)); 205 pack(); // Resize to accomodate the new image 206 center(); 207 } 208 209 /** 210 * <code>showDialog</code> sets this dialog as visble, and brings it to the front. 211 */ 212 private void showDialog() { 213 setVisible(true); 214 toFront(); 215 } 216 217 /** 218 * <code>center</code> places this <code>PropertiesDialog</code> in the center of the screen. 219 */ 220 private void center() { 221 int x, y; 222 x = (Toolkit.getDefaultToolkit().getScreenSize().width - getWidth()) / 2; 223 y = (Toolkit.getDefaultToolkit().getScreenSize().height - getHeight()) / 2; 224 this.setLocation(x, y); 225 } 226 227 /** 228 * <code>init</code> creates the components to use the dialog. 229 */ 230 private void createUI() { 231 try { 232 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 233 } catch (final Exception e) { 234 logger.warning("Could not set native look and feel."); 235 } 236 237 addWindowListener(new WindowAdapter() { 238 @Override 239 public void windowClosing(final WindowEvent e) { 240 cancelled = true; 241 dispose(); 242 } 243 }); 244 245 setTitle("Select Display Settings"); 246 247 // The panels... 248 final JPanel mainPanel = new JPanel(); 249 final JPanel centerPanel = new JPanel(); 250 final JPanel optionsPanel = new JPanel(); 251 final JPanel buttonPanel = new JPanel(); 252 // The buttons... 253 final JButton ok = new JButton("Ok"); 254 final JButton cancel = new JButton("Cancel"); 255 256 icon = new JLabel(imageFile != null ? new ImageIcon(imageFile) : null); 257 258 mainPanel.setLayout(new BorderLayout()); 259 260 centerPanel.setLayout(new BorderLayout()); 261 262 final KeyListener aListener = new KeyAdapter() { 263 @Override 264 public void keyPressed(final KeyEvent e) { 265 if (e.getKeyCode() == KeyEvent.VK_ENTER) { 266 if (verifyAndSaveCurrentSelection()) { 267 dispose(); 268 } 269 } 270 } 271 }; 272 273 displayResCombo = setUpResolutionChooser(); 274 displayResCombo.addKeyListener(aListener); 275 samplesCombo = setUpSamplesChooser(); 276 samplesCombo.addKeyListener(aListener); 277 colorDepthCombo = new JComboBox<>(); 278 colorDepthCombo.addKeyListener(aListener); 279 displayFreqCombo = new JComboBox<>(); 280 281 displayFreqCombo.addKeyListener(aListener); 282 fullscreenBox = new JCheckBox("Fullscreen?"); 283 fullscreenBox.setSelected(source.isFullscreen()); 284 fullscreenBox.addActionListener(new ActionListener() { 285 @Override 286 public void actionPerformed(final ActionEvent e) { 287 updateResolutionChoices(); 288 } 289 }); 290 rendererCombo = setUpRendererChooser(); 291 rendererCombo.addKeyListener(aListener); 292 293 updateResolutionChoices(); 294 displayResCombo.setSelectedItem(source.getWidth() + " x " + source.getHeight()); 295 296 samplesCombo.setSelectedItem(source.getSamples() + " samples"); 297 298 optionsPanel.add(displayResCombo); 299 optionsPanel.add(colorDepthCombo); 300 optionsPanel.add(displayFreqCombo); 301 optionsPanel.add(samplesCombo); 302 optionsPanel.add(fullscreenBox); 303 optionsPanel.add(rendererCombo); 304 305 // Set the button action listeners. Cancel disposes without saving, OK 306 // saves. 307 ok.addActionListener(new ActionListener() { 308 @Override 309 public void actionPerformed(final ActionEvent e) { 310 if (verifyAndSaveCurrentSelection()) { 311 dispose(); 312 } 313 } 314 }); 315 316 cancel.addActionListener(new ActionListener() { 317 @Override 318 public void actionPerformed(final ActionEvent e) { 319 cancelled = true; 320 dispose(); 321 } 322 }); 323 324 buttonPanel.add(ok); 325 buttonPanel.add(cancel); 326 327 if (icon != null) { 328 centerPanel.add(icon, BorderLayout.NORTH); 329 } 330 centerPanel.add(optionsPanel, BorderLayout.SOUTH); 331 332 mainPanel.add(centerPanel, BorderLayout.CENTER); 333 mainPanel.add(buttonPanel, BorderLayout.SOUTH); 334 335 getContentPane().add(mainPanel); 336 337 pack(); 338 center(); 339 showDialog(); 340 } 341 342 /** 343 * <code>verifyAndSaveCurrentSelection</code> first verifies that the display mode is valid for this system, and 344 * then saves the current selection as a properties.cfg file. 345 * 346 * @return if the selection is valid 347 */ 348 private boolean verifyAndSaveCurrentSelection() { 349 String display = (String) displayResCombo.getSelectedItem(); 350 final boolean fullscreen = fullscreenBox.isSelected(); 351 352 final int width = Integer.parseInt(display.substring(0, display.indexOf(" x "))); 353 display = display.substring(display.indexOf(" x ") + 3); 354 final int height = Integer.parseInt(display); 355 356 String depthString = (String) colorDepthCombo.getSelectedItem(); 357 int depth = 0; 358 if (depthString != null) { 359 depthString = depthString.substring(0, depthString.indexOf(' ')); 360 if (depthString.equals("?")) { 361 depth = DisplayMode.BIT_DEPTH_MULTI; 362 } else { 363 depth = Integer.parseInt(depthString); 364 } 365 } 366 367 final String freqString = (String) displayFreqCombo.getSelectedItem(); 368 int freq = -1; 369 if (fullscreen) { 370 freq = Integer.parseInt(freqString.substring(0, freqString.indexOf(' '))); 371 } 372 373 final String samplesString = (String) samplesCombo.getSelectedItem(); 374 int samples = -1; 375 samples = Integer.parseInt(samplesString.substring(0, samplesString.indexOf(' '))); 376 377 final String renderer = (String) rendererCombo.getSelectedItem(); 378 379 boolean valid = false; 380 381 // test valid display mode when going full screen 382 if (!fullscreen) { 383 valid = true; 384 } else { 385 final ModeValidator validator = new ModeValidator(renderer, width, height, depth, freq, samples); 386 if (mainThreadTasks != null) { 387 mainThreadTasks.add(validator); 388 } else { 389 validator.run(); 390 } 391 392 valid = validator.isValid(); 393 } 394 395 if (valid) { 396 // use the GameSettings class to save it. 397 source.setWidth(width); 398 source.setHeight(height); 399 source.setDepth(depth); 400 source.setFrequency(freq); 401 source.setFullscreen(fullscreen); 402 source.setRenderer(renderer); 403 source.setSamples(samples); 404 try { 405 source.save(); 406 } catch (final IOException ioe) { 407 logger.log(Level.WARNING, "Failed to save setting changes", ioe); 408 } 409 } else { 410 showError(this, "Your monitor claims to not support the display mode you've selected.\n" 411 + "The combination of bit depth and refresh rate is not supported."); 412 } 413 414 return valid; 415 } 416 417 /** 418 * <code>setUpChooser</code> retrieves all available display modes and places them in a <code>JComboBox</code>. The 419 * resolution specified by GameSettings is used as the default value. 420 * 421 * @return the combo box of display modes. 422 */ 423 private JComboBox<String> setUpResolutionChooser() { 424 final String[] res = getResolutions(modes); 425 final JComboBox<String> resolutionBox = new JComboBox<>(res); 426 427 resolutionBox.setSelectedItem(source.getWidth() + " x " + source.getHeight()); 428 resolutionBox.addActionListener(new ActionListener() { 429 @Override 430 public void actionPerformed(final ActionEvent e) { 431 updateDisplayChoices(); 432 } 433 }); 434 435 return resolutionBox; 436 } 437 438 /** 439 * <code>setUpRendererChooser</code> sets the list of available renderers. The renderer specified by GameSettings is 440 * used as the default value. 441 * 442 * @return the list of renderers. 443 */ 444 private JComboBox<String> setUpRendererChooser() { 445 final JComboBox<String> nameBox = new JComboBox<>(new String[] { "JOGL 2" }); 446 // final String old = source.getRenderer(); 447 /* 448 * if (old != null) { if (old.startsWith("JOGL")) { 449 */ 450 nameBox.setSelectedIndex(0); 451 /* 452 * } } 453 */ 454 return nameBox; 455 } 456 457 private JComboBox<String> setUpSamplesChooser() { 458 final JComboBox<String> nameBox = new JComboBox<>(samples); 459 nameBox.setSelectedItem(source.getRenderer()); 460 return nameBox; 461 } 462 463 /** 464 * <code>updateDisplayChoices</code> updates the available color depth and display frequency options to match the 465 * currently selected resolution. 466 */ 467 private void updateDisplayChoices() { 468 if (!fullscreenBox.isSelected()) { 469 // don't run this function when changing windowed settings 470 return; 471 } 472 final String resolution = (String) displayResCombo.getSelectedItem(); 473 String colorDepth = (String) colorDepthCombo.getSelectedItem(); 474 if (colorDepth == null) { 475 colorDepth = (source.getDepth() != DisplayMode.BIT_DEPTH_MULTI) ? source.getDepth() + " bpp" : "? bpp"; 476 } 477 String displayFreq = (String) displayFreqCombo.getSelectedItem(); 478 if (displayFreq == null) { 479 displayFreq = source.getFrequency() + " Hz"; 480 } 481 482 // grab available depths 483 final String[] depths = getDepths(resolution, modes); 484 colorDepthCombo.setModel(new DefaultComboBoxModel<>(depths)); 485 colorDepthCombo.setSelectedItem(colorDepth); 486 // grab available frequencies 487 final String[] freqs = getFrequencies(resolution, modes); 488 displayFreqCombo.setModel(new DefaultComboBoxModel<>(freqs)); 489 // Try to reset freq 490 displayFreqCombo.setSelectedItem(displayFreq); 491 } 492 493 /** 494 * <code>updateResolutionChoices</code> updates the available resolutions list to match the currently selected 495 * window mode (fullscreen or windowed). It then sets up a list of standard options (if windowed) or calls 496 * <code>updateDisplayChoices</code> (if fullscreen). 497 */ 498 private void updateResolutionChoices() { 499 if (!fullscreenBox.isSelected()) { 500 displayResCombo.setModel(new DefaultComboBoxModel<>(windowedResolutions)); 501 colorDepthCombo.setModel(new DefaultComboBoxModel<>(new String[] { "24 bpp", "16 bpp" })); 502 displayFreqCombo.setModel(new DefaultComboBoxModel<>(new String[] { "n/a" })); 503 displayFreqCombo.setEnabled(false); 504 } else { 505 displayResCombo.setModel(new DefaultComboBoxModel<>(getResolutions(modes))); 506 displayFreqCombo.setEnabled(true); 507 updateDisplayChoices(); 508 } 509 pack(); 510 } 511 512 // 513 // Utility methods 514 // 515 516 /** 517 * Utility method for converting a String denoting a file into a URL. 518 * 519 * @param file 520 * the string denoting a file 521 * 522 * @return a URL pointing to the file or null 523 */ 524 private static URL getURL(final String file) { 525 URL url = null; 526 try { 527 url = new URL("file:" + file); 528 //FIXME use this to get rid of a deprecated call: 529 //url = new URI("file:" + file).toURL(); 530 } catch (final MalformedURLException/*|URISyntaxException*/ e) { 531 } 532 return url; 533 } 534 535 private static void showError(final java.awt.Component parent, final String message) { 536 JOptionPane.showMessageDialog(parent, message, "Error", JOptionPane.ERROR_MESSAGE); 537 } 538 539 /** 540 * Returns every unique resolution from an array of <code>DisplayMode</code>s. 541 * 542 * @param modes 543 * the display modes 544 * 545 * @return the resolutions 546 */ 547 private static String[] getResolutions(final DisplayMode[] modes) { 548 final List<String> resolutions = new ArrayList<>(modes.length); 549 for (int i = 0; i < modes.length; i++) { 550 final String res = modes[i].getWidth() + " x " + modes[i].getHeight(); 551 if (!resolutions.contains(res)) { 552 resolutions.add(res); 553 } 554 } 555 556 final String[] res = new String[resolutions.size()]; 557 resolutions.toArray(res); 558 return res; 559 } 560 561 /** 562 * Returns every possible bit depth for the given resolution. 563 * 564 * @param resolution 565 * the resolution 566 * @param modes 567 * the display modes 568 * 569 * @return the bit depths 570 */ 571 private static String[] getDepths(final String resolution, final DisplayMode[] modes) { 572 final Set<String> depths = new TreeSet<>(new Comparator<String>() { 573 @Override 574 public int compare(final String o1, final String o2) { 575 // reverse order 576 return -o1.compareTo(o2); 577 } 578 }); 579 for (int i = 0; i < modes.length; i++) { 580 // Filter out modes with bit depths that we don't care about. 581 if (modes[i].getBitDepth() < 16 && modes[i].getBitDepth() != DisplayMode.BIT_DEPTH_MULTI) { 582 continue; 583 } 584 585 final String res = modes[i].getWidth() + " x " + modes[i].getHeight(); 586 final String depth = (modes[i].getBitDepth() != DisplayMode.BIT_DEPTH_MULTI) 587 ? modes[i].getBitDepth() + " bpp" 588 : "? bpp"; 589 if (res.equals(resolution) && !depths.contains(depth)) { 590 depths.add(depth); 591 } 592 } 593 594 final String[] res = new String[depths.size()]; 595 depths.toArray(res); 596 return res; 597 } 598 599 /** 600 * Returns every possible refresh rate for the given resolution. 601 * 602 * @param resolution 603 * the resolution 604 * @param modes 605 * the display modes 606 * 607 * @return the refresh rates 608 */ 609 private static String[] getFrequencies(final String resolution, final DisplayMode[] modes) { 610 final List<String> freqs = new ArrayList<>(4); 611 for (int i = 0; i < modes.length; i++) { 612 final String res = modes[i].getWidth() + " x " + modes[i].getHeight(); 613 final String freq = modes[i].getRefreshRate() + " Hz"; 614 if (res.equals(resolution) && !freqs.contains(freq)) { 615 freqs.add(freq); 616 } 617 } 618 619 final String[] res = new String[freqs.size()]; 620 freqs.toArray(res); 621 return res; 622 } 623 624 /** 625 * Utility class for sorting <code>DisplayMode</code>s. Sorts by resolution, then bit depth, and then finally 626 * refresh rate. 627 */ 628 private static class DisplayModeSorter implements Comparator<DisplayMode> { 629 /** 630 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) 631 */ 632 @Override 633 public int compare(final DisplayMode a, final DisplayMode b) { 634 // Width 635 if (a.getWidth() != b.getWidth()) { 636 return (a.getWidth() > b.getWidth()) ? 1 : -1; 637 } 638 // Height 639 if (a.getHeight() != b.getHeight()) { 640 return (a.getHeight() > b.getHeight()) ? 1 : -1; 641 } 642 // Bit depth 643 if (a.getBitDepth() != b.getBitDepth()) { 644 return (a.getBitDepth() > b.getBitDepth()) ? 1 : -1; 645 } 646 // Refresh rate 647 if (a.getRefreshRate() != b.getRefreshRate()) { 648 return (a.getRefreshRate() > b.getRefreshRate()) ? 1 : -1; 649 } 650 // All fields are equal 651 return 0; 652 } 653 } 654 655 /** 656 * @return Returns true if this dialog was cancelled 657 */ 658 public boolean isCancelled() { 659 return cancelled; 660 } 661 662 private static class ModeValidator implements Runnable { 663 664 boolean ready = false, valid = true; 665 666 String renderer; 667 668 ModeValidator(final String renderer, final int width, final int height, final int depth, final int freq, 669 final int samples) { 670 this.renderer = renderer; 671 } 672 673 @Override 674 public void run() { 675 if (renderer.startsWith("JOGL")) { 676 // TODO: can we implement this? 677 } 678 ready = true; 679 } 680 681 public boolean isValid() { 682 while (!ready) { 683 try { 684 Thread.sleep(10); 685 } catch (final Exception e) { 686 } 687 } 688 return valid; 689 } 690 } 691 692 private static class ModesRetriever implements Runnable { 693 694 boolean ready = false; 695 DisplayMode[] modes = null; 696 697 ModesRetriever() { 698 } 699 700 @Override 701 public void run() { 702 try { 703 modes = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDisplayModes(); 704 } catch (final Exception e) { 705 logger.logp(Level.SEVERE, this.getClass().toString(), "PropertiesDialog(GameSettings, URL)", 706 "Exception", e); 707 return; 708 } 709 ready = true; 710 } 711 712 public DisplayMode[] getModes() { 713 while (!ready) { 714 try { 715 Thread.sleep(10); 716 } catch (final Exception e) { 717 } 718 } 719 return modes; 720 } 721 } 722}