// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import org.infinity.NearInfinity;
import org.infinity.icon.Icons;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.util.Pair;
/**
* Display verbose information about the currently selected game.
*/
public final class GameProperties extends ChildFrame implements ActionListener
{
private static final EnumMap<Profile.Key, String> RES_TYPES = new EnumMap<>(Profile.Key.class);
static {
RES_TYPES.put(Profile.Key.IS_SUPPORTED_2DA, "2DA V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ACM, "ACM");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ARE_V10, "ARE V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ARE_V91, "ARE V9.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BAM_V1, "BAM V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BAMC_V1, "BAMC V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BAM_V2, "BAM V2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BCS, "BCS");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BIFF, "BIFF V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BIFC, "BIFC V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BIF, "BIF V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BIK, "BIK");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BIO, "BIO");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BMP_PAL, "BMP (8-bit)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_BMP_ALPHA, "BMP (32-bit)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CHR_V10, "CHR V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CHR_V20, "CHR V2.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CHR_V21, "CHR V2.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CHR_V22, "CHR V2.2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CHU, "CHU V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CRE_V10, "CRE V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CRE_V12, "CRE V1.2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CRE_V22, "CRE V2.2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_CRE_V90, "CRE V9.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_DLG, "DLG V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_EFF, "EFF V2.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_FNT, "FNT");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GAM_V11, "GAM V1.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GAM_V20, "GAM V2.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GAM_V21, "GAM V2.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GAM_V22, "GAM V2.2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GLSL, "GLSL");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_GUI, "GUI");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_IDS, "IDS V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_INI, "INI");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ITM_V10, "ITM V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ITM_V11, "ITM V1.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_ITM_V20, "ITM V2.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_KEY, "KEY V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_LUA, "LUA");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MENU, "MENU");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MOS_V1, "MOS V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MOSC_V1, "MOSC V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MOS_V2, "MOS V2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MUS, "MUS");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_MVE, "MVE");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_OGG, "OGG");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_PLT, "PLT V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_PNG, "PNG");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_PRO, "PRO V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_PVRZ, "PVRZ");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_RES, "RES");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SAV, "SAV V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SPL_V1, "SPL V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SPL_V2, "SPL V2.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SQL, "SQL");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SRC_PST, "SRC (PS:T)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_SRC_IWD2, "SRC (IWD2)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_STO_V10, "STO V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_STO_V11, "STO V1.1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_STO_V90, "STO V9.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TIS_V1, "TIS (Tiled)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TIS_V2, "TIS (PVRZ)");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TLK, "TLK V1");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TO_V1, "TOH/TOT");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TO_V2, "TOH V2");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_TTF, "TTF");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_VAR, "VAR");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_VEF, "VEF");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_VVC, "VVC V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WAV, "WAV");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WAVC, "WAVC V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WBM, "WBM");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WED, "WED V1.3");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WFX, "WFX V1.0");
RES_TYPES.put(Profile.Key.IS_SUPPORTED_WMP, "WMP V1.0");
}
private final JButton bClose = new JButton("Close");
private final JButton bEdit = new JButton("Edit...");
public GameProperties(Window owner)
{
super("Game Properties", true);
init();
}
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (event.getSource() == bClose) {
setVisible(false);
} else if (event.getSource() == bEdit) {
NearInfinity.getInstance().editGameIni(this);
}
}
//--------------------- End Interface ActionListener ---------------------
private void init()
{
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLayout(new BorderLayout());
GridBagConstraints gbc = new GridBagConstraints();
List<Pair<JComponent>> listControls = new ArrayList<Pair<JComponent>>();
JLabel l;
JTextField tf;
String s;
// Entry: game type
s = Profile.getProperty(Profile.Key.GET_GAME_TITLE);
l = new JLabel("Game type:");
tf = createReadOnlyField(s, true);
listControls.add(new Pair<JComponent>(l, tf));
// Entry: profile name
s = Profile.getProperty(Profile.Key.GET_GAME_DESC);
l = new JLabel("Profile name:");
tf = createReadOnlyField((s != null) ? s : "n/a", true);
listControls.add(new Pair<JComponent>(l, tf));
// Entry: game folder
s = (Profile.getGameRoot()).toString();
l = new JLabel("Game folder:");
tf = createReadOnlyField(s, true);
listControls.add(new Pair<JComponent>(l, tf));
if (Profile.isEnhancedEdition()) {
// Entry: home folder
s = Profile.getHomeRoot().toString();
l = new JLabel("Home folder:");
tf = createReadOnlyField(s, true);
listControls.add(new Pair<JComponent>(l, tf));
// Entry: detected DLC
List<Path> dlcPaths = Profile.getProperty(Profile.Key.GET_GAME_DLC_FOLDERS_AVAILABLE);
if (dlcPaths != null && !dlcPaths.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (final Path dlcPath: dlcPaths) {
if (sb.length() > 0) {
sb.append("; ");
}
sb.append(dlcPath.getFileSystem().toString());
}
l = new JLabel("DLC archives:");
tf = createReadOnlyField(sb.toString(), true);
listControls.add(new Pair<JComponent>(l, tf));
}
// Entry: available languages
List<String> languages = Profile.getProperty(Profile.Key.GET_GAME_LANG_FOLDER_NAMES_AVAILABLE);
StringBuilder sb = new StringBuilder();
s = ResourceFactory.autodetectGameLanguage(Profile.getProperty(Profile.Key.GET_GAME_INI_FILE));
if (s != null) {
sb.append(String.format("Autodetect (%1$s)", getLanguageName(s)));
}
if (languages != null) {
for (final String lang: languages) {
if (lang != null && !lang.isEmpty()) {
sb.append(String.format(", %1$s", getLanguageName(lang)));
}
}
}
l = new JLabel("Available languages:");
tf = createReadOnlyField(sb.toString(), true);
listControls.add(new Pair<JComponent>(l, tf));
// Entry: language
s = getLanguageName(Profile.getProperty(Profile.Key.GET_GAME_LANG_FOLDER_NAME));
l = new JLabel("Current language:");
tf = createReadOnlyField(s, true);
listControls.add(new Pair<JComponent>(l, tf));
}
// Entry: Use female TLK file
l = new JLabel("Uses female TLK file:");
tf = createReadOnlyField(Boolean.toString((Profile.getProperty(Profile.Key.GET_GAME_DIALOGF_FILE) != null)), true);
listControls.add(new Pair<JComponent>(l, tf));
// Entry: game's ini file
l = new JLabel("Game's INI file:");
JPanel pIni = new JPanel(new GridBagLayout());
Path iniFile = Profile.getProperty(Profile.Key.GET_GAME_INI_FILE);
s = (iniFile != null) ? iniFile.toString() : "n/a";
tf = createReadOnlyField(s, true);
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pIni.add(tf, gbc);
bEdit.setMargin(new Insets(2, 4, 2, 4));
bEdit.addActionListener(this);
bEdit.setEnabled(iniFile != null && Files.isRegularFile(iniFile));
gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
pIni.add(bEdit, gbc);
listControls.add(new Pair<JComponent>(l, pIni));
// adding controls from listControls to dialog
JPanel pFixed = new JPanel(new GridBagLayout());
int row = 0;
for (Iterator<Pair<JComponent>> iter = listControls.iterator(); iter.hasNext();) {
Pair<JComponent> pair = iter.next();
gbc = ViewerUtil.setGBC(gbc, 0, row, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 0, 0, 0), 0, 0);
pFixed.add(pair.getFirst(), gbc);
gbc = ViewerUtil.setGBC(gbc, 1, row, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 0, 0), 0, 0);
pFixed.add(pair.getSecond(), gbc);
row++;
}
// setting max. preferred width
DisplayMode dm = NearInfinity.getInstance().getGraphicsConfiguration().getDevice().getDisplayMode();
if (pFixed.getPreferredSize().width > (dm.getWidth() / 2)) {
pFixed.setPreferredSize(new Dimension(dm.getWidth() / 2, pFixed.getPreferredSize().height));
}
// adding list of supported file formats
FlowLayout flow = new FlowLayout(FlowLayout.LEFT, 8, 4);
JPanel pSupportList = new JPanel(flow);
pSupportList.setBorder(BorderFactory.createLineBorder(Color.BLACK));
List<JLabel> listTypes = new ArrayList<JLabel>();
int maxWidth = 0, maxHeight = 0;
// preparing entries
for (Iterator<Profile.Key> iter = RES_TYPES.keySet().iterator(); iter.hasNext();) {
Profile.Key key = iter.next();
if (key != null) {
JLabel label = createCheckLabel(key, RES_TYPES.get(key));
maxWidth = Math.max(maxWidth, label.getPreferredSize().width);
maxHeight = Math.max(maxHeight, label.getPreferredSize().height);
listTypes.add(label);
}
}
Collections.sort(listTypes, new Comparator<JLabel>() {
@Override
public int compare(JLabel o1, JLabel o2)
{
return o1.getText().compareToIgnoreCase(o2.getText());
}
});
// setting preferred size to fit all entries
int itemsPerRow = pFixed.getPreferredSize().width / (maxWidth + flow.getHgap());
if (pFixed.getPreferredSize().width % (maxWidth + flow.getHgap()) > (maxWidth / 2)) {
itemsPerRow++; // prevent bigger gaps on the right side
}
itemsPerRow = Math.max(itemsPerRow, 4);
int numCols = ((listTypes.size() + itemsPerRow - 1) / itemsPerRow);
int panelWidth = itemsPerRow * (maxWidth + flow.getHgap()) + flow.getHgap() +
pSupportList.getInsets().left + pSupportList.getInsets().right;
int panelHeight = numCols*(maxHeight + flow.getVgap()) + flow.getVgap() +
pSupportList.getInsets().top + pSupportList.getInsets().bottom;
pSupportList.setPreferredSize(new Dimension(panelWidth, panelHeight));
// adding entries to GUI
for (Iterator<JLabel> iter = listTypes.iterator(); iter.hasNext();) {
JLabel label = iter.next();
Dimension d = label.getPreferredSize();
d.width = maxWidth;
label.setPreferredSize(d);
pSupportList.add(label);
}
// creating actual panel for supported resource types
JPanel pSupport = new JPanel(new GridBagLayout());
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pSupport.add(new JLabel("Supported resource types:"), gbc);
gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pSupport.add(new JPanel(), gbc);
gbc = ViewerUtil.setGBC(gbc, 0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.BOTH, new Insets(4, 0, 0, 0), 0, 0);
pSupport.add(pSupportList, gbc);
// adding close button
JPanel pButton = new JPanel(new GridBagLayout());
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pButton.add(new JPanel(), gbc);
bClose.addActionListener(this);
gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pButton.add(bClose, gbc);
gbc = ViewerUtil.setGBC(gbc, 2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pButton.add(new JPanel(), gbc);
// putting everything together
JPanel pMain = new JPanel(new GridBagLayout());
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 0, 8), 0, 0);
pMain.add(pFixed, gbc);
gbc = ViewerUtil.setGBC(gbc, 0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.BOTH, new Insets(8, 8, 0, 8), 0, 0);
pMain.add(pSupport, gbc);
gbc = ViewerUtil.setGBC(gbc, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 8, 8), 0, 0);
pMain.add(pButton, gbc);
add(pMain, BorderLayout.CENTER);
pack();
setLocationRelativeTo(getOwner());
bClose.requestFocusInWindow();
getRootPane().setDefaultButton(bClose);
setVisible(true);
}
// Returns the name of the language specified by the given language code
private static String getLanguageName(String langCode)
{
if (langCode != null && langCode.matches("[a-z]{2}_[A-Z]{2}")) {
String lang[] = langCode.split("_");
if (lang.length >= 2) {
String name = (new Locale(lang[0], lang[1])).getDisplayLanguage();
if (name != null && !name.isEmpty()) {
return name;
}
}
}
return langCode;
}
// Creates a read-only text field, optionally with visible caret
private static JTextField createReadOnlyField(String text, boolean showCaret)
{
JTextField tf = new JTextField();
if (showCaret) {
tf.addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e)
{
JTextField tf = (JTextField)e.getSource();
tf.getCaret().setVisible(false);
}
@Override
public void focusGained(FocusEvent e)
{
JTextField tf = (JTextField)e.getSource();
tf.getCaret().setVisible(true);
}
});
}
tf.setEditable(false);
tf.setFont(UIManager.getFont("Label.font"));
if (text != null) {
tf.setText(text);
tf.setCaretPosition(0);
}
return tf;
}
// Creates a label with a graphical icon specifying checked or unchecked state
private static JLabel createCheckLabel(Profile.Key key, String desc)
{
if (key != null && desc != null && Profile.getProperty(key) instanceof Boolean) {
ImageIcon icon;
if ((Boolean)Profile.getProperty(key)) {
icon = Icons.getIcon(Icons.ICON_CHECK_16);
} else {
icon = Icons.getIcon(Icons.ICON_CHECK_NOT_16);
}
JLabel label = new JLabel(desc, icon, SwingConstants.LEFT);
label.setFont(new Font(Font.MONOSPACED, label.getFont().getStyle(), label.getFont().getSize()));
return label;
} else {
return null;
}
}
}