/*******************************************************************************
 * Copyright 2011-2015 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.tools.hiero;

import static org.lwjgl.opengl.GL11.*;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FileDialog;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.prefs.Preferences;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.lwjgl.opengl.GL11;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl.LwjglCanvas;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.tools.hiero.unicodefont.GlyphPage;
import com.badlogic.gdx.tools.hiero.unicodefont.UnicodeFont;
import com.badlogic.gdx.tools.hiero.unicodefont.UnicodeFont.RenderType;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.ColorEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.ConfigurableEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.ConfigurableEffect.Value;
import com.badlogic.gdx.utils.StringBuilder;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.DistanceFieldEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.EffectUtil;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.GradientEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.OutlineEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.OutlineWobbleEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.OutlineZigzagEffect;
import com.badlogic.gdx.tools.hiero.unicodefont.effects.ShadowEffect;

/** A tool to visualize settings for {@link UnicodeFont} and to export BMFont files for use with {@link BitmapFont}.
 * <p>
 * This tool is quite old. It was originally written for Slick, then ported to libgdx. It has had a few different contributors
 * over the years, the FreeType rendering was tacked on and in general the code isn't great. It does its job though!
 * @author Nathan Sweet */
public class Hiero extends JFrame {
	UnicodeFont unicodeFont;
	Color renderingBackgroundColor = Color.BLACK;
	List<EffectPanel> effectPanels = new ArrayList<EffectPanel>();
	Preferences prefs;
	ColorEffect colorEffect;
	boolean batchMode = false;

	JScrollPane appliedEffectsScroll;
	JPanel appliedEffectsPanel;
	JButton addEffectButton;
	JTextPane sampleTextPane;
	JSpinner padAdvanceXSpinner;
	JList effectsList;
	LwjglCanvas rendererCanvas;
	JPanel gamePanel;
	JTextField fontFileText;
	JRadioButton fontFileRadio;
	JRadioButton systemFontRadio;
	JSpinner padBottomSpinner;
	JSpinner padLeftSpinner;
	JSpinner padRightSpinner;
	JSpinner padTopSpinner;
	JList fontList;
	JSpinner fontSizeSpinner;
	JSpinner gammaSpinner;
	DefaultComboBoxModel fontListModel;
	JLabel backgroundColorLabel;
	JButton browseButton;
	JSpinner padAdvanceYSpinner;
	JCheckBox italicCheckBox;
	JCheckBox boldCheckBox;
	JCheckBox monoCheckBox;
	JRadioButton javaRadio;
	JRadioButton nativeRadio;
	JRadioButton freeTypeRadio;
	JLabel glyphsTotalLabel;
	JLabel glyphPagesTotalLabel;
	JComboBox glyphPageHeightCombo;
	JComboBox glyphPageWidthCombo;
	JComboBox glyphPageCombo;
	JPanel glyphCachePanel;
	JRadioButton glyphCacheRadio;
	JRadioButton sampleTextRadio;
	DefaultComboBoxModel glyphPageComboModel;
	JButton resetCacheButton;
	JButton sampleAsciiButton;
	JButton sampleNeheButton;
	JButton sampleExtendedButton;
	DefaultComboBoxModel effectsListModel;
	JMenuItem openMenuItem;
	JMenuItem saveMenuItem;
	JMenuItem exitMenuItem;
	JMenuItem saveBMFontMenuItem;
	File saveBmFontFile;
	String lastSaveFilename = "", lastSaveBMFilename = "", lastOpenFilename = "";
	JPanel effectsPanel;
	JScrollPane effectsScroll;
	JPanel unicodePanel, bitmapPanel;

	public Hiero (String[] args) {
		super("Hiero v5 - Bitmap Font Tool");
		Splash splash = new Splash(this, "/splash.jpg", 2000);
		initialize();
		splash.close();

		rendererCanvas = new LwjglCanvas(new Renderer());
		gamePanel.add(rendererCanvas.getCanvas());

		prefs = Preferences.userNodeForPackage(Hiero.class);
		java.awt.Color backgroundColor = EffectUtil.fromString(prefs.get("background", "000000"));
		backgroundColorLabel.setIcon(getColorIcon(backgroundColor));
		renderingBackgroundColor = new Color(backgroundColor.getRed() / 255f, backgroundColor.getGreen() / 255f,
			backgroundColor.getBlue() / 255f, 1);
		fontList.setSelectedValue(prefs.get("system.font", "Arial"), true);
		fontFileText.setText(prefs.get("font.file", ""));

		java.awt.Color foregroundColor = EffectUtil.fromString(prefs.get("foreground", "ffffff"));
		colorEffect = new ColorEffect();
		colorEffect.setColor(foregroundColor);
		effectsListModel.addElement(colorEffect);
		effectsListModel.addElement(new GradientEffect());
		effectsListModel.addElement(new OutlineEffect());
		effectsListModel.addElement(new OutlineWobbleEffect());
		effectsListModel.addElement(new OutlineZigzagEffect());
		effectsListModel.addElement(new ShadowEffect());
		effectsListModel.addElement(new DistanceFieldEffect());
		new EffectPanel(colorEffect);

		parseArgs(args);

		addWindowListener(new WindowAdapter() {
			public void windowClosed (WindowEvent event) {
				System.exit(0);
				// Gdx.app.quit();
			}
		});

		updateFontSelector();
		setVisible(true);
	}

	void initialize () {
		initializeComponents();
		initializeMenus();
		initializeEvents();

		setSize(1024, 600);
		setLocationRelativeTo(null);
		setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
	}

	private void parseArgs (String[] args) {
		float scale = 1f;

		for (int i = 0; i < args.length; i++) {
			final String param = args[i];
			final boolean more = i < args.length - 1;

			if (param.equals("-b") || param.equals("--batch")) {
				batchMode = true;
			} else if (more && (param.equals("-s") || param.equals("--scale"))) {
				scale = Float.parseFloat(args[++i]);
			} else if (more && (param.equals("-i") || param.equals("--input"))) {
				File f = new File(args[++i]);
				open(f);
				fontFileRadio.setText("");
				updateFont();
			} else if (more && (param.equals("-o") || param.equals("--output"))) {
				File f = new File(args[++i]);
				saveBm(f);
			} else {
				System.err.println("Unknown parameter: " + param);
				exit(3);
			}
		}

		fontSizeSpinner.setValue((int)(0.5f + Math.max(4, scale * ((Integer)fontSizeSpinner.getValue()))));
	}

	void updateFontSelector () {
		final boolean useFile = fontFileRadio.isSelected();
		fontList.setEnabled(!useFile);
		fontFileText.setEnabled(useFile);
		browseButton.setEnabled(useFile);
	}

	void updateFont () {
		int fontSize = ((Integer)fontSizeSpinner.getValue()).intValue();

		File file = null;
		if (fontFileRadio.isSelected()) {
			file = new File(fontFileText.getText());
			if (!file.exists() || !file.isFile()) file = null;
		}

		boolean isFreeType = freeTypeRadio.isSelected();
		boolean isNative = nativeRadio.isSelected();
		boolean isJava = javaRadio.isSelected();
		addEffectButton.setVisible(isJava);
		effectsScroll.setVisible(isJava);
		appliedEffectsScroll.setVisible(isJava);
		boldCheckBox.setEnabled(!isFreeType);
		italicCheckBox.setEnabled(!isFreeType);
		bitmapPanel.setVisible(isFreeType);
		unicodePanel.setVisible(!isFreeType);
		updateFontSelector();

		UnicodeFont unicodeFont = null;
		if (file != null) {
			// Load from file.
			try {
				unicodeFont = new UnicodeFont(fontFileText.getText(), fontSize, boldCheckBox.isSelected(),
					italicCheckBox.isSelected());
			} catch (Throwable ex) {
				ex.printStackTrace();
				fontFileRadio.setSelected(false);
			}
		}

		if (unicodeFont == null) {
			// Load from java.awt.Font (kerning not available!).
			unicodeFont = new UnicodeFont(Font.decode((String)fontList.getSelectedValue()), fontSize, boldCheckBox.isSelected(),
				italicCheckBox.isSelected());
		}

		unicodeFont.setMono(monoCheckBox.isSelected());
		unicodeFont.setGamma(((Number)gammaSpinner.getValue()).floatValue());
		unicodeFont.setPaddingTop(((Number)padTopSpinner.getValue()).intValue());
		unicodeFont.setPaddingRight(((Number)padRightSpinner.getValue()).intValue());
		unicodeFont.setPaddingBottom(((Number)padBottomSpinner.getValue()).intValue());
		unicodeFont.setPaddingLeft(((Number)padLeftSpinner.getValue()).intValue());
		unicodeFont.setPaddingAdvanceX(((Number)padAdvanceXSpinner.getValue()).intValue());
		unicodeFont.setPaddingAdvanceY(((Number)padAdvanceYSpinner.getValue()).intValue());
		unicodeFont.setGlyphPageWidth(((Number)glyphPageWidthCombo.getSelectedItem()).intValue());
		unicodeFont.setGlyphPageHeight(((Number)glyphPageHeightCombo.getSelectedItem()).intValue());
		if (nativeRadio.isSelected())
			unicodeFont.setRenderType(RenderType.Native);
		else if (freeTypeRadio.isSelected())
			unicodeFont.setRenderType(RenderType.FreeType);
		else
			unicodeFont.setRenderType(RenderType.Java);

		for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {
			EffectPanel panel = (EffectPanel)iter.next();
			unicodeFont.getEffects().add(panel.getEffect());
		}

		int size = sampleTextPane.getFont().getSize();
		if (size < 14) size = 14;
		sampleTextPane.setFont(unicodeFont.getFont().deriveFont((float)size));

		if (this.unicodeFont != null) this.unicodeFont.dispose();
		this.unicodeFont = unicodeFont;

		updateFontSelector();
	}

	void saveBm (File file) {
		saveBmFontFile = file;
	}

	void save (File file) throws IOException {
		HieroSettings settings = new HieroSettings();
		settings.setFontName((String)fontList.getSelectedValue());
		settings.setFontSize(((Integer)fontSizeSpinner.getValue()).intValue());
		settings.setFont2File(fontFileText.getText());
		settings.setFont2Active(fontFileRadio.isSelected());
		settings.setBold(boldCheckBox.isSelected());
		settings.setItalic(italicCheckBox.isSelected());
		settings.setMono(monoCheckBox.isSelected());
		settings.setGamma(((Number)gammaSpinner.getValue()).floatValue());
		settings.setPaddingTop(((Number)padTopSpinner.getValue()).intValue());
		settings.setPaddingRight(((Number)padRightSpinner.getValue()).intValue());
		settings.setPaddingBottom(((Number)padBottomSpinner.getValue()).intValue());
		settings.setPaddingLeft(((Number)padLeftSpinner.getValue()).intValue());
		settings.setPaddingAdvanceX(((Number)padAdvanceXSpinner.getValue()).intValue());
		settings.setPaddingAdvanceY(((Number)padAdvanceYSpinner.getValue()).intValue());
		settings.setGlyphPageWidth(((Number)glyphPageWidthCombo.getSelectedItem()).intValue());
		settings.setGlyphPageHeight(((Number)glyphPageHeightCombo.getSelectedItem()).intValue());
		settings.setGlyphText(sampleTextPane.getText());
		for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {
			EffectPanel panel = (EffectPanel)iter.next();
			settings.getEffects().add(panel.getEffect());
		}
		settings.save(file);
	}

	void open (File file) {
		EffectPanel[] panels = (EffectPanel[])effectPanels.toArray(new EffectPanel[effectPanels.size()]);
		for (int i = 0; i < panels.length; i++)
			panels[i].remove();

		HieroSettings settings = new HieroSettings(file.getAbsolutePath());
		fontList.setSelectedValue(settings.getFontName(), true);
		fontSizeSpinner.setValue(new Integer(settings.getFontSize()));
		boldCheckBox.setSelected(settings.isBold());
		italicCheckBox.setSelected(settings.isItalic());
		monoCheckBox.setSelected(settings.isMono());
		gammaSpinner.setValue(new Float(settings.getGamma()));
		padTopSpinner.setValue(new Integer(settings.getPaddingTop()));
		padRightSpinner.setValue(new Integer(settings.getPaddingRight()));
		padBottomSpinner.setValue(new Integer(settings.getPaddingBottom()));
		padLeftSpinner.setValue(new Integer(settings.getPaddingLeft()));
		padAdvanceXSpinner.setValue(new Integer(settings.getPaddingAdvanceX()));
		padAdvanceYSpinner.setValue(new Integer(settings.getPaddingAdvanceY()));
		glyphPageWidthCombo.setSelectedItem(new Integer(settings.getGlyphPageWidth()));
		glyphPageHeightCombo.setSelectedItem(new Integer(settings.getGlyphPageHeight()));
		String gt = settings.getGlyphText();
		if (gt.length() > 0) {
			sampleTextPane.setText(settings.getGlyphText());
		}

		final String font2 = settings.getFont2File();
		if (font2.length() > 0)
			fontFileText.setText(font2);
		else
			fontFileText.setText(prefs.get("font.file", ""));

		fontFileRadio.setSelected(settings.isFont2Active());
		systemFontRadio.setSelected(!settings.isFont2Active());

		for (Iterator iter = settings.getEffects().iterator(); iter.hasNext();) {
			ConfigurableEffect settingsEffect = (ConfigurableEffect)iter.next();
			for (int i = 0, n = effectsListModel.getSize(); i < n; i++) {
				ConfigurableEffect effect = (ConfigurableEffect)effectsListModel.getElementAt(i);
				if (effect.getClass() == settingsEffect.getClass()) {
					effect.setValues(settingsEffect.getValues());
					new EffectPanel(effect);
					break;
				}
			}
		}

		updateFont();
	}

	void exit (final int exitCode) {
		rendererCanvas.stop();
		EventQueue.invokeLater(new Runnable() {
			public void run () {
				System.exit(exitCode);
			}
		});
	}

	private void initializeEvents () {
		fontList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged (ListSelectionEvent evt) {
				if (evt.getValueIsAdjusting()) return;
				prefs.put("system.font", (String)fontList.getSelectedValue());
				updateFont();
			}
		});

		class FontUpdateListener implements ChangeListener, ActionListener {
			public void stateChanged (ChangeEvent evt) {
				updateFont();
			}

			public void actionPerformed (ActionEvent evt) {
				updateFont();
			}

			public void addSpinners (JSpinner[] spinners) {
				for (int i = 0; i < spinners.length; i++) {
					final JSpinner spinner = spinners[i];
					spinner.addChangeListener(this);
					((JSpinner.DefaultEditor)spinner.getEditor()).getTextField().addKeyListener(new KeyAdapter() {
						String lastText;

						public void keyReleased (KeyEvent evt) {
							JFormattedTextField textField = ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField();
							String text = textField.getText();
							if (text.length() == 0) return;
							if (text.equals(lastText)) return;
							lastText = text;
							int caretPosition = textField.getCaretPosition();
							try {
								spinner.setValue(Integer.valueOf(text));
							} catch (NumberFormatException ex) {
							}
							textField.setCaretPosition(caretPosition);
						}
					});
				}
			}
		}
		FontUpdateListener listener = new FontUpdateListener();

		listener.addSpinners(new JSpinner[] {padTopSpinner, padRightSpinner, padBottomSpinner, padLeftSpinner, padAdvanceXSpinner,
			padAdvanceYSpinner});
		fontSizeSpinner.addChangeListener(listener);
		gammaSpinner.addChangeListener(listener);

		glyphPageWidthCombo.addActionListener(listener);
		glyphPageHeightCombo.addActionListener(listener);
		boldCheckBox.addActionListener(listener);
		italicCheckBox.addActionListener(listener);
		monoCheckBox.addActionListener(listener);
		resetCacheButton.addActionListener(listener);
		javaRadio.addActionListener(listener);
		nativeRadio.addActionListener(listener);
		freeTypeRadio.addActionListener(listener);

		sampleTextRadio.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				glyphCachePanel.setVisible(false);
			}
		});
		glyphCacheRadio.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				glyphCachePanel.setVisible(true);
			}
		});

		fontFileText.getDocument().addDocumentListener(new DocumentListener() {
			public void removeUpdate (DocumentEvent evt) {
				changed();
			}

			public void insertUpdate (DocumentEvent evt) {
				changed();
			}

			public void changedUpdate (DocumentEvent evt) {
				changed();
			}

			private void changed () {
				File file = new File(fontFileText.getText());
				if (fontList.isEnabled() && (!file.exists() || !file.isFile())) return;
				prefs.put("font.file", fontFileText.getText());
				updateFont();
			}
		});

		final ActionListener al = new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				updateFontSelector();
				updateFont();
			}
		};

		systemFontRadio.addActionListener(al);
		fontFileRadio.addActionListener(al);

		browseButton.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				FileDialog dialog = new FileDialog(Hiero.this, "Choose TrueType font file", FileDialog.LOAD);
				dialog.setLocationRelativeTo(null);
				dialog.setFile("*.ttf");
				dialog.setDirectory(prefs.get("dir.font", ""));
				dialog.setVisible(true);
				if (dialog.getDirectory() != null) {
					prefs.put("dir.font", dialog.getDirectory());
				}

				String fileName = dialog.getFile();
				if (fileName == null) return;
				fontFileText.setText(new File(dialog.getDirectory(), fileName).getAbsolutePath());
			}
		});

		backgroundColorLabel.addMouseListener(new MouseAdapter() {
			public void mouseClicked (MouseEvent evt) {
				java.awt.Color color = JColorChooser.showDialog(null, "Choose a background color",
					EffectUtil.fromString(prefs.get("background", "000000")));
				if (color == null) return;
				renderingBackgroundColor = new Color(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1);
				backgroundColorLabel.setIcon(getColorIcon(color));
				prefs.put("background", EffectUtil.toString(color));
			}
		});

		effectsList.addListSelectionListener(new ListSelectionListener() {
			public void valueChanged (ListSelectionEvent evt) {
				ConfigurableEffect selectedEffect = (ConfigurableEffect)effectsList.getSelectedValue();
				boolean enabled = selectedEffect != null;
				for (Iterator iter = effectPanels.iterator(); iter.hasNext();) {
					ConfigurableEffect effect = ((EffectPanel)iter.next()).getEffect();
					if (effect == selectedEffect) {
						enabled = false;
						break;
					}
				}
				addEffectButton.setEnabled(enabled);
			}
		});

		effectsList.addMouseListener(new MouseAdapter() {
			public void mouseClicked (MouseEvent evt) {
				if (evt.getClickCount() == 2 && addEffectButton.isEnabled()) addEffectButton.doClick();
			}
		});

		addEffectButton.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				new EffectPanel((ConfigurableEffect)effectsList.getSelectedValue());
			}
		});

		openMenuItem.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				FileDialog dialog = new FileDialog(Hiero.this, "Open Hiero settings file", FileDialog.LOAD);
				dialog.setLocationRelativeTo(null);
				dialog.setFile("*.hiero");
				dialog.setDirectory(prefs.get("dir.open", ""));
				dialog.setVisible(true);
				if (dialog.getDirectory() != null) {
					prefs.put("dir.open", dialog.getDirectory());
				}

				String fileName = dialog.getFile();
				if (fileName == null) return;
				lastOpenFilename = fileName;
				open(new File(dialog.getDirectory(), fileName));
			}
		});

		saveMenuItem.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				FileDialog dialog = new FileDialog(Hiero.this, "Save Hiero settings file", FileDialog.SAVE);
				dialog.setLocationRelativeTo(null);
				dialog.setFile("*.hiero");
				dialog.setDirectory(prefs.get("dir.save", ""));

				if (lastSaveFilename.length() > 0) {
					dialog.setFile(lastSaveFilename);
				} else if (lastOpenFilename.length() > 0) {
					dialog.setFile(lastOpenFilename);
				}

				dialog.setVisible(true);

				if (dialog.getDirectory() != null) {
					prefs.put("dir.save", dialog.getDirectory());
				}

				String fileName = dialog.getFile();
				if (fileName == null) return;
				if (!fileName.endsWith(".hiero")) fileName += ".hiero";
				lastSaveFilename = fileName;
				File file = new File(dialog.getDirectory(), fileName);
				try {
					save(file);
				} catch (IOException ex) {
					throw new RuntimeException("Error saving Hiero settings file: " + file.getAbsolutePath(), ex);
				}
			}
		});

		saveBMFontMenuItem.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				FileDialog dialog = new FileDialog(Hiero.this, "Save BMFont files", FileDialog.SAVE);
				dialog.setLocationRelativeTo(null);
				dialog.setFile("*.fnt");
				dialog.setDirectory(prefs.get("dir.savebm", ""));

				if (lastSaveBMFilename.length() > 0) {
					dialog.setFile(lastSaveBMFilename);
				} else if (lastOpenFilename.length() > 0) {
					dialog.setFile(lastOpenFilename.replace(".hiero", ".fnt"));
				}

				dialog.setVisible(true);
				if (dialog.getDirectory() != null) {
					prefs.put("dir.savebm", dialog.getDirectory());
				}

				String fileName = dialog.getFile();
				if (fileName == null) return;
				lastSaveBMFilename = fileName;
				saveBm(new File(dialog.getDirectory(), fileName));
			}
		});

		exitMenuItem.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				dispose();
			}
		});

		sampleNeheButton.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				sampleTextPane.setText(NEHE_CHARS);
				resetCacheButton.doClick();
			}
		});

		sampleAsciiButton.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				StringBuilder buffer = new StringBuilder();
				buffer.append(NEHE_CHARS);
				buffer.append('\n');
				int count = 0;
				for (int i = 33; i <= 255; i++) {
					if (buffer.indexOf(Character.toString((char)i)) != -1) continue;
					buffer.append((char)i);
					if (++count % 30 == 0) buffer.append('\n');
				}
				sampleTextPane.setText(buffer.toString());
				resetCacheButton.doClick();
			}
		});

		sampleExtendedButton.addActionListener(new ActionListener() {
			public void actionPerformed (ActionEvent evt) {
				sampleTextPane.setText(EXTENDED_CHARS);
				resetCacheButton.doClick();
			}
		});
	}

	private void initializeComponents () {
		getContentPane().setLayout(new GridBagLayout());
		JPanel leftSidePanel = new JPanel();
		leftSidePanel.setLayout(new GridBagLayout());
		getContentPane().add(leftSidePanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
		{
			JPanel fontPanel = new JPanel();
			leftSidePanel.add(fontPanel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
			fontPanel.setLayout(new GridBagLayout());
			fontPanel.setBorder(BorderFactory.createTitledBorder("Font"));
			{
				fontSizeSpinner = new JSpinner(new SpinnerNumberModel(32, 0, 256, 1));
				fontPanel.add(fontSizeSpinner, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 10), 0, 0));
				((JSpinner.DefaultEditor)fontSizeSpinner.getEditor()).getTextField().setColumns(2);
			}
			{
				JScrollPane fontScroll = new JScrollPane();
				fontPanel.add(fontScroll, new GridBagConstraints(1, 1, 3, 1, 1.0, 1.0, GridBagConstraints.CENTER,
					GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));
				{
					fontListModel = new DefaultComboBoxModel(
						GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
					fontList = new JList();
					fontScroll.setViewportView(fontList);
					fontList.setModel(fontListModel);
					fontList.setVisibleRowCount(6);
					fontList.setSelectedIndex(0);
					fontScroll.setMinimumSize(new Dimension(220, fontList.getPreferredScrollableViewportSize().height));
				}
			}
			{
				systemFontRadio = new JRadioButton("System:", true);
				fontPanel.add(systemFontRadio, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST,
					GridBagConstraints.NONE, new Insets(0, 5, 0, 5), 0, 0));
				systemFontRadio.setMargin(new Insets(0, 0, 0, 0));
			}
			{
				fontFileRadio = new JRadioButton("File:");
				fontPanel.add(fontFileRadio, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				fontFileRadio.setMargin(new Insets(0, 0, 0, 0));
			}
			{
				fontFileText = new JTextField();
				fontPanel.add(fontFileText, new GridBagConstraints(1, 2, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
			}
			{
				fontPanel.add(new JLabel("Size:"), new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
			}

			{
				unicodePanel = new JPanel(new GridBagLayout());
				fontPanel.add(unicodePanel, new GridBagConstraints(2, 3, 2, 1, 0.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 5), 0, 0));
				{
					boldCheckBox = new JCheckBox("Bold");
					unicodePanel.add(boldCheckBox, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					italicCheckBox = new JCheckBox("Italic");
					unicodePanel.add(italicCheckBox, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
			}
			{
				bitmapPanel = new JPanel(new GridBagLayout());
				fontPanel.add(bitmapPanel, new GridBagConstraints(2, 3, 2, 1, 1.0, 0.0, GridBagConstraints.WEST,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));
				{
					bitmapPanel.add(new JLabel("Gamma:"), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					gammaSpinner = new JSpinner(new SpinnerNumberModel(1.8f, 0, 30, 0.01));
					((JSpinner.DefaultEditor)gammaSpinner.getEditor()).getTextField().setColumns(2);
					bitmapPanel.add(gammaSpinner, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 10), 0, 0));
				}
				{
					monoCheckBox = new JCheckBox("Mono");
					bitmapPanel.add(monoCheckBox, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
			}
			{
				browseButton = new JButton("...");
				fontPanel.add(browseButton, new GridBagConstraints(3, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				browseButton.setMargin(new Insets(0, 0, 0, 0));
			}
			{
				fontPanel.add(new JLabel("Rendering:"), new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
			}
			{
				JPanel renderingPanel = new JPanel(new GridBagLayout());
				fontPanel.add(renderingPanel, new GridBagConstraints(1, 4, 3, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
				{
					freeTypeRadio = new JRadioButton("FreeType");
					renderingPanel.add(freeTypeRadio, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					javaRadio = new JRadioButton("Java");
					renderingPanel.add(javaRadio, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					nativeRadio = new JRadioButton("Native");
					renderingPanel.add(nativeRadio, new GridBagConstraints(2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
			}
			ButtonGroup buttonGroup = new ButtonGroup();
			buttonGroup.add(systemFontRadio);
			buttonGroup.add(fontFileRadio);
			buttonGroup = new ButtonGroup();
			buttonGroup.add(freeTypeRadio);
			buttonGroup.add(javaRadio);
			buttonGroup.add(nativeRadio);
			freeTypeRadio.setSelected(true);
		}
		{
			JPanel samplePanel = new JPanel();
			leftSidePanel.add(samplePanel, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(5, 0, 5, 5), 0, 0));
			samplePanel.setLayout(new GridBagLayout());
			samplePanel.setBorder(BorderFactory.createTitledBorder("Sample Text"));
			{
				JScrollPane textScroll = new JScrollPane();
				samplePanel.add(textScroll, new GridBagConstraints(0, 0, 4, 1, 1.0, 1.0, GridBagConstraints.CENTER,
					GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));
				{
					sampleTextPane = new JTextPane();
					textScroll.setViewportView(sampleTextPane);
				}
			}
			{
				sampleNeheButton = new JButton();
				sampleNeheButton.setText("NEHE");
				samplePanel.add(sampleNeheButton, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
			}
			{
				sampleAsciiButton = new JButton();
				sampleAsciiButton.setText("ASCII");
				samplePanel.add(sampleAsciiButton, new GridBagConstraints(3, 1, 1, 1, 0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
			}
			{
				sampleExtendedButton = new JButton();
				sampleExtendedButton.setText("Extended");
				samplePanel.add(sampleExtendedButton, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
			}
		}
		{
			JPanel renderingPanel = new JPanel();
			leftSidePanel.add(renderingPanel, new GridBagConstraints(0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));
			renderingPanel.setBorder(BorderFactory.createTitledBorder("Rendering"));
			renderingPanel.setLayout(new GridBagLayout());
			{
				JPanel wrapperPanel = new JPanel();
				renderingPanel.add(wrapperPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
					GridBagConstraints.BOTH, new Insets(0, 5, 5, 5), 0, 0));
				wrapperPanel.setLayout(new BorderLayout());
				wrapperPanel.setBackground(java.awt.Color.white);
				{
					gamePanel = new JPanel();
					wrapperPanel.add(gamePanel);
					gamePanel.setLayout(new BorderLayout());
					gamePanel.setBackground(java.awt.Color.white);
				}
			}
			{
				glyphCachePanel = new JPanel() {
					private int maxWidth;

					public Dimension getPreferredSize () {
						// Keep glyphCachePanel width from ever going down so the CanvasGameContainer doesn't change sizes and flicker.
						Dimension size = super.getPreferredSize();
						maxWidth = Math.max(maxWidth, size.width);
						size.width = maxWidth;
						return size;
					}
				};
				glyphCachePanel.setVisible(false);
				renderingPanel.add(glyphCachePanel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTH,
					GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
				glyphCachePanel.setLayout(new GridBagLayout());
				{
					glyphCachePanel.add(new JLabel("Glyphs:"), new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					glyphCachePanel.add(new JLabel("Pages:"), new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					glyphCachePanel.add(new JLabel("Page width:"), new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
						GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					glyphCachePanel.add(new JLabel("Page height:"), new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
						GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					glyphPageWidthCombo = new JComboBox(new DefaultComboBoxModel(new Integer[] {new Integer(32), new Integer(64),
						new Integer(128), new Integer(256), new Integer(512), new Integer(1024), new Integer(2048)}));
					glyphCachePanel.add(glyphPageWidthCombo, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
					glyphPageWidthCombo.setSelectedIndex(4);
				}
				{
					glyphPageHeightCombo = new JComboBox(new DefaultComboBoxModel(new Integer[] {new Integer(32), new Integer(64),
						new Integer(128), new Integer(256), new Integer(512), new Integer(1024), new Integer(2048)}));
					glyphCachePanel.add(glyphPageHeightCombo, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
					glyphPageHeightCombo.setSelectedIndex(4);
				}
				{
					resetCacheButton = new JButton("Reset Cache");
					glyphCachePanel.add(resetCacheButton, new GridBagConstraints(0, 6, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					glyphPagesTotalLabel = new JLabel("1");
					glyphCachePanel.add(glyphPagesTotalLabel, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					glyphsTotalLabel = new JLabel("0");
					glyphCachePanel.add(glyphsTotalLabel, new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					glyphPageComboModel = new DefaultComboBoxModel();
					glyphPageCombo = new JComboBox();
					glyphCachePanel.add(glyphPageCombo, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
					glyphPageCombo.setModel(glyphPageComboModel);
				}
				{
					glyphCachePanel.add(new JLabel("View:"), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
						GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
			}
			{
				JPanel radioButtonsPanel = new JPanel();
				renderingPanel.add(radioButtonsPanel, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
				radioButtonsPanel.setLayout(new GridBagLayout());
				{
					sampleTextRadio = new JRadioButton("Sample text");
					radioButtonsPanel.add(sampleTextRadio, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
					sampleTextRadio.setSelected(true);
				}
				{
					glyphCacheRadio = new JRadioButton("Glyph cache");
					radioButtonsPanel.add(glyphCacheRadio, new GridBagConstraints(3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				{
					radioButtonsPanel.add(new JLabel("Background:"), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
						GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 5, 5, 5), 0, 0));
				}
				{
					backgroundColorLabel = new JLabel();
					radioButtonsPanel.add(backgroundColorLabel, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
						GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
				}
				ButtonGroup buttonGroup = new ButtonGroup();
				buttonGroup.add(glyphCacheRadio);
				buttonGroup.add(sampleTextRadio);
			}
		}
		JPanel rightSidePanel = new JPanel();
		rightSidePanel.setLayout(new GridBagLayout());
		getContentPane().add(rightSidePanel, new GridBagConstraints(1, 0, 1, 2, 0.0, 0.0, GridBagConstraints.CENTER,
			GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
		{
			JPanel paddingPanel = new JPanel();
			paddingPanel.setLayout(new GridBagLayout());
			rightSidePanel.add(paddingPanel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(0, 0, 5, 5), 0, 0));
			paddingPanel.setBorder(BorderFactory.createTitledBorder("Padding"));
			{
				padTopSpinner = new JSpinner(new SpinnerNumberModel(1, 0, 999, 1));
				paddingPanel.add(padTopSpinner, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
				((JSpinner.DefaultEditor)padTopSpinner.getEditor()).getTextField().setColumns(2);
			}
			{
				padRightSpinner = new JSpinner(new SpinnerNumberModel(1, 0, 999, 1));
				paddingPanel.add(padRightSpinner, new GridBagConstraints(2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));
				((JSpinner.DefaultEditor)padRightSpinner.getEditor()).getTextField().setColumns(2);
			}
			{
				padLeftSpinner = new JSpinner(new SpinnerNumberModel(1, 0, 999, 1));
				paddingPanel.add(padLeftSpinner, new GridBagConstraints(0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0));
				((JSpinner.DefaultEditor)padLeftSpinner.getEditor()).getTextField().setColumns(2);
			}
			{
				padBottomSpinner = new JSpinner(new SpinnerNumberModel(1, 0, 999, 1));
				paddingPanel.add(padBottomSpinner, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
				((JSpinner.DefaultEditor)padBottomSpinner.getEditor()).getTextField().setColumns(2);
			}
			{
				JPanel advancePanel = new JPanel();
				FlowLayout advancePanelLayout = new FlowLayout();
				advancePanel.setLayout(advancePanelLayout);
				paddingPanel.add(advancePanel, new GridBagConstraints(0, 4, 3, 1, 1.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
				{
					advancePanel.add(new JLabel("X:"));
				}
				{
					padAdvanceXSpinner = new JSpinner(new SpinnerNumberModel(-2, -999, 999, 1));
					advancePanel.add(padAdvanceXSpinner);
					((JSpinner.DefaultEditor)padAdvanceXSpinner.getEditor()).getTextField().setColumns(2);
				}
				{
					advancePanel.add(new JLabel("Y:"));
				}
				{
					padAdvanceYSpinner = new JSpinner(new SpinnerNumberModel(-2, -999, 999, 1));
					advancePanel.add(padAdvanceYSpinner);
					((JSpinner.DefaultEditor)padAdvanceYSpinner.getEditor()).getTextField().setColumns(2);
				}
			}
		}
		{
			effectsPanel = new JPanel();
			effectsPanel.setLayout(new GridBagLayout());
			rightSidePanel.add(effectsPanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(5, 0, 5, 5), 0, 0));
			effectsPanel.setBorder(BorderFactory.createTitledBorder("Effects"));
			effectsPanel.setMinimumSize(new Dimension(210, 1));
			{
				effectsScroll = new JScrollPane();
				effectsPanel.add(effectsScroll, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.NORTH,
					GridBagConstraints.HORIZONTAL, new Insets(0, 5, 5, 5), 0, 0));
				{
					effectsListModel = new DefaultComboBoxModel();
					effectsList = new JList();
					effectsScroll.setViewportView(effectsList);
					effectsList.setModel(effectsListModel);
					effectsList.setVisibleRowCount(7);
					effectsScroll.setMinimumSize(effectsList.getPreferredScrollableViewportSize());
				}
			}
			{
				addEffectButton = new JButton("Add");
				effectsPanel.add(addEffectButton, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
					GridBagConstraints.NONE, new Insets(0, 5, 6, 5), 0, 0));
				addEffectButton.setEnabled(false);
			}
			{
				appliedEffectsScroll = new JScrollPane();
				effectsPanel.add(appliedEffectsScroll, new GridBagConstraints(1, 3, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,
					GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
				appliedEffectsScroll.setBorder(new EmptyBorder(0, 0, 0, 0));
				appliedEffectsScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
				{
					JPanel panel = new JPanel();
					panel.setLayout(new GridBagLayout());
					appliedEffectsScroll.setViewportView(panel);
					{
						appliedEffectsPanel = new JPanel();
						appliedEffectsPanel.setLayout(new GridBagLayout());
						panel.add(appliedEffectsPanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,
							GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
						appliedEffectsPanel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, java.awt.Color.black));
					}
				}
			}
		}
	}

	private void initializeMenus () {
		{
			JMenuBar menuBar = new JMenuBar();
			setJMenuBar(menuBar);
			{
				JMenu fileMenu = new JMenu();
				menuBar.add(fileMenu);
				fileMenu.setText("File");
				fileMenu.setMnemonic(KeyEvent.VK_F);
				{
					openMenuItem = new JMenuItem("Open Hiero settings file...");
					openMenuItem.setMnemonic(KeyEvent.VK_O);
					openMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK));
					fileMenu.add(openMenuItem);
				}
				{
					saveMenuItem = new JMenuItem("Save Hiero settings file...");
					saveMenuItem.setMnemonic(KeyEvent.VK_S);
					saveMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));
					fileMenu.add(saveMenuItem);
				}
				fileMenu.addSeparator();
				{
					saveBMFontMenuItem = new JMenuItem("Save BMFont files (text)...");
					saveBMFontMenuItem.setMnemonic(KeyEvent.VK_B);
					saveBMFontMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK));
					fileMenu.add(saveBMFontMenuItem);
				}
				fileMenu.addSeparator();
				{
					exitMenuItem = new JMenuItem("Exit");
					exitMenuItem.setMnemonic(KeyEvent.VK_X);
					fileMenu.add(exitMenuItem);
				}
			}
		}
	}

	static Icon getColorIcon (java.awt.Color color) {
		BufferedImage image = new BufferedImage(32, 16, BufferedImage.TYPE_INT_RGB);
		java.awt.Graphics g = image.getGraphics();
		g.setColor(color);
		g.fillRect(1, 1, 30, 14);
		g.setColor(java.awt.Color.black);
		g.drawRect(0, 0, 31, 15);
		return new ImageIcon(image);
	}

	private class EffectPanel extends JPanel {
		final java.awt.Color selectedColor = new java.awt.Color(0xb1d2e9);

		final ConfigurableEffect effect;
		List values;

		JButton upButton;
		JButton downButton;
		JButton deleteButton;
		private JPanel valuesPanel;
		JLabel nameLabel;

		GridBagConstraints constrains = new GridBagConstraints(0, -1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,
			GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);

		EffectPanel (final ConfigurableEffect effect) {
			this.effect = effect;
			effectPanels.add(this);
			effectsList.getListSelectionListeners()[0].valueChanged(null);

			setLayout(new GridBagLayout());
			setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, java.awt.Color.black));
			appliedEffectsPanel.add(this, constrains);
			{
				JPanel titlePanel = new JPanel();
				titlePanel.setLayout(new LayoutManager() {
					public void removeLayoutComponent (Component comp) {
					}

					public Dimension preferredLayoutSize (Container parent) {
						return null;
					}

					public Dimension minimumLayoutSize (Container parent) {
						return null;
					}

					public void layoutContainer (Container parent) {
						Dimension buttonSize = upButton.getPreferredSize();
						int upButtonX = getWidth() - buttonSize.width * 3 - 6 - 5;
						upButton.setBounds(upButtonX, 0, buttonSize.width, buttonSize.height);
						downButton.setBounds(getWidth() - buttonSize.width * 2 - 3 - 5, 0, buttonSize.width, buttonSize.height);
						deleteButton.setBounds(getWidth() - buttonSize.width - 5, 0, buttonSize.width, buttonSize.height);

						Dimension labelSize = nameLabel.getPreferredSize();
						nameLabel.setBounds(5, buttonSize.height / 2 - labelSize.height / 2, getWidth() - 5, labelSize.height);
					}

					public void addLayoutComponent (String name, Component comp) {
					}
				});
				{
					upButton = new JButton();
					titlePanel.add(upButton);
					upButton.setText("Up");
					upButton.setMargin(new Insets(0, 0, 0, 0));
					Font font = upButton.getFont();
					upButton.setFont(new Font(font.getName(), font.getStyle(), font.getSize() - 2));
				}
				{
					downButton = new JButton();
					titlePanel.add(downButton);
					downButton.setText("Down");
					downButton.setMargin(new Insets(0, 0, 0, 0));
					Font font = downButton.getFont();
					downButton.setFont(new Font(font.getName(), font.getStyle(), font.getSize() - 2));
				}
				{
					deleteButton = new JButton();
					titlePanel.add(deleteButton);
					deleteButton.setText("X");
					deleteButton.setMargin(new Insets(0, 0, 0, 0));
					Font font = deleteButton.getFont();
					deleteButton.setFont(new Font(font.getName(), font.getStyle(), font.getSize() - 2));
				}
				{
					nameLabel = new JLabel(effect.toString());
					titlePanel.add(nameLabel);
					Font font = nameLabel.getFont();
					nameLabel.setFont(new Font(font.getName(), Font.BOLD, font.getSize()));
				}
				titlePanel.setPreferredSize(
					new Dimension(0, Math.max(nameLabel.getPreferredSize().height, deleteButton.getPreferredSize().height)));
				add(titlePanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
					new Insets(5, 0, 0, 5), 0, 0));
				titlePanel.setOpaque(false);
			}
			{
				valuesPanel = new JPanel();
				valuesPanel.setOpaque(false);
				valuesPanel.setLayout(new GridBagLayout());
				add(valuesPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, new Insets(0, 10, 5, 0), 0, 0));
			}

			upButton.addActionListener(new ActionListener() {
				public void actionPerformed (ActionEvent evt) {
					int currentIndex = effectPanels.indexOf(EffectPanel.this);
					if (currentIndex > 0) {
						moveEffect(currentIndex - 1);
						updateFont();
						updateUpDownButtons();
					}
				}
			});

			downButton.addActionListener(new ActionListener() {
				public void actionPerformed (ActionEvent evt) {
					int currentIndex = effectPanels.indexOf(EffectPanel.this);
					if (currentIndex < effectPanels.size() - 1) {
						moveEffect(currentIndex + 1);
						updateFont();
						updateUpDownButtons();
					}
				}
			});

			deleteButton.addActionListener(new ActionListener() {
				public void actionPerformed (ActionEvent evt) {
					remove();
					updateFont();
					updateUpDownButtons();
				}
			});

			updateValues();
			updateFont();
			updateUpDownButtons();
		}

		public void remove () {
			effectPanels.remove(this);
			appliedEffectsPanel.remove(EffectPanel.this);
			getContentPane().validate();
			effectsList.getListSelectionListeners()[0].valueChanged(null);
		}

		public void updateValues () {
			prefs.put("foreground", EffectUtil.toString(colorEffect.getColor()));
			valuesPanel.removeAll();
			values = effect.getValues();
			for (Iterator iter = values.iterator(); iter.hasNext();)
				addValue((Value)iter.next());
		}

		public void updateUpDownButtons () {
			for (int index = 0; index < effectPanels.size(); index++) {
				EffectPanel effectPanel = effectPanels.get(index);
				if (index == 0) {
					effectPanel.upButton.setEnabled(false);
				} else {
					effectPanel.upButton.setEnabled(true);
				}

				if (index == effectPanels.size() - 1) {
					effectPanel.downButton.setEnabled(false);
				} else {
					effectPanel.downButton.setEnabled(true);
				}
			}
		}

		public void moveEffect (int newIndex) {
			appliedEffectsPanel.remove(this);
			effectPanels.remove(this);
			appliedEffectsPanel.add(this, constrains, newIndex);
			effectPanels.add(newIndex, this);
		}

		public void addValue (final Value value) {
			JLabel valueNameLabel = new JLabel(value.getName() + ":");
			valuesPanel.add(valueNameLabel, new GridBagConstraints(0, -1, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));

			final JLabel valueValueLabel = new JLabel();
			valuesPanel.add(valueValueLabel, new GridBagConstraints(1, -1, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
				GridBagConstraints.BOTH, new Insets(0, 0, 0, 5), 0, 0));
			valueValueLabel.setOpaque(true);
			if (value.getObject() instanceof java.awt.Color)
				valueValueLabel.setIcon(getColorIcon((java.awt.Color)value.getObject()));
			else
				valueValueLabel.setText(value.toString());

			valueValueLabel.addMouseListener(new MouseAdapter() {
				public void mouseEntered (MouseEvent evt) {
					valueValueLabel.setBackground(selectedColor);
				}

				public void mouseExited (MouseEvent evt) {
					valueValueLabel.setBackground(null);
				}

				public void mouseClicked (MouseEvent evt) {
					Object oldObject = value.getObject();
					value.showDialog();
					if (!value.getObject().equals(oldObject)) {
						effect.setValues(values);
						updateValues();
						updateFont();
					}
				}
			});
		}

		public ConfigurableEffect getEffect () {
			return effect;
		}

		public boolean equals (Object obj) {
			if (this == obj) return true;
			if (obj == null) return false;
			if (getClass() != obj.getClass()) return false;
			final EffectPanel other = (EffectPanel)obj;
			if (effect == null) {
				if (other.effect != null) return false;
			} else if (!effect.equals(other.effect)) return false;
			return true;
		}

	}

	static private class Splash extends JWindow {
		final int minMillis;
		final long startTime;

		public Splash (Frame frame, String imageFile, int minMillis) {
			super(frame);
			this.minMillis = minMillis;
			getContentPane().add(new JLabel(new ImageIcon(Splash.class.getResource(imageFile))), BorderLayout.CENTER);
			pack();
			setLocationRelativeTo(null);
			setVisible(true);
			startTime = System.currentTimeMillis();
		}

		public void close () {
			final long endTime = System.currentTimeMillis();
			new Thread(new Runnable() {
				public void run () {
					if (endTime - startTime < minMillis) {
						addMouseListener(new MouseAdapter() {
							public void mousePressed (MouseEvent evt) {
								dispose();
							}
						});
						try {
							Thread.sleep(minMillis - (endTime - startTime));
						} catch (InterruptedException ignored) {
						}
					}
					EventQueue.invokeLater(new Runnable() {
						public void run () {
							dispose();
						}
					});
				}
			}, "Splash").start();
		}
	}

	class Renderer extends ApplicationAdapter {
		SpriteBatch batch;
		int width, height;

		public void create () {
			glClearColor(0, 0, 0, 0);
			glClearDepth(1);
			glDisable(GL_LIGHTING);

			batch = new SpriteBatch();

			sampleNeheButton.doClick();
		}

		public void resize (int width, int height) {
			this.width = width;
			this.height = height;
			batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
		}

		public void render () {
			int viewWidth = Gdx.graphics.getWidth();
			int viewHeight = Gdx.graphics.getHeight();

			if (sampleTextRadio.isSelected()) {
				GL11.glClearColor(renderingBackgroundColor.r, renderingBackgroundColor.g, renderingBackgroundColor.b,
					renderingBackgroundColor.a);
				GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
			} else {
				GL11.glClearColor(1, 1, 1, 1);
				GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
			}

			String sampleText = sampleTextPane.getText();

			glEnable(GL_TEXTURE_2D);
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);
			glEnableClientState(GL_VERTEX_ARRAY);

			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

			glViewport(0, 0, width, height);
			glScissor(0, 0, width, height);

			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();
			glOrtho(0, width, height, 0, 1, -1);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();

			unicodeFont.addGlyphs(sampleText);

			if (!unicodeFont.getEffects().isEmpty() && unicodeFont.loadGlyphs(64)) {
				glyphPageComboModel.removeAllElements();
				int pageCount = unicodeFont.getGlyphPages().size();
				int glyphCount = 0;
				for (int i = 0; i < pageCount; i++) {
					glyphPageComboModel.addElement("Page " + (i + 1));
					glyphCount += ((GlyphPage)unicodeFont.getGlyphPages().get(i)).getGlyphs().size();
				}
				glyphPagesTotalLabel.setText(String.valueOf(pageCount));
				glyphsTotalLabel.setText(String.valueOf(glyphCount));
			}

			if (sampleTextRadio.isSelected()) {
				int offset = unicodeFont.getYOffset(sampleText);
				if (offset > 0) offset = 0;
				unicodeFont.drawString(10, 12, sampleText, Color.WHITE, 0, sampleText.length());
			} else {
				// GL11.glColor4f(renderingBackgroundColor.r, renderingBackgroundColor.g, renderingBackgroundColor.b,
				// renderingBackgroundColor.a);
				// fillRect(0, 0, unicodeFont.getGlyphPageWidth() + 2, unicodeFont.getGlyphPageHeight() + 2);
				int index = glyphPageCombo.getSelectedIndex();
				List pages = unicodeFont.getGlyphPages();
				if (index >= 0 && index < pages.size()) {
					Texture texture = ((GlyphPage)pages.get(glyphPageCombo.getSelectedIndex())).getTexture();

					glDisable(GL_TEXTURE_2D);
					glColor4f(renderingBackgroundColor.r, renderingBackgroundColor.g, renderingBackgroundColor.b,
						renderingBackgroundColor.a);
					glBegin(GL_QUADS);
					glVertex3f(0, 0, 0);
					glVertex3f(0, texture.getHeight(), 0);
					glVertex3f(texture.getWidth(), texture.getHeight(), 0);
					glVertex3f(texture.getWidth(), 0, 0);
					glEnd();
					glEnable(GL_TEXTURE_2D);

					texture.bind();
					glColor4f(1, 1, 1, 1);
					glBegin(GL_QUADS);
					glTexCoord2f(0, 0);
					glVertex3f(0, 0, 0);

					glTexCoord2f(0, 1);
					glVertex3f(0, texture.getHeight(), 0);

					glTexCoord2f(1, 1);
					glVertex3f(texture.getWidth(), texture.getHeight(), 0);

					glTexCoord2f(1, 0);
					glVertex3f(texture.getWidth(), 0, 0);
					glEnd();
				}
			}

			glDisable(GL_TEXTURE_2D);
			glDisableClientState(GL_TEXTURE_COORD_ARRAY);
			glDisableClientState(GL_VERTEX_ARRAY);

			if (saveBmFontFile != null) {
				try {
					BMFontUtil bmFont = new BMFontUtil(unicodeFont);
					bmFont.save(saveBmFontFile);

					if (batchMode) {
						exit(0);
					}
				} catch (Throwable ex) {
					System.out.println("Error saving BMFont files: " + saveBmFontFile.getAbsolutePath());
					ex.printStackTrace();
				} finally {
					saveBmFontFile = null;
				}
			}
		}

	}

	static final String NEHE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n" //
		+ "abcdefghijklmnopqrstuvwxyz\n1234567890 \n" //
		+ "\"!`?'.,;:()[]{}<>|/@\\^$-%+=#_&~*\u0000\u007F";
	static public final String EXTENDED_CHARS;

	static {
		StringBuilder buffer = new StringBuilder();
		int i = 0;
		for (int c : new int[] {0, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
			57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
			87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
			114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170,
			171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194,
			195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
			219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242,
			243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266,
			267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290,
			291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314,
			315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
			339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362,
			363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 884, 885, 890,
			891, 892, 893, 894, 900, 901, 902, 903, 904, 905, 906, 908, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921,
			922, 923, 924, 925, 926, 927, 928, 929, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946,
			947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970,
			971, 972, 973, 974, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995,
			996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016,
			1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036,
			1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056,
			1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076,
			1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096,
			1097, 1098, 1099, 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116,
			1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136,
			1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156,
			1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176,
			1177, 1178, 1179, 1180, 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1196,
			1197, 1198, 1199, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216,
			1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236,
			1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256,
			1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276,
			1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296,
			1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316,
			1317, 1318, 1319, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205, 8206, 8207, 8210,
			8211, 8212, 8213, 8214, 8215, 8216, 8217, 8218, 8219, 8220, 8221, 8222, 8223, 8224, 8225, 8226, 8230, 8234, 8235, 8236,
			8237, 8238, 8239, 8240, 8242, 8243, 8244, 8249, 8250, 8252, 8254, 8260, 8286, 8298, 8299, 8300, 8301, 8302, 8303, 8352,
			8353, 8354, 8355, 8356, 8357, 8358, 8359, 8360, 8361, 8363, 8364, 8365, 8366, 8367, 8368, 8369, 8370, 8371, 8372, 8373,
			8377, 8378, 11360, 11361, 11362, 11363, 11364, 11365, 11366, 11367, 11368, 11369, 11370, 11371, 11372, 11373, 11377,
			11378, 11379, 11380, 11381, 11382, 11383}) {
			i++;
			if (i > 26) {
				i = 0;
				buffer.append("\r\n");
			}
			buffer.append((char)c);
		}
		EXTENDED_CHARS = buffer.toString();
	}

	public static void main (final String[] args) throws Exception {
		SwingUtilities.invokeLater(new Runnable() {
			public void run () {
				new Hiero(args);
			}
		});
	}
}
