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 */
32package com.jme3.app;
33
34import com.jme3.system.AppSettings;
35import java.awt.*;
36import java.awt.event.*;
37import java.awt.image.BufferedImage;
38import java.lang.reflect.Method;
39import java.net.MalformedURLException;
40import java.net.URL;
41import java.util.ArrayList;
42import java.util.Arrays;
43import java.util.Comparator;
44import java.util.List;
45import java.util.logging.Level;
46import java.util.logging.Logger;
47import java.util.prefs.BackingStoreException;
48import 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 */
61public 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