Ali-RS a47b4a46ba Added a (hacky) fix for a Java Swing/AWT + GLFW interaction issue… (#968)
* Added a (hacky) fix for a Java Swing/AWT + GLFW interaction issue… 

… on Linux.

* Added a proper comment

* Added an extra call to System.gc() as recommendation from lwjgl’s @Spasi

"Run System.gc(), at least two times. Not sure how AWT handles native
resources, maybe there’s a finalizable/PhantomRef-ed resource that is not
released immediately, but a subsequent GC triggers a (too late) free. "
2018-12-14 09:19:25 +01:00

905 lines
32 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2009-2012 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.app;
import com.jme3.system.AppSettings;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import javax.swing.*;
/**
* <code>PropertiesDialog</code> provides an interface to make use of the
* <code>GameSettings</code> class. The <code>GameSettings</code> object
* is still created by the client application, and passed during construction.
*
* @see AppSettings
* @author Mark Powell
* @author Eric Woroshow
* @author Joshua Slack - reworked for proper use of GL commands.
*/
public final class SettingsDialog extends JFrame {
public static interface SelectionListener {
public void onSelection(int selection);
}
private static final Logger logger = Logger.getLogger(SettingsDialog.class.getName());
private static final long serialVersionUID = 1L;
public static final int NO_SELECTION = 0,
APPROVE_SELECTION = 1,
CANCEL_SELECTION = 2;
// Resource bundle for i18n.
ResourceBundle resourceBundle = ResourceBundle.getBundle("com.jme3.app/SettingsDialog");
// connection to properties file.
private final AppSettings source;
// Title Image
private URL imageFile = null;
// Array of supported display modes
private DisplayMode[] modes = null;
private static final DisplayMode[] windowDefaults = new DisplayMode[] {
new DisplayMode(1024, 768, 24, 60),
new DisplayMode(1280, 720, 24, 60),
new DisplayMode(1280, 1024, 24, 60),
new DisplayMode(1440, 900, 24, 60),
new DisplayMode(1680, 1050, 24, 60),
};
private DisplayMode[] windowModes = null;
// UI components
private JCheckBox vsyncBox = null;
private JCheckBox gammaBox = null;
private JCheckBox fullscreenBox = null;
private JComboBox displayResCombo = null;
private JComboBox colorDepthCombo = null;
private JComboBox displayFreqCombo = null;
private JComboBox antialiasCombo = null;
private JLabel icon = null;
private int selection = 0;
private SelectionListener selectionListener = null;
private int minWidth = 0;
private int minHeight = 0;
/**
* Constructor for the <code>PropertiesDialog</code>. Creates a
* properties dialog initialized for the primary display.
*
* @param source
* the <code>AppSettings</code> object to use for working with
* the properties file.
* @param imageFile
* the image file to use as the title of the dialog;
* <code>null</code> will result in to image being displayed
* @throws NullPointerException
* if the source is <code>null</code>
*/
public SettingsDialog(AppSettings source, String imageFile, boolean loadSettings) {
this(source, getURL(imageFile), loadSettings);
}
/**
* Constructor for the <code>PropertiesDialog</code>. Creates a
* properties dialog initialized for the primary display.
*
* @param source
* the <code>GameSettings</code> object to use for working with
* the properties file.
* @param imageFile
* the image file to use as the title of the dialog;
* <code>null</code> will result in to image being displayed
* @param loadSettings
* @throws JmeException
* if the source is <code>null</code>
*/
public SettingsDialog(AppSettings source, URL imageFile, boolean loadSettings) {
if (source == null) {
throw new NullPointerException("Settings source cannot be null");
}
this.source = source;
this.imageFile = imageFile;
//setModal(true);
setAlwaysOnTop(true);
setResizable(false);
AppSettings registrySettings = new AppSettings(true);
String appTitle;
if(source.getTitle()!=null){
appTitle = source.getTitle();
}else{
appTitle = registrySettings.getTitle();
}
minWidth = source.getMinWidth();
minHeight = source.getMinHeight();
try {
registrySettings.load(appTitle);
} catch (BackingStoreException ex) {
logger.log(Level.WARNING,
"Failed to load settings", ex);
}
if (loadSettings) {
source.copyFrom(registrySettings);
} else if(!registrySettings.isEmpty()) {
source.mergeFrom(registrySettings);
}
GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
modes = device.getDisplayModes();
Arrays.sort(modes, new DisplayModeSorter());
DisplayMode[] merged = new DisplayMode[modes.length + windowDefaults.length];
int wdIndex = 0;
int dmIndex = 0;
int mergedIndex;
for (mergedIndex = 0;
mergedIndex<merged.length
&& (wdIndex < windowDefaults.length
|| dmIndex < modes.length);
mergedIndex++) {
if (dmIndex >= modes.length) {
merged[mergedIndex] = windowDefaults[wdIndex++];
} else if (wdIndex >= windowDefaults.length) {
merged[mergedIndex] = modes[dmIndex++];
} else if (modes[dmIndex].getWidth() < windowDefaults[wdIndex].getWidth()) {
merged[mergedIndex] = modes[dmIndex++];
} else if (modes[dmIndex].getWidth() == windowDefaults[wdIndex].getWidth()) {
if (modes[dmIndex].getHeight() < windowDefaults[wdIndex].getHeight()) {
merged[mergedIndex] = modes[dmIndex++];
} else if (modes[dmIndex].getHeight() == windowDefaults[wdIndex].getHeight()) {
merged[mergedIndex] = modes[dmIndex++];
wdIndex++;
} else {
merged[mergedIndex] = windowDefaults[wdIndex++];
}
} else {
merged[mergedIndex] = windowDefaults[wdIndex++];
}
}
if (merged.length == mergedIndex) {
windowModes = merged;
} else {
windowModes = Arrays.copyOfRange(merged, 0, mergedIndex);
}
createUI();
}
public void setSelectionListener(SelectionListener sl) {
this.selectionListener = sl;
}
public int getUserSelection() {
return selection;
}
private void setUserSelection(int selection) {
this.selection = selection;
selectionListener.onSelection(selection);
}
public int getMinWidth() {
return minWidth;
}
public void setMinWidth(int minWidth) {
this.minWidth = minWidth;
}
public int getMinHeight() {
return minHeight;
}
public void setMinHeight(int minHeight) {
this.minHeight = minHeight;
}
/**
* <code>setImage</code> sets the background image of the dialog.
*
* @param image
* <code>String</code> representing the image file.
*/
public void setImage(String image) {
try {
URL file = new URL("file:" + image);
setImage(file);
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Couldnt read from file '" + image + "'", e);
}
}
/**
* <code>setImage</code> sets the background image of this dialog.
*
* @param image
* <code>URL</code> pointing to the image file.
*/
public void setImage(URL image) {
icon.setIcon(new ImageIcon(image));
pack(); // Resize to accomodate the new image
setLocationRelativeTo(null); // put in center
}
/**
* <code>showDialog</code> sets this dialog as visble, and brings it to
* the front.
*/
public void showDialog() {
setLocationRelativeTo(null);
setVisible(true);
toFront();
}
/**
* <code>init</code> creates the components to use the dialog.
*/
private void createUI() {
GridBagConstraints gbc;
JPanel mainPanel = new JPanel(new GridBagLayout());
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
logger.warning("Could not set native look and feel.");
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
setUserSelection(CANCEL_SELECTION);
dispose();
}
});
if (source.getIcons() != null) {
safeSetIconImages( (List<BufferedImage>) Arrays.asList((BufferedImage[]) source.getIcons()) );
}
setTitle(MessageFormat.format(resourceBundle.getString("frame.title"), source.getTitle()));
// The buttons...
JButton ok = new JButton(resourceBundle.getString("button.ok"));
JButton cancel = new JButton(resourceBundle.getString("button.cancel"));
icon = new JLabel(imageFile != null ? new ImageIcon(imageFile) : null);
KeyListener aListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
if (verifyAndSaveCurrentSelection()) {
setUserSelection(APPROVE_SELECTION);
dispose();
}
}
else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
setUserSelection(CANCEL_SELECTION);
dispose();
}
}
};
displayResCombo = setUpResolutionChooser();
displayResCombo.addKeyListener(aListener);
colorDepthCombo = new JComboBox();
colorDepthCombo.addKeyListener(aListener);
displayFreqCombo = new JComboBox();
displayFreqCombo.addKeyListener(aListener);
antialiasCombo = new JComboBox();
antialiasCombo.addKeyListener(aListener);
fullscreenBox = new JCheckBox(resourceBundle.getString("checkbox.fullscreen"));
fullscreenBox.setSelected(source.isFullscreen());
fullscreenBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
updateResolutionChoices();
}
});
vsyncBox = new JCheckBox(resourceBundle.getString("checkbox.vsync"));
vsyncBox.setSelected(source.isVSync());
gammaBox = new JCheckBox(resourceBundle.getString("checkbox.gamma"));
gammaBox.setSelected(source.isGammaCorrection());
gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.gridx = 0;
gbc.gridwidth = 2;
gbc.gridy = 1;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(fullscreenBox, gbc);
gbc = new GridBagConstraints();
gbc.weightx = 0.5;
// gbc.insets = new Insets(4, 16, 0, 4);
gbc.gridx = 2;
// gbc.gridwidth = 2;
gbc.gridy = 1;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(vsyncBox, gbc);
gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.gridx = 3;
gbc.gridy = 1;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(gammaBox, gbc);
gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.gridx = 0;
gbc.gridy = 2;
gbc.anchor = GridBagConstraints.EAST;
gbc.weightx = 0.5;
mainPanel.add(new JLabel(resourceBundle.getString("label.resolutions")), gbc);
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 2;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(displayResCombo, gbc);
gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 16, 4, 4);
gbc.gridx = 2;
gbc.gridy = 2;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(new JLabel(resourceBundle.getString("label.colordepth")), gbc);
gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.gridx = 3;
gbc.gridy = 2;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(colorDepthCombo, gbc);
gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.weightx = 0.5;
gbc.gridx = 0;
gbc.gridy = 3;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(new JLabel(resourceBundle.getString("label.refresh")), gbc);
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 3;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(displayFreqCombo, gbc);
gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 16, 4, 4);
gbc.gridx = 2;
gbc.gridy = 3;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(new JLabel(resourceBundle.getString("label.antialias")), gbc);
gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.gridx = 3;
gbc.gridy = 3;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(antialiasCombo, gbc);
// Set the button action listeners. Cancel disposes without saving, OK
// saves.
ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (verifyAndSaveCurrentSelection()) {
setUserSelection(APPROVE_SELECTION);
dispose();
// System.gc() should be called to prevent "X Error of failed request: RenderBadPicture (invalid Picture parameter)"
// on Linux when using AWT/Swing + GLFW.
// For more info see: https://github.com/LWJGL/lwjgl3/issues/149, https://hub.jmonkeyengine.org/t/experimenting-lwjgl3/37275
System.gc();
System.gc();
}
}
});
cancel.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
setUserSelection(CANCEL_SELECTION);
dispose();
}
});
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridwidth = 2;
gbc.gridy = 4;
gbc.anchor = GridBagConstraints.EAST;
mainPanel.add(ok, gbc);
gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 16, 4, 4);
gbc.gridx = 2;
gbc.gridwidth = 2;
gbc.gridy = 4;
gbc.anchor = GridBagConstraints.WEST;
mainPanel.add(cancel, gbc);
if (icon != null) {
gbc = new GridBagConstraints();
gbc.gridwidth = 4;
mainPanel.add(icon, gbc);
}
this.getContentPane().add(mainPanel);
pack();
mainPanel.getRootPane().setDefaultButton(ok);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Fill in the combos once the window has opened so that the insets can be read.
// The assumption is made that the settings window and the display window will have the
// same insets as that is used to resize the "full screen windowed" mode appropriately.
updateResolutionChoices();
if (source.getWidth() != 0 && source.getHeight() != 0) {
displayResCombo.setSelectedItem(source.getWidth() + " x "
+ source.getHeight());
} else {
displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
}
updateAntialiasChoices();
colorDepthCombo.setSelectedItem(source.getBitsPerPixel() + " bpp");
}
});
}
/* Access JDialog.setIconImages by reflection in case we're running on JRE < 1.6 */
private void safeSetIconImages(List<? extends Image> icons) {
try {
// Due to Java bug 6445278, we try to set icon on our shared owner frame first.
// Otherwise, our alt-tab icon will be the Java default under Windows.
Window owner = getOwner();
if (owner != null) {
Method setIconImages = owner.getClass().getMethod("setIconImages", List.class);
setIconImages.invoke(owner, icons);
return;
}
Method setIconImages = getClass().getMethod("setIconImages", List.class);
setIconImages.invoke(this, icons);
} catch (Exception e) {
logger.log(Level.WARNING, "Error setting icon images", e);
}
}
/**
* <code>verifyAndSaveCurrentSelection</code> first verifies that the
* display mode is valid for this system, and then saves the current
* selection as a properties.cfg file.
*
* @return if the selection is valid
*/
private boolean verifyAndSaveCurrentSelection() {
String display = (String) displayResCombo.getSelectedItem();
boolean fullscreen = fullscreenBox.isSelected();
boolean vsync = vsyncBox.isSelected();
boolean gamma = gammaBox.isSelected();
int width = Integer.parseInt(display.substring(0, display.indexOf(" x ")));
display = display.substring(display.indexOf(" x ") + 3);
int height = Integer.parseInt(display);
String depthString = (String) colorDepthCombo.getSelectedItem();
int depth = -1;
if (depthString.equals("???")) {
depth = 0;
} else {
depth = Integer.parseInt(depthString.substring(0, depthString.indexOf(' ')));
}
String freqString = (String) displayFreqCombo.getSelectedItem();
int freq = -1;
if (fullscreen) {
if (freqString.equals("???")) {
freq = 0;
} else {
freq = Integer.parseInt(freqString.substring(0, freqString.indexOf(' ')));
}
}
String aaString = (String) antialiasCombo.getSelectedItem();
int multisample = -1;
if (aaString.equals(resourceBundle.getString("antialias.disabled"))) {
multisample = 0;
} else {
multisample = Integer.parseInt(aaString.substring(0, aaString.indexOf('x')));
}
// FIXME: Does not work in Linux
/*
* if (!fullscreen) { //query the current bit depth of the desktop int
* curDepth = GraphicsEnvironment.getLocalGraphicsEnvironment()
* .getDefaultScreenDevice().getDisplayMode().getBitDepth(); if (depth >
* curDepth) { showError(this,"Cannot choose a higher bit depth in
* windowed " + "mode than your current desktop bit depth"); return
* false; } }
*/
boolean valid = false;
// test valid display mode when going full screen
if (!fullscreen) {
valid = true;
} else {
GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
valid = device.isFullScreenSupported();
}
if (valid) {
//use the GameSettings class to save it.
source.setWidth(width);
source.setHeight(height);
source.setBitsPerPixel(depth);
source.setFrequency(freq);
source.setFullscreen(fullscreen);
source.setVSync(vsync);
source.setGammaCorrection(gamma);
//source.setRenderer(renderer);
source.setSamples(multisample);
String appTitle = source.getTitle();
try {
source.save(appTitle);
} catch (BackingStoreException ex) {
logger.log(Level.WARNING,
"Failed to save setting changes", ex);
}
} else {
showError(
this,
resourceBundle.getString("error.unsupportedmode"));
}
return valid;
}
/**
* <code>setUpChooser</code> retrieves all available display modes and
* places them in a <code>JComboBox</code>. The resolution specified by
* GameSettings is used as the default value.
*
* @return the combo box of display modes.
*/
private JComboBox setUpResolutionChooser() {
JComboBox resolutionBox = new JComboBox();
resolutionBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
updateDisplayChoices();
}
});
return resolutionBox;
}
/**
* <code>updateDisplayChoices</code> updates the available color depth and
* display frequency options to match the currently selected resolution.
*/
private void updateDisplayChoices() {
if (!fullscreenBox.isSelected()) {
// don't run this function when changing windowed settings
return;
}
String resolution = (String) displayResCombo.getSelectedItem();
String colorDepth = (String) colorDepthCombo.getSelectedItem();
if (colorDepth == null) {
colorDepth = source.getBitsPerPixel() + " bpp";
}
String displayFreq = (String) displayFreqCombo.getSelectedItem();
if (displayFreq == null) {
displayFreq = source.getFrequency() + " Hz";
}
// grab available depths
String[] depths = getDepths(resolution, modes);
colorDepthCombo.setModel(new DefaultComboBoxModel(depths));
colorDepthCombo.setSelectedItem(colorDepth);
// grab available frequencies
String[] freqs = getFrequencies(resolution, modes);
displayFreqCombo.setModel(new DefaultComboBoxModel(freqs));
// Try to reset freq
displayFreqCombo.setSelectedItem(displayFreq);
if (!displayFreqCombo.getSelectedItem().equals(displayFreq)) {
// Cannot find saved frequency in available frequencies.
// Choose the closest one to 60 Hz.
displayFreqCombo.setSelectedItem(getBestFrequency(resolution, modes));
}
}
/**
* <code>updateResolutionChoices</code> updates the available resolutions
* list to match the currently selected window mode (fullscreen or
* windowed). It then sets up a list of standard options (if windowed) or
* calls <code>updateDisplayChoices</code> (if fullscreen).
*/
private void updateResolutionChoices() {
if (!fullscreenBox.isSelected()) {
displayResCombo.setModel(new DefaultComboBoxModel(
getWindowedResolutions(windowModes)));
if (displayResCombo.getItemCount() > 0) {
displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
}
colorDepthCombo.setModel(new DefaultComboBoxModel(new String[]{
"24 bpp", "16 bpp"}));
displayFreqCombo.setModel(new DefaultComboBoxModel(
new String[]{resourceBundle.getString("refresh.na")}));
displayFreqCombo.setEnabled(false);
} else {
displayResCombo.setModel(new DefaultComboBoxModel(
getResolutions(modes, Integer.MAX_VALUE, Integer.MAX_VALUE)));
if (displayResCombo.getItemCount() > 0) {
displayResCombo.setSelectedIndex(displayResCombo.getItemCount()-1);
}
displayFreqCombo.setEnabled(true);
updateDisplayChoices();
}
}
private void updateAntialiasChoices() {
// maybe in the future will add support for determining this info
// through pbuffer
String[] choices = new String[]{resourceBundle.getString("antialias.disabled"), "2x", "4x", "6x", "8x", "16x"};
antialiasCombo.setModel(new DefaultComboBoxModel(choices));
antialiasCombo.setSelectedItem(choices[Math.min(source.getSamples()/2,5)]);
}
//
// Utility methods
//
/**
* Utility method for converting a String denoting a file into a URL.
*
* @return a URL pointing to the file or null
*/
private static URL getURL(String file) {
URL url = null;
try {
url = new URL("file:" + file);
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Invalid file name '" + file + "'", e);
}
return url;
}
private static void showError(java.awt.Component parent, String message) {
JOptionPane.showMessageDialog(parent, message, "Error",
JOptionPane.ERROR_MESSAGE);
}
/**
* Returns every unique resolution from an array of <code>DisplayMode</code>s
* where the resolution is greater than the configured minimums.
*/
private String[] getResolutions(DisplayMode[] modes, int heightLimit, int widthLimit) {
Insets insets = getInsets();
heightLimit -= insets.top + insets.bottom;
widthLimit -= insets.left + insets.right;
ArrayList<String> resolutions = new ArrayList<String>(modes.length);
for (int i = 0; i < modes.length; i++) {
int height = modes[i].getHeight();
int width = modes[i].getWidth();
if (width >= minWidth && height >= minHeight) {
if (height >= heightLimit) {
height = heightLimit;
}
if (width >= widthLimit) {
width = widthLimit;
}
String res = width + " x " + height;
if (!resolutions.contains(res)) {
resolutions.add(res);
}
}
}
String[] res = new String[resolutions.size()];
resolutions.toArray(res);
return res;
}
/**
* Returns every unique resolution from an array of <code>DisplayMode</code>s
* where the resolution is greater than the configured minimums and the height
* is less than the current screen resolution.
*/
private String[] getWindowedResolutions(DisplayMode[] modes) {
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < modes.length; i++) {
if (maxHeight < modes[i].getHeight()) {
maxHeight = modes[i].getHeight();
}
if (maxWidth < modes[i].getWidth()) {
maxWidth = modes[i].getWidth();
}
}
return getResolutions(modes, maxHeight, maxWidth);
}
/**
* Returns every possible bit depth for the given resolution.
*/
private static String[] getDepths(String resolution, DisplayMode[] modes) {
ArrayList<String> depths = new ArrayList<String>(4);
for (int i = 0; i < modes.length; i++) {
// Filter out all bit depths lower than 16 - Java incorrectly
// reports
// them as valid depths though the monitor does not support them
if (modes[i].getBitDepth() < 16 && modes[i].getBitDepth() > 0) {
continue;
}
String res = modes[i].getWidth() + " x " + modes[i].getHeight();
String depth = modes[i].getBitDepth() + " bpp";
if (res.equals(resolution) && !depths.contains(depth)) {
depths.add(depth);
}
}
if (depths.size() == 1 && depths.contains("-1 bpp")) {
// add some default depths, possible system is multi-depth supporting
depths.clear();
depths.add("24 bpp");
}
String[] res = new String[depths.size()];
depths.toArray(res);
return res;
}
/**
* Returns every possible refresh rate for the given resolution.
*/
private static String[] getFrequencies(String resolution,
DisplayMode[] modes) {
ArrayList<String> freqs = new ArrayList<String>(4);
for (int i = 0; i < modes.length; i++) {
String res = modes[i].getWidth() + " x " + modes[i].getHeight();
String freq;
if (modes[i].getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) {
freq = "???";
} else {
freq = modes[i].getRefreshRate() + " Hz";
}
if (res.equals(resolution) && !freqs.contains(freq)) {
freqs.add(freq);
}
}
String[] res = new String[freqs.size()];
freqs.toArray(res);
return res;
}
/**
* Chooses the closest frequency to 60 Hz.
*
* @param resolution
* @param modes
* @return
*/
private static String getBestFrequency(String resolution, DisplayMode[] modes) {
int closest = Integer.MAX_VALUE;
int desired = 60;
for (int i = 0; i < modes.length; i++) {
String res = modes[i].getWidth() + " x " + modes[i].getHeight();
int freq = modes[i].getRefreshRate();
if (freq != DisplayMode.REFRESH_RATE_UNKNOWN && res.equals(resolution)) {
if (Math.abs(freq - desired) <
Math.abs(closest - desired)) {
closest = modes[i].getRefreshRate();
}
}
}
if (closest != Integer.MAX_VALUE) {
return closest + " Hz";
} else {
return null;
}
}
/**
* Utility class for sorting <code>DisplayMode</code>s. Sorts by
* resolution, then bit depth, and then finally refresh rate.
*/
private class DisplayModeSorter implements Comparator<DisplayMode> {
/**
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(DisplayMode a, DisplayMode b) {
// Width
if (a.getWidth() != b.getWidth()) {
return (a.getWidth() > b.getWidth()) ? 1 : -1;
}
// Height
if (a.getHeight() != b.getHeight()) {
return (a.getHeight() > b.getHeight()) ? 1 : -1;
}
// Bit depth
if (a.getBitDepth() != b.getBitDepth()) {
return (a.getBitDepth() > b.getBitDepth()) ? 1 : -1;
}
// Refresh rate
if (a.getRefreshRate() != b.getRefreshRate()) {
return (a.getRefreshRate() > b.getRefreshRate()) ? 1 : -1;
}
// All fields are equal
return 0;
}
}
}