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}